@humanforest/charts 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/FSparkline.vue +16 -23
- package/src/sparklineDomain.ts +25 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanforest/charts",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"vue": "^3.5.22"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@humanforest/tokens": "^0.3.
|
|
27
|
+
"@humanforest/tokens": "^0.3.5"
|
|
28
28
|
},
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
package/src/FSparkline.vue
CHANGED
|
@@ -18,6 +18,7 @@ import { VisXYContainer, VisLine, VisArea } from '@unovis/vue';
|
|
|
18
18
|
import { CurveType } from '@unovis/ts';
|
|
19
19
|
import { motionDuration } from './motionDuration';
|
|
20
20
|
import { useThemeVersion } from './useThemeVersion';
|
|
21
|
+
import { sparklineDomain } from './sparklineDomain';
|
|
21
22
|
|
|
22
23
|
const props = withDefaults(
|
|
23
24
|
defineProps<{
|
|
@@ -52,7 +53,7 @@ const props = withDefaults(
|
|
|
52
53
|
endpoint?: boolean;
|
|
53
54
|
/**
|
|
54
55
|
* Takes the height its container gives it, with `height` as the floor. For a graphic in a space
|
|
55
|
-
* its surroundings have made taller (a KPI
|
|
56
|
+
* its surroundings have made taller (a KPI's chart row that a stretched card or a group has given
|
|
56
57
|
* more height), where the chart is the part that should grow. Works for both marks.
|
|
57
58
|
*/
|
|
58
59
|
fill?: boolean;
|
|
@@ -100,23 +101,18 @@ type Point = { i: number; v: number | null; c: number | null };
|
|
|
100
101
|
const points = computed<Point[]>(() =>
|
|
101
102
|
props.data.map((v, i) => ({ i, v, c: props.compare?.[i] ?? null })),
|
|
102
103
|
);
|
|
104
|
+
// Readings are plotted as their height above the frame's floor, which is 0 except under `area`.
|
|
103
105
|
const x = (d: Point) => d.i;
|
|
104
|
-
const y = (d: Point) => d.v;
|
|
105
|
-
const yCompare = (d: Point) => d.c;
|
|
106
|
+
const y = (d: Point) => (d.v === null ? null : d.v - frame.value.floor);
|
|
107
|
+
const yCompare = (d: Point) => (d.c === null ? null : d.c - frame.value.floor);
|
|
106
108
|
|
|
107
109
|
const hasCompare = computed(() => !!props.compare?.some((v) => v !== null && v !== undefined));
|
|
108
110
|
|
|
109
111
|
// The line marks share one scale, framed by the floor and ceiling of everything drawn on it — a
|
|
110
|
-
// line reads as a shape, and a shape wants its own range.
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
const
|
|
114
|
-
const vals = [...props.data, ...(props.compare ?? [])].filter((v): v is number => typeof v === 'number');
|
|
115
|
-
return vals.length ? [Math.min(...vals), Math.max(...vals)] : null;
|
|
116
|
-
});
|
|
117
|
-
// An area is a band from its baseline to baseline + y, not a fill up to y, so reaching the line
|
|
118
|
-
// from the floor of the frame means passing the height above that floor.
|
|
119
|
-
const yFromFloor = (d: Point) => (d.v === null || !domain.value ? null : d.v - domain.value[0]);
|
|
112
|
+
// line reads as a shape, and a shape wants its own range. The frame, and why `area` shifts it to
|
|
113
|
+
// start at 0, live in sparklineDomain. An area is a band from its baseline to baseline + y, so with
|
|
114
|
+
// the floor at 0 it runs from 0 up to each reading's height above the floor.
|
|
115
|
+
const frame = computed(() => sparklineDomain(props.data, props.compare, props.area));
|
|
120
116
|
|
|
121
117
|
// Bars are the exception: they are measured from zero, not from the data's own floor. A bar reads
|
|
122
118
|
// as a LENGTH, and a bar that starts anywhere but zero has a length no longer proportional to its
|
|
@@ -142,9 +138,6 @@ const barFor = (v: number | null) => {
|
|
|
142
138
|
// A reading of zero keeps a hairline. Zero is a real measurement and a gap is the absence of
|
|
143
139
|
// one; collapsing the bar to nothing would render the two identically.
|
|
144
140
|
height: `max(${(to - from) * 100}%, 1px)`,
|
|
145
|
-
// The cap goes on the free end, so a bar below the baseline rounds downward rather than into
|
|
146
|
-
// the axis it grows from.
|
|
147
|
-
negative: v < 0,
|
|
148
141
|
};
|
|
149
142
|
};
|
|
150
143
|
|
|
@@ -154,9 +147,9 @@ const last = computed(() => {
|
|
|
154
147
|
});
|
|
155
148
|
// Fraction of the height the final point sits at, for the endpoint dot.
|
|
156
149
|
const endpointTop = computed(() => {
|
|
157
|
-
const [lo, hi] =
|
|
150
|
+
const [lo, hi] = frame.value.domain ?? [0, 0];
|
|
158
151
|
if (last.value === null || hi === lo) return 50;
|
|
159
|
-
return (1 - (last.value - lo) / (hi - lo)) * 100;
|
|
152
|
+
return (1 - (last.value - frame.value.floor - lo) / (hi - lo)) * 100;
|
|
160
153
|
});
|
|
161
154
|
</script>
|
|
162
155
|
|
|
@@ -167,7 +160,8 @@ const endpointTop = computed(() => {
|
|
|
167
160
|
The 2px gap is fixed, so the bucket count sets how thin the bars get: n buckets across w
|
|
168
161
|
pixels leaves (w - 2(n - 1)) / n each. A tile 280px wide holds 12 buckets at 21px and 60 at
|
|
169
162
|
2.7px — still bars; past roughly 60 the gap outweighs the bar and the row reads as a
|
|
170
|
-
texture, where the measure belongs to a line instead.
|
|
163
|
+
texture, where the measure belongs to a line instead. Each bar is rounded at both ends:
|
|
164
|
+
no baseline is drawn for a square end to sit on. -->
|
|
171
165
|
<div v-if="mark === 'bar'" class="flex w-full items-end gap-0.5" :class="fill ? 'absolute inset-0' : 'h-full'" aria-hidden="true">
|
|
172
166
|
<span
|
|
173
167
|
v-for="(v, i) in data"
|
|
@@ -176,8 +170,7 @@ const endpointTop = computed(() => {
|
|
|
176
170
|
>
|
|
177
171
|
<span
|
|
178
172
|
v-if="barFor(v)"
|
|
179
|
-
class="absolute inset-x-0 bg-current"
|
|
180
|
-
:class="barFor(v)!.negative ? 'rounded-b-[3px]' : 'rounded-t-[3px]'"
|
|
173
|
+
class="absolute inset-x-0 rounded-[3px] bg-current"
|
|
181
174
|
:style="{ bottom: barFor(v)!.bottom, height: barFor(v)!.height }"
|
|
182
175
|
/>
|
|
183
176
|
</span>
|
|
@@ -198,7 +191,7 @@ const endpointTop = computed(() => {
|
|
|
198
191
|
:data="points"
|
|
199
192
|
:height="fill ? undefined : height"
|
|
200
193
|
:margin="{ top: 2, right: endpoint ? 4 : 0, bottom: 0, left: 0 }"
|
|
201
|
-
:y-domain="domain ?? undefined"
|
|
194
|
+
:y-domain="frame.domain ?? undefined"
|
|
202
195
|
:duration="motionDuration('slow')"
|
|
203
196
|
>
|
|
204
197
|
<!-- Comparison first, so the current series draws over it. Dashed and thinner: the dash says
|
|
@@ -214,7 +207,7 @@ const endpointTop = computed(() => {
|
|
|
214
207
|
:opacity="0.45"
|
|
215
208
|
:curve-type="curve ? CurveType.MonotoneX : CurveType.Linear"
|
|
216
209
|
/>
|
|
217
|
-
<VisArea v-if="area" :x="x" :y="
|
|
210
|
+
<VisArea v-if="area" :x="x" :y="y" :baseline="0" :color="color" :opacity="0.12" :curve-type="curve ? CurveType.MonotoneX : CurveType.Linear" />
|
|
218
211
|
<VisLine :x="x" :y="y" :color="color" :line-width="1.5" :curve-type="curve ? CurveType.MonotoneX : CurveType.Linear" />
|
|
219
212
|
</VisXYContainer>
|
|
220
213
|
<span
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frame a sparkline's line marks are plotted in: the floor and ceiling of everything drawn,
|
|
3
|
+
* shared by the series and its comparison, or null when there is nothing to draw so the container
|
|
4
|
+
* keeps its own fallback. Set by hand because an Unovis area is measured from a baseline: left to
|
|
5
|
+
* itself, `area` pulls the floor to zero and flattens the line it is filling under.
|
|
6
|
+
*
|
|
7
|
+
* Under `area` the frame starts at 0 and every reading is plotted as its height above the floor.
|
|
8
|
+
* Unovis insets a line's scale by half its stroke unless the domain starts at 0, and the area shares
|
|
9
|
+
* that scale, so from a non-zero floor the fill stopped 0.75px above the bottom edge: a hairline of
|
|
10
|
+
* card under the chart. From 0 the fill meets the edge, and the line's lowest point gives the lower
|
|
11
|
+
* half of its stroke to that edge, where the graphic sits anyway. A plain line keeps its range and
|
|
12
|
+
* its whole stroke.
|
|
13
|
+
*/
|
|
14
|
+
export function sparklineDomain(
|
|
15
|
+
data: (number | null)[],
|
|
16
|
+
compare: (number | null)[] | undefined,
|
|
17
|
+
area: boolean,
|
|
18
|
+
): { floor: number; domain: [number, number] | null } {
|
|
19
|
+
const vals = [...data, ...(compare ?? [])].filter((v): v is number => typeof v === 'number');
|
|
20
|
+
if (!vals.length) return { floor: 0, domain: null };
|
|
21
|
+
const lo = Math.min(...vals);
|
|
22
|
+
const hi = Math.max(...vals);
|
|
23
|
+
const floor = area ? lo : 0;
|
|
24
|
+
return { floor, domain: [lo - floor, hi - floor] };
|
|
25
|
+
}
|