@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.
@@ -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
- const yMin = $derived(min ?? Math.min(...values))
40
- const yMax = $derived(max ?? Math.max(...values))
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={yScale(v)}
196
+ y={top}
106
197
  width={barWidth}
107
- height={height - yScale(v)}
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={yScale(v)}
204
+ y={top}
114
205
  width={barWidth}
115
- height={height - yScale(v)}
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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/chart",
3
- "version": "1.3.12",
3
+ "version": "1.3.14",
4
4
  "type": "module",
5
5
  "description": "Data-driven chart components",
6
6
  "repository": {