@rokkit/chart 1.3.12 → 1.3.14
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/dist/Sparkline.svelte +144 -7
- package/dist/Sparkline.svelte.d.ts +11 -0
- package/package.json +1 -1
package/dist/Sparkline.svelte
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
import { line as d3line, area as d3area, curveCatmullRom } from 'd3-shape'
|
|
4
4
|
import PatternDef from './patterns/PatternDef.svelte'
|
|
5
5
|
import { PATTERNS } from './patterns/patterns.js'
|
|
6
|
+
import { resolveHighlight } from './lib/highlight.js'
|
|
7
|
+
import { computeTrend } from './lib/trend.js'
|
|
8
|
+
|
|
9
|
+
type HighlightSelector =
|
|
10
|
+
| 'first'
|
|
11
|
+
| 'last'
|
|
12
|
+
| 'min'
|
|
13
|
+
| 'max'
|
|
14
|
+
| number
|
|
15
|
+
| ((row: { x: number; y: number }, i: number) => boolean)
|
|
16
|
+
|
|
17
|
+
type TrendMethod = string | number | { type: string; [k: string]: unknown }
|
|
6
18
|
|
|
7
19
|
type Props = {
|
|
8
20
|
data?: number[] | Record<string, unknown>[]
|
|
@@ -15,6 +27,9 @@
|
|
|
15
27
|
height?: number
|
|
16
28
|
min?: number
|
|
17
29
|
max?: number
|
|
30
|
+
baseline?: number
|
|
31
|
+
highlight?: HighlightSelector | HighlightSelector[]
|
|
32
|
+
trend?: TrendMethod | TrendMethod[]
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
let {
|
|
@@ -27,7 +42,10 @@
|
|
|
27
42
|
width = 80,
|
|
28
43
|
height = 24,
|
|
29
44
|
min = undefined,
|
|
30
|
-
max = undefined
|
|
45
|
+
max = undefined,
|
|
46
|
+
baseline = undefined,
|
|
47
|
+
highlight = undefined,
|
|
48
|
+
trend = undefined
|
|
31
49
|
}: Props = $props()
|
|
32
50
|
|
|
33
51
|
const values = $derived(
|
|
@@ -36,8 +54,24 @@
|
|
|
36
54
|
)
|
|
37
55
|
)
|
|
38
56
|
|
|
39
|
-
|
|
40
|
-
const
|
|
57
|
+
// Adapter to the pure geom utilities (they operate on {x,y} rows).
|
|
58
|
+
const rows = $derived(values.map((v, i) => ({ x: i, y: v })))
|
|
59
|
+
|
|
60
|
+
const hasNegative = $derived(values.some((v) => v < 0))
|
|
61
|
+
// Bars with negative values are meaningless when measured from the pixel bottom, so a
|
|
62
|
+
// bar sparkline auto-anchors at 0 when any value is negative. Everything else is opt-in.
|
|
63
|
+
const effectiveBaseline = $derived(baseline ?? (type === 'bar' && hasNegative ? 0 : undefined))
|
|
64
|
+
|
|
65
|
+
const rawMin = $derived(Math.min(...values))
|
|
66
|
+
const rawMax = $derived(Math.max(...values))
|
|
67
|
+
// Explicit min/max win; otherwise extend the auto domain to include the baseline so it
|
|
68
|
+
// (and the full negative extent) stay on-canvas.
|
|
69
|
+
const yMin = $derived(
|
|
70
|
+
min ?? (effectiveBaseline !== undefined ? Math.min(rawMin, effectiveBaseline) : rawMin)
|
|
71
|
+
)
|
|
72
|
+
const yMax = $derived(
|
|
73
|
+
max ?? (effectiveBaseline !== undefined ? Math.max(rawMax, effectiveBaseline) : rawMax)
|
|
74
|
+
)
|
|
41
75
|
|
|
42
76
|
const xScale = $derived(
|
|
43
77
|
scaleLinear()
|
|
@@ -46,6 +80,60 @@
|
|
|
46
80
|
)
|
|
47
81
|
const yScale = $derived(scaleLinear().domain([yMin, yMax]).range([height, 0]))
|
|
48
82
|
|
|
83
|
+
// When no baseline is in effect, anchor at the pixel bottom — preserves the classic
|
|
84
|
+
// min-anchored sparkbar look (Math.min/abs below both collapse to the old formula).
|
|
85
|
+
const barAnchorY = $derived(effectiveBaseline !== undefined ? yScale(effectiveBaseline) : height)
|
|
86
|
+
|
|
87
|
+
const highlightSelectors = $derived(
|
|
88
|
+
Array.isArray(highlight)
|
|
89
|
+
? highlight
|
|
90
|
+
: highlight === null || highlight === undefined
|
|
91
|
+
? []
|
|
92
|
+
: [highlight]
|
|
93
|
+
)
|
|
94
|
+
const highlightIndices = $derived.by(() => {
|
|
95
|
+
const seen = new Set<number>()
|
|
96
|
+
for (const sel of highlightSelectors) {
|
|
97
|
+
for (const i of resolveHighlight(rows, sel, { y: 'y' })) seen.add(i)
|
|
98
|
+
}
|
|
99
|
+
return [...seen]
|
|
100
|
+
})
|
|
101
|
+
const highlightMarks = $derived(
|
|
102
|
+
highlightIndices
|
|
103
|
+
.map((i) => ({ i, cx: xScale(i), cy: yScale(values[i]) }))
|
|
104
|
+
.filter((m) => Number.isFinite(m.cx) && Number.isFinite(m.cy))
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const trendMethods = $derived(
|
|
108
|
+
Array.isArray(trend) ? trend : trend === null || trend === undefined ? [] : [trend]
|
|
109
|
+
)
|
|
110
|
+
const typeOf = (m: TrendMethod) => {
|
|
111
|
+
const t = typeof m === 'string' ? m : typeof m === 'number' ? 'value' : (m?.type ?? '')
|
|
112
|
+
return t === 'mean' ? 'avg' : t
|
|
113
|
+
}
|
|
114
|
+
const trendPaths = $derived.by(() => {
|
|
115
|
+
const out: { d: string; type: string; i: number }[] = []
|
|
116
|
+
trendMethods.forEach((m, idx) => {
|
|
117
|
+
const res = computeTrend(rows, { x: 'x', y: 'y' }, m)
|
|
118
|
+
if (!res) return
|
|
119
|
+
if (res.kind === 'constant') {
|
|
120
|
+
const yy = yScale(res.value)
|
|
121
|
+
if (Number.isFinite(yy))
|
|
122
|
+
out.push({ d: `M0,${yy} L${width},${yy}`, type: typeOf(m), i: idx })
|
|
123
|
+
} else {
|
|
124
|
+
const pts = res.values
|
|
125
|
+
.map((v, i) => ({ vx: xScale(i), vy: yScale(v) }))
|
|
126
|
+
.filter((p) => Number.isFinite(p.vx) && Number.isFinite(p.vy))
|
|
127
|
+
const gen = d3line<{ vx: number; vy: number }>()
|
|
128
|
+
.x((p) => p.vx)
|
|
129
|
+
.y((p) => p.vy)
|
|
130
|
+
const d = gen(pts)
|
|
131
|
+
if (d) out.push({ d, type: typeOf(m), i: idx })
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
return out
|
|
135
|
+
})
|
|
136
|
+
|
|
49
137
|
const linePath = $derived.by(() => {
|
|
50
138
|
const gen = d3line<number>()
|
|
51
139
|
.x((_, i) => xScale(i))
|
|
@@ -100,23 +188,72 @@
|
|
|
100
188
|
/>
|
|
101
189
|
{:else if type === 'bar'}
|
|
102
190
|
{#each values as v, i (i)}
|
|
191
|
+
{@const vy = yScale(v)}
|
|
192
|
+
{@const top = Math.min(vy, barAnchorY)}
|
|
193
|
+
{@const barHeight = Math.abs(vy - barAnchorY)}
|
|
103
194
|
<rect
|
|
104
195
|
x={xScale(i) - barWidth / 2}
|
|
105
|
-
y={
|
|
196
|
+
y={top}
|
|
106
197
|
width={barWidth}
|
|
107
|
-
height={
|
|
198
|
+
height={barHeight}
|
|
108
199
|
fill={strokeColor}
|
|
109
200
|
/>
|
|
110
201
|
{#if patternMarks}
|
|
111
202
|
<rect
|
|
112
203
|
x={xScale(i) - barWidth / 2}
|
|
113
|
-
y={
|
|
204
|
+
y={top}
|
|
114
205
|
width={barWidth}
|
|
115
|
-
height={
|
|
206
|
+
height={barHeight}
|
|
116
207
|
fill="url(#{patternId})"
|
|
117
208
|
pointer-events="none"
|
|
118
209
|
/>
|
|
119
210
|
{/if}
|
|
120
211
|
{/each}
|
|
121
212
|
{/if}
|
|
213
|
+
|
|
214
|
+
{#if trendPaths.length}
|
|
215
|
+
<g data-plot-geom="trend">
|
|
216
|
+
{#each trendPaths as p (p.i)}
|
|
217
|
+
<path d={p.d} data-plot-trend={p.type} />
|
|
218
|
+
{/each}
|
|
219
|
+
</g>
|
|
220
|
+
{/if}
|
|
221
|
+
|
|
222
|
+
{#if effectiveBaseline !== undefined}
|
|
223
|
+
<line x1={0} y1={barAnchorY} x2={width} y2={barAnchorY} data-plot-baseline />
|
|
224
|
+
{/if}
|
|
225
|
+
|
|
226
|
+
{#if highlightMarks.length}
|
|
227
|
+
<g data-plot-geom="highlight">
|
|
228
|
+
{#each highlightMarks as m (m.i)}
|
|
229
|
+
<circle cx={m.cx} cy={m.cy} data-plot-highlight />
|
|
230
|
+
{/each}
|
|
231
|
+
</g>
|
|
232
|
+
{/if}
|
|
122
233
|
</svg>
|
|
234
|
+
|
|
235
|
+
<style>
|
|
236
|
+
[data-plot-baseline] {
|
|
237
|
+
stroke: var(--chart-baseline-color, currentColor);
|
|
238
|
+
stroke-width: var(--chart-baseline-width, 1);
|
|
239
|
+
stroke-dasharray: var(--chart-baseline-dash, 4 4);
|
|
240
|
+
opacity: var(--chart-baseline-opacity, 0.5);
|
|
241
|
+
pointer-events: none;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
[data-plot-highlight] {
|
|
245
|
+
fill: var(--chart-highlight-color, rgb(var(--color-accent-500, 194 65 12)));
|
|
246
|
+
stroke: var(--chart-highlight-ring, none);
|
|
247
|
+
r: var(--chart-highlight-radius, 3);
|
|
248
|
+
pointer-events: none;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
[data-plot-trend] {
|
|
252
|
+
fill: none;
|
|
253
|
+
stroke: var(--chart-trend-color, currentColor);
|
|
254
|
+
stroke-width: var(--chart-trend-width, 1);
|
|
255
|
+
stroke-dasharray: var(--chart-trend-dash, 4 4);
|
|
256
|
+
opacity: var(--chart-trend-opacity, 0.7);
|
|
257
|
+
pointer-events: none;
|
|
258
|
+
}
|
|
259
|
+
</style>
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { PATTERNS } from './patterns/patterns.js';
|
|
2
|
+
type HighlightSelector = 'first' | 'last' | 'min' | 'max' | number | ((row: {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}, i: number) => boolean);
|
|
6
|
+
type TrendMethod = string | number | {
|
|
7
|
+
type: string;
|
|
8
|
+
[k: string]: unknown;
|
|
9
|
+
};
|
|
2
10
|
type Props = {
|
|
3
11
|
data?: number[] | Record<string, unknown>[];
|
|
4
12
|
field?: string;
|
|
@@ -10,6 +18,9 @@ type Props = {
|
|
|
10
18
|
height?: number;
|
|
11
19
|
min?: number;
|
|
12
20
|
max?: number;
|
|
21
|
+
baseline?: number;
|
|
22
|
+
highlight?: HighlightSelector | HighlightSelector[];
|
|
23
|
+
trend?: TrendMethod | TrendMethod[];
|
|
13
24
|
};
|
|
14
25
|
declare const Sparkline: import("svelte").Component<Props, {}, "">;
|
|
15
26
|
type Sparkline = ReturnType<typeof Sparkline>;
|