@humanforest/charts 0.3.2 → 0.3.4
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 +28 -21
- 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.4",
|
|
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.4"
|
|
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<{
|
|
@@ -50,6 +51,12 @@ const props = withDefaults(
|
|
|
50
51
|
area?: boolean;
|
|
51
52
|
/** Marks the last point, so the series has an end rather than running off the edge. */
|
|
52
53
|
endpoint?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Takes the height its container gives it, with `height` as the floor. For a graphic in a space
|
|
56
|
+
* its surroundings have made taller (a KPI's chart row that a stretched card or a group has given
|
|
57
|
+
* more height), where the chart is the part that should grow. Works for both marks.
|
|
58
|
+
*/
|
|
59
|
+
fill?: boolean;
|
|
53
60
|
/**
|
|
54
61
|
* Monotone interpolation, on by default. Monotone rather than a plain spline: it stays inside
|
|
55
62
|
* the data's own range, so no peak appears between two points that neither of them reached.
|
|
@@ -71,7 +78,7 @@ const props = withDefaults(
|
|
|
71
78
|
*/
|
|
72
79
|
mark?: 'line' | 'bar';
|
|
73
80
|
}>(),
|
|
74
|
-
{ height: 32, color: 'var(--ui-primary)', area: false, endpoint: false, curve: true, mark: 'line', compare: undefined, compareColor: undefined },
|
|
81
|
+
{ height: 32, color: 'var(--ui-primary)', area: false, endpoint: false, fill: false, curve: true, mark: 'line', compare: undefined, compareColor: undefined },
|
|
75
82
|
);
|
|
76
83
|
|
|
77
84
|
if (import.meta.env.DEV) {
|
|
@@ -94,23 +101,18 @@ type Point = { i: number; v: number | null; c: number | null };
|
|
|
94
101
|
const points = computed<Point[]>(() =>
|
|
95
102
|
props.data.map((v, i) => ({ i, v, c: props.compare?.[i] ?? null })),
|
|
96
103
|
);
|
|
104
|
+
// Readings are plotted as their height above the frame's floor, which is 0 except under `area`.
|
|
97
105
|
const x = (d: Point) => d.i;
|
|
98
|
-
const y = (d: Point) => d.v;
|
|
99
|
-
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);
|
|
100
108
|
|
|
101
109
|
const hasCompare = computed(() => !!props.compare?.some((v) => v !== null && v !== undefined));
|
|
102
110
|
|
|
103
111
|
// The line marks share one scale, framed by the floor and ceiling of everything drawn on it — a
|
|
104
|
-
// line reads as a shape, and a shape wants its own range.
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
const
|
|
108
|
-
const vals = [...props.data, ...(props.compare ?? [])].filter((v): v is number => typeof v === 'number');
|
|
109
|
-
return vals.length ? [Math.min(...vals), Math.max(...vals)] : null;
|
|
110
|
-
});
|
|
111
|
-
// An area is a band from its baseline to baseline + y, not a fill up to y, so reaching the line
|
|
112
|
-
// from the floor of the frame means passing the height above that floor.
|
|
113
|
-
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));
|
|
114
116
|
|
|
115
117
|
// Bars are the exception: they are measured from zero, not from the data's own floor. A bar reads
|
|
116
118
|
// as a LENGTH, and a bar that starts anywhere but zero has a length no longer proportional to its
|
|
@@ -148,21 +150,21 @@ const last = computed(() => {
|
|
|
148
150
|
});
|
|
149
151
|
// Fraction of the height the final point sits at, for the endpoint dot.
|
|
150
152
|
const endpointTop = computed(() => {
|
|
151
|
-
const [lo, hi] =
|
|
153
|
+
const [lo, hi] = frame.value.domain ?? [0, 0];
|
|
152
154
|
if (last.value === null || hi === lo) return 50;
|
|
153
|
-
return (1 - (last.value - lo) / (hi - lo)) * 100;
|
|
155
|
+
return (1 - (last.value - frame.value.floor - lo) / (hi - lo)) * 100;
|
|
154
156
|
});
|
|
155
157
|
</script>
|
|
156
158
|
|
|
157
159
|
<template>
|
|
158
|
-
<div class="relative w-full" :style="{ height: `${height}px`, color }">
|
|
160
|
+
<div class="relative w-full" :class="fill ? 'h-full' : ''" :style="fill ? { minHeight: `${height}px`, color } : { height: `${height}px`, color }">
|
|
159
161
|
<!-- Plain DOM rather than a mark: there are no axes, scales or tooltip to share with the XY
|
|
160
162
|
container, so a flex row of columns is the whole implementation and costs no chart runtime.
|
|
161
163
|
The 2px gap is fixed, so the bucket count sets how thin the bars get: n buckets across w
|
|
162
164
|
pixels leaves (w - 2(n - 1)) / n each. A tile 280px wide holds 12 buckets at 21px and 60 at
|
|
163
165
|
2.7px — still bars; past roughly 60 the gap outweighs the bar and the row reads as a
|
|
164
166
|
texture, where the measure belongs to a line instead. -->
|
|
165
|
-
<div v-if="mark === 'bar'" class="flex
|
|
167
|
+
<div v-if="mark === 'bar'" class="flex w-full items-end gap-0.5" :class="fill ? 'absolute inset-0' : 'h-full'" aria-hidden="true">
|
|
166
168
|
<span
|
|
167
169
|
v-for="(v, i) in data"
|
|
168
170
|
:key="i"
|
|
@@ -180,14 +182,19 @@ const endpointTop = computed(() => {
|
|
|
180
182
|
<!-- No bottom margin: the graphic sits on the tile's own bottom edge, and 2px of clearance
|
|
181
183
|
there reads as a misaligned card rather than as breathing room. The top keeps its 2px so a
|
|
182
184
|
peak is not clipped by the stroke's own width, and the right reserves room for the endpoint
|
|
183
|
-
dot when there is one.
|
|
185
|
+
dot when there is one. Under `fill` the container is laid over the root rather than sizing
|
|
186
|
+
it: the root takes the space it is given, floored at `height`, and Unovis measures that box,
|
|
187
|
+
never its own SVG (which would feed back) and never its 300px fallback for an empty box.
|
|
188
|
+
An inline style, not a utility: Unovis's own stylesheet pins `.unovis-xy-container` to
|
|
189
|
+
`position: relative`, and a class loses to it. -->
|
|
184
190
|
<VisXYContainer
|
|
185
191
|
v-else
|
|
186
192
|
:key="themeVersion"
|
|
193
|
+
:style="fill ? { position: 'absolute', inset: '0' } : undefined"
|
|
187
194
|
:data="points"
|
|
188
|
-
:height="height"
|
|
195
|
+
:height="fill ? undefined : height"
|
|
189
196
|
:margin="{ top: 2, right: endpoint ? 4 : 0, bottom: 0, left: 0 }"
|
|
190
|
-
:y-domain="domain ?? undefined"
|
|
197
|
+
:y-domain="frame.domain ?? undefined"
|
|
191
198
|
:duration="motionDuration('slow')"
|
|
192
199
|
>
|
|
193
200
|
<!-- Comparison first, so the current series draws over it. Dashed and thinner: the dash says
|
|
@@ -203,7 +210,7 @@ const endpointTop = computed(() => {
|
|
|
203
210
|
:opacity="0.45"
|
|
204
211
|
:curve-type="curve ? CurveType.MonotoneX : CurveType.Linear"
|
|
205
212
|
/>
|
|
206
|
-
<VisArea v-if="area" :x="x" :y="
|
|
213
|
+
<VisArea v-if="area" :x="x" :y="y" :baseline="0" :color="color" :opacity="0.12" :curve-type="curve ? CurveType.MonotoneX : CurveType.Linear" />
|
|
207
214
|
<VisLine :x="x" :y="y" :color="color" :line-width="1.5" :curve-type="curve ? CurveType.MonotoneX : CurveType.Linear" />
|
|
208
215
|
</VisXYContainer>
|
|
209
216
|
<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
|
+
}
|