@rokkit/chart 1.3.6 → 1.3.7

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.
@@ -0,0 +1,105 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte'
3
+ import type { PlotState } from '../PlotState.svelte.js'
4
+ import { resolveHighlight } from '../lib/highlight.js'
5
+ import { scalePos } from '../lib/scale.js'
6
+
7
+ type Row = Record<string, unknown>
8
+ type Props = {
9
+ x?: string
10
+ y?: string
11
+ highlight?: 'first' | 'last' | 'min' | 'max' | number | ((row: Row, i: number) => boolean)
12
+ label?: boolean | string | ((row: Row) => unknown)
13
+ }
14
+ let { x, y, highlight = undefined, label = false }: Props = $props()
15
+
16
+ const state = getContext<PlotState>('plot-state')
17
+
18
+ const marks = $derived.by(() => {
19
+ const rows = state?.data ?? []
20
+ const xs = state?.xScale
21
+ const ys = state?.yScale
22
+ if (!xs || !ys || !x || !y) return []
23
+ const selectedRows = state?.selectedRows ?? []
24
+ const selectedSet = new Set(selectedRows)
25
+ const out = []
26
+ const seen = new Set()
27
+ if (highlight !== null && highlight !== undefined && rows.length) {
28
+ for (const i of resolveHighlight(rows, highlight, { y })) {
29
+ const row = rows[i]
30
+ out.push({
31
+ key: `h${i}`,
32
+ cx: scalePos(xs, row[x]),
33
+ cy: ys(Number(row[y])),
34
+ row,
35
+ // A statically-highlighted point that is also selected renders as
36
+ // selected — selection is the stronger, interactive state.
37
+ selected: selectedSet.has(row)
38
+ })
39
+ seen.add(row)
40
+ }
41
+ }
42
+ selectedRows.forEach((row, j) => {
43
+ if (seen.has(row)) return
44
+ out.push({
45
+ key: `s${j}`,
46
+ cx: scalePos(xs, row[x]),
47
+ cy: ys(Number(row[y])),
48
+ row,
49
+ selected: true
50
+ })
51
+ })
52
+ return out.filter((m) => Number.isFinite(m.cx) && Number.isFinite(m.cy))
53
+ })
54
+
55
+ function resolveLabel(row: Row): string | null {
56
+ if (!label) return null
57
+ if (label === true) return String((y ? row[y] : '') ?? '')
58
+ if (typeof label === 'function') return String(label(row) ?? '')
59
+ return String(row[label] ?? '')
60
+ }
61
+ </script>
62
+
63
+ {#if marks.length}
64
+ <g data-plot-geom="highlight">
65
+ {#each marks as m (m.key)}
66
+ <circle
67
+ cx={m.cx}
68
+ cy={m.cy}
69
+ data-plot-highlight
70
+ data-plot-selected={m.selected ? 'true' : undefined}
71
+ />
72
+ {#if label}
73
+ {@const text = resolveLabel(m.row)}
74
+ {#if text}
75
+ <text x={m.cx} y={m.cy} dy="-8" text-anchor="middle" data-plot-highlight-label
76
+ >{text}</text
77
+ >
78
+ {/if}
79
+ {/if}
80
+ {/each}
81
+ </g>
82
+ {/if}
83
+
84
+ <style>
85
+ [data-plot-highlight] {
86
+ fill: var(--chart-highlight-color, rgb(var(--color-accent-500, 194 65 12)));
87
+ stroke: var(--chart-highlight-ring, none);
88
+ r: var(--chart-highlight-radius, 4);
89
+ /* Visual only — let clicks pass through to the data hit target beneath so the
90
+ highlighted/selected observation stays selectable (and de-selectable). */
91
+ pointer-events: none;
92
+ }
93
+ [data-plot-highlight-label] {
94
+ fill: currentColor;
95
+ font-size: 11px;
96
+ }
97
+ [data-plot-highlight][data-plot-selected='true'] {
98
+ stroke: var(
99
+ --chart-selected-ring,
100
+ var(--chart-highlight-color, rgb(var(--color-accent-500, 194 65 12)))
101
+ );
102
+ stroke-width: var(--chart-selected-ring-width, 2);
103
+ fill: var(--chart-selected-fill, var(--color-paper, #fff));
104
+ }
105
+ </style>
@@ -0,0 +1,10 @@
1
+ type Row = Record<string, unknown>;
2
+ type Props = {
3
+ x?: string;
4
+ y?: string;
5
+ highlight?: 'first' | 'last' | 'min' | 'max' | number | ((row: Row, i: number) => boolean);
6
+ label?: boolean | string | ((row: Row) => unknown);
7
+ };
8
+ declare const Highlight: import("svelte").Component<Props, {}, "">;
9
+ type Highlight = ReturnType<typeof Highlight>;
10
+ export default Highlight;
@@ -4,6 +4,7 @@
4
4
  import { buildLines } from '../lib/brewing/marks/lines.js'
5
5
  import { buildSymbolPath } from '../lib/brewing/marks/points.js'
6
6
  import { keyboardNav } from '../lib/keyboard-nav.js'
7
+ import { buildSelectDetail } from '../lib/select.js'
7
8
  import LabelPill from './LabelPill.svelte'
8
9
 
9
10
  type Row = Record<string, unknown>
@@ -48,6 +49,18 @@
48
49
  const plotState = getContext<PlotState>('plot-state')
49
50
  let id = $state<string | null>(null)
50
51
 
52
+ function selectPoint(
53
+ pt: { data: Row },
54
+ seg: { key?: unknown },
55
+ event: MouseEvent | KeyboardEvent
56
+ ) {
57
+ onselect?.(pt.data)
58
+ if (plotState.interactive)
59
+ plotState.handleSelect(
60
+ buildSelectDetail(pt.data, plotState.data.indexOf(pt.data), { x, y }, 'line', seg?.key, event)
61
+ )
62
+ }
63
+
51
64
  onMount(() => {
52
65
  id = plotState.registerGeom({
53
66
  type: 'line',
@@ -91,7 +104,7 @@
91
104
  data-plot-element="line"
92
105
  />
93
106
  {#if symbolField && symbolMap}
94
- {#each seg.points as pt (`${pt.x}::${pt.y}`)}
107
+ {#each seg.points as pt (pt.i)}
95
108
  <path
96
109
  transform="translate({pt.x},{pt.y})"
97
110
  d={buildSymbolPath(symbolMap.get(pt.data[symbolField]) ?? 'circle', markerRadius)}
@@ -103,7 +116,7 @@
103
116
  {/each}
104
117
  {/if}
105
118
  {#if label}
106
- {#each seg.points as pt (`label::${pt.x}::${pt.y}`)}
119
+ {#each seg.points as pt (`label::${pt.i}`)}
107
120
  {@const text = resolveLabel(pt.data)}
108
121
  {#if text}
109
122
  <LabelPill
@@ -117,21 +130,25 @@
117
130
  {/if}
118
131
  <!-- Invisible hit areas for tooltip and selection -->
119
132
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
120
- {#each seg.points as pt (`hover::${pt.x}::${pt.y}`)}
133
+ {#each seg.points as pt (`hover::${pt.i}`)}
121
134
  <circle
122
135
  cx={pt.x}
123
136
  cy={pt.y}
124
137
  r="8"
125
138
  fill="transparent"
126
139
  stroke="none"
127
- role={onselect || keyboard ? 'button' : 'presentation'}
128
- tabindex={onselect || keyboard ? 0 : undefined}
129
- style:cursor={onselect ? 'pointer' : undefined}
140
+ role={onselect || keyboard || plotState.interactive ? 'button' : 'presentation'}
141
+ tabindex={onselect || keyboard || plotState.interactive ? 0 : undefined}
142
+ style:cursor={onselect || keyboard || plotState.interactive ? 'pointer' : undefined}
130
143
  data-plot-element="line-hover"
131
144
  onmouseenter={() => plotState.setHovered(pt.data)}
132
145
  onmouseleave={() => plotState.clearHovered()}
133
- onclick={onselect ? () => onselect(pt.data) : undefined}
134
- onkeydown={onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(pt.data) : undefined}
146
+ onclick={onselect || keyboard || plotState.interactive
147
+ ? (e) => selectPoint(pt, seg, e)
148
+ : undefined}
149
+ onkeydown={onselect || keyboard || plotState.interactive
150
+ ? (e) => (e.key === 'Enter' || e.key === ' ') && selectPoint(pt, seg, e)
151
+ : undefined}
135
152
  use:keyboardNav={keyboard}
136
153
  />
137
154
  {/each}
@@ -4,6 +4,7 @@
4
4
  import type { PlotState } from '../PlotState.svelte.js'
5
5
  import { buildPoints } from '../lib/brewing/marks/points.js'
6
6
  import { keyboardNav } from '../lib/keyboard-nav.js'
7
+ import { buildSelectDetail } from '../lib/select.js'
7
8
  import LabelPill from './LabelPill.svelte'
8
9
 
9
10
  type Row = Record<string, unknown>
@@ -52,6 +53,21 @@
52
53
  const plotState = getContext<PlotState>('plot-state')
53
54
  let id = $state<string | null>(null)
54
55
 
56
+ function selectPoint(pt: { data: Row }, event: MouseEvent | KeyboardEvent) {
57
+ onselect?.(pt.data)
58
+ if (plotState.interactive)
59
+ plotState.handleSelect(
60
+ buildSelectDetail(
61
+ pt.data,
62
+ plotState.data.indexOf(pt.data),
63
+ { x, y },
64
+ 'point',
65
+ color ? pt.data[color] : undefined,
66
+ event
67
+ )
68
+ )
69
+ }
70
+
55
71
  onMount(() => {
56
72
  id = plotState.registerGeom({
57
73
  type: 'point',
@@ -118,14 +134,16 @@
118
134
  stroke-width="1"
119
135
  fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
120
136
  data-plot-element="point"
121
- role={onselect || keyboard ? 'button' : 'graphics-symbol'}
122
- tabindex={onselect || keyboard ? 0 : undefined}
123
- style:cursor={onselect ? 'pointer' : undefined}
137
+ role={onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
138
+ tabindex={onselect || keyboard || plotState.interactive ? 0 : undefined}
139
+ style:cursor={onselect || keyboard || plotState.interactive ? 'pointer' : undefined}
124
140
  aria-label="{pt.data[x ?? '']}, {pt.data[y ?? '']}"
125
141
  onmouseenter={() => plotState.setHovered(pt.data)}
126
142
  onmouseleave={() => plotState.clearHovered()}
127
- onclick={onselect ? () => onselect(pt.data) : undefined}
128
- onkeydown={onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(pt.data) : undefined}
143
+ onclick={onselect || keyboard || plotState.interactive ? (e) => selectPoint(pt, e) : undefined}
144
+ onkeydown={onselect || keyboard || plotState.interactive
145
+ ? (e) => (e.key === 'Enter' || e.key === ' ') && selectPoint(pt, e)
146
+ : undefined}
129
147
  use:keyboardNav={keyboard}
130
148
  />
131
149
  {:else}
@@ -138,14 +156,16 @@
138
156
  stroke-width="1"
139
157
  fill-opacity={options.opacity ?? plotState.chartPreset.opacity.point}
140
158
  data-plot-element="point"
141
- role={onselect || keyboard ? 'button' : 'graphics-symbol'}
142
- tabindex={onselect || keyboard ? 0 : undefined}
143
- style:cursor={onselect ? 'pointer' : undefined}
159
+ role={onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
160
+ tabindex={onselect || keyboard || plotState.interactive ? 0 : undefined}
161
+ style:cursor={onselect || keyboard || plotState.interactive ? 'pointer' : undefined}
144
162
  aria-label="{pt.data[x ?? '']}, {pt.data[y ?? '']}"
145
163
  onmouseenter={() => plotState.setHovered(pt.data)}
146
164
  onmouseleave={() => plotState.clearHovered()}
147
- onclick={onselect ? () => onselect(pt.data) : undefined}
148
- onkeydown={onselect ? (e) => (e.key === 'Enter' || e.key === ' ') && onselect(pt.data) : undefined}
165
+ onclick={onselect || keyboard || plotState.interactive ? (e) => selectPoint(pt, e) : undefined}
166
+ onkeydown={onselect || keyboard || plotState.interactive
167
+ ? (e) => (e.key === 'Enter' || e.key === ' ') && selectPoint(pt, e)
168
+ : undefined}
149
169
  use:keyboardNav={keyboard}
150
170
  />
151
171
  {/if}
@@ -0,0 +1,66 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte'
3
+ import { line as d3line } from 'd3-shape'
4
+ import type { PlotState } from '../PlotState.svelte.js'
5
+ import { computeTrend } from '../lib/trend.js'
6
+ import { scalePos } from '../lib/scale.js'
7
+
8
+ type Method = string | number | { type: string; [k: string]: unknown }
9
+ type Props = { x?: string; y?: string; trend?: Method | Method[] }
10
+ let { x, y, trend = undefined }: Props = $props()
11
+
12
+ const state = getContext<PlotState>('plot-state')
13
+
14
+ const methods = $derived(
15
+ Array.isArray(trend) ? trend : trend === null || trend === undefined ? [] : [trend]
16
+ )
17
+
18
+ const typeOf = (m: Method) => {
19
+ const t = typeof m === 'string' ? m : typeof m === 'number' ? 'value' : (m?.type ?? '')
20
+ return t === 'mean' ? 'avg' : t
21
+ }
22
+
23
+ const paths = $derived.by(() => {
24
+ const rows = state?.data ?? []
25
+ const xs = state?.xScale
26
+ const ys = state?.yScale
27
+ if (!rows.length || !xs || !ys || !x || !y || !methods.length) return []
28
+ const out: { d: string; type: string; i: number }[] = []
29
+ methods.forEach((m, idx) => {
30
+ const res = computeTrend(rows, { x, y }, m)
31
+ if (!res) return
32
+ if (res.kind === 'constant') {
33
+ const yy = ys(res.value)
34
+ out.push({ d: `M0,${yy} L${state.innerWidth},${yy}`, type: typeOf(m), i: idx })
35
+ } else {
36
+ const pts = res.values
37
+ .map((v, i) => ({ vx: scalePos(xs, rows[i][x]), vy: ys(v) }))
38
+ .filter((p) => Number.isFinite(p.vx) && Number.isFinite(p.vy))
39
+ const gen = d3line<{ vx: number; vy: number }>()
40
+ .x((p) => p.vx)
41
+ .y((p) => p.vy)
42
+ const d = gen(pts)
43
+ if (d) out.push({ d, type: typeOf(m), i: idx })
44
+ }
45
+ })
46
+ return out
47
+ })
48
+ </script>
49
+
50
+ {#if paths.length}
51
+ <g data-plot-geom="trend">
52
+ {#each paths as p (p.i)}
53
+ <path d={p.d} data-plot-trend={p.type} />
54
+ {/each}
55
+ </g>
56
+ {/if}
57
+
58
+ <style>
59
+ [data-plot-trend] {
60
+ fill: none;
61
+ stroke: var(--chart-trend-color, currentColor);
62
+ stroke-width: var(--chart-trend-width, 1);
63
+ stroke-dasharray: var(--chart-trend-dash, 4 4);
64
+ opacity: var(--chart-trend-opacity, 0.7);
65
+ }
66
+ </style>
@@ -0,0 +1,12 @@
1
+ type Method = string | number | {
2
+ type: string;
3
+ [k: string]: unknown;
4
+ };
5
+ type Props = {
6
+ x?: string;
7
+ y?: string;
8
+ trend?: Method | Method[];
9
+ };
10
+ declare const Trend: import("svelte").Component<Props, {}, "">;
11
+ type Trend = ReturnType<typeof Trend>;
12
+ export default Trend;
package/dist/index.d.ts CHANGED
@@ -8,6 +8,8 @@ export namespace Plot {
8
8
  export { Area };
9
9
  export { Point };
10
10
  export { Arc };
11
+ export { Highlight };
12
+ export { Trend };
11
13
  }
12
14
  export namespace PlotLayers { }
13
15
  export { default as PlotChart } from "./Plot.svelte";
@@ -28,6 +30,8 @@ export { default as GeomCandlestick } from "./geoms/Candlestick.svelte";
28
30
  export { default as GeomWaterfall } from "./geoms/Waterfall.svelte";
29
31
  export { default as GeomHexbin } from "./geoms/Hexbin.svelte";
30
32
  export { default as GeomRibbon } from "./geoms/Ribbon.svelte";
33
+ export { default as GeomHighlight } from "./geoms/Highlight.svelte";
34
+ export { default as GeomTrend } from "./geoms/Trend.svelte";
31
35
  export { default as Chart } from "./Chart.svelte";
32
36
  export { default as Sparkline } from "./Sparkline.svelte";
33
37
  export { default as BarChart } from "./charts/BarChart.svelte";
@@ -60,5 +64,7 @@ import Line from './Plot/Line.svelte';
60
64
  import Area from './Plot/Area.svelte';
61
65
  import Point from './Plot/Point.svelte';
62
66
  import Arc from './Plot/Arc.svelte';
67
+ import Highlight from './geoms/Highlight.svelte';
68
+ import Trend from './geoms/Trend.svelte';
63
69
  export { createChartPreset, defaultPreset } from "./lib/preset.js";
64
70
  export { buildSequentialScale, buildDivergingScale } from "./lib/brewing/colors.js";
package/dist/index.js CHANGED
@@ -7,6 +7,8 @@ import Line from './Plot/Line.svelte'
7
7
  import Area from './Plot/Area.svelte'
8
8
  import Point from './Plot/Point.svelte'
9
9
  import Arc from './Plot/Arc.svelte'
10
+ import Highlight from './geoms/Highlight.svelte'
11
+ import Trend from './geoms/Trend.svelte'
10
12
 
11
13
  // Composable Plot primitives — use as <Plot.Root>, <Plot.Axis>, <Plot.Bar>, etc.
12
14
  export const Plot = {
@@ -18,7 +20,9 @@ export const Plot = {
18
20
  Line,
19
21
  Area,
20
22
  Point,
21
- Arc
23
+ Arc,
24
+ Highlight,
25
+ Trend
22
26
  }
23
27
 
24
28
  /** @deprecated Use Plot instead */
@@ -48,6 +52,8 @@ export { default as GeomCandlestick } from './geoms/Candlestick.svelte'
48
52
  export { default as GeomWaterfall } from './geoms/Waterfall.svelte'
49
53
  export { default as GeomHexbin } from './geoms/Hexbin.svelte'
50
54
  export { default as GeomRibbon } from './geoms/Ribbon.svelte'
55
+ export { default as GeomHighlight } from './geoms/Highlight.svelte'
56
+ export { default as GeomTrend } from './geoms/Trend.svelte'
51
57
 
52
58
  // Export standalone components
53
59
  export { default as Chart } from './Chart.svelte'
@@ -51,6 +51,7 @@ export class ChartBrewer {
51
51
  x: number;
52
52
  y: number;
53
53
  data: Record<string, unknown>;
54
+ i: number;
54
55
  }[];
55
56
  key?: unknown;
56
57
  }[];
@@ -5,7 +5,7 @@
5
5
  * @param {Function} yScale
6
6
  * @param {Map} colors
7
7
  * @param {'linear'|'smooth'|'step'} [curve]
8
- * @returns {{ d: string | null, fill: string, stroke: string, points: {x:number, y:number, data: Record<string, unknown>}[], key?: unknown }[]}
8
+ * @returns {{ d: string | null, fill: string, stroke: string, points: {x:number, y:number, data: Record<string, unknown>, i: number}[], key?: unknown }[]}
9
9
  */
10
10
  export function buildLines(data: Object[], channels: {
11
11
  x?: string;
@@ -19,6 +19,7 @@ export function buildLines(data: Object[], channels: {
19
19
  x: number;
20
20
  y: number;
21
21
  data: Record<string, unknown>;
22
+ i: number;
22
23
  }[];
23
24
  key?: unknown;
24
25
  }[];
@@ -7,7 +7,7 @@ import { line, curveCatmullRom, curveStep } from 'd3-shape'
7
7
  * @param {Function} yScale
8
8
  * @param {Map} colors
9
9
  * @param {'linear'|'smooth'|'step'} [curve]
10
- * @returns {{ d: string | null, fill: string, stroke: string, points: {x:number, y:number, data: Record<string, unknown>}[], key?: unknown }[]}
10
+ * @returns {{ d: string | null, fill: string, stroke: string, points: {x:number, y:number, data: Record<string, unknown>, i: number}[], key?: unknown }[]}
11
11
  */
12
12
  export function buildLines(data, channels, xScale, yScale, colors, curve) {
13
13
  const { x: xf, y: yf, color: cf } = channels
@@ -15,27 +15,31 @@ export function buildLines(data, channels, xScale, yScale, colors, curve) {
15
15
  typeof xScale.bandwidth === 'function' ? xScale(d[xf]) + xScale.bandwidth() / 2 : xScale(d[xf])
16
16
  const makeGen = () => {
17
17
  const gen = line()
18
- .x(xPos)
19
- .y((d) => yScale(d[yf]))
18
+ .x((d) => xPos(d.data ?? d))
19
+ .y((d) => yScale((d.data ?? d)[yf]))
20
20
  if (curve === 'smooth') gen.curve(curveCatmullRom)
21
21
  else if (curve === 'step') gen.curve(curveStep)
22
22
  return gen
23
23
  }
24
- const toPoints = (rows) => rows.map((d) => ({ x: xPos(d), y: yScale(d[yf]), data: d }))
24
+ const toPoints = (rowsWithIndices) => rowsWithIndices.map(({ data: d, i }) => ({ x: xPos(d), y: yScale(d[yf]), data: d, i }))
25
25
 
26
- const sortByX = (rows) => [...rows].sort((a, b) => xPos(a) - xPos(b))
26
+ const sortByX = (rows) => {
27
+ const withIndices = rows.map((d, i) => ({ data: d, i }))
28
+ withIndices.sort((a, b) => xPos(a.data) - xPos(b.data))
29
+ return withIndices
30
+ }
27
31
 
28
32
  if (!cf) {
29
33
  const sorted = sortByX(data)
30
34
  const stroke = colors?.values().next().value?.stroke ?? '#888'
31
- return [{ d: makeGen()(sorted), fill: 'none', stroke, points: toPoints(sorted) }]
35
+ return [{ d: makeGen()(sorted.map(d => d.data)), fill: 'none', stroke, points: toPoints(sorted) }]
32
36
  }
33
37
  const groups = groupBy(data, cf)
34
38
  return [...groups.entries()].map(([key, rows]) => {
35
39
  const sorted = sortByX(rows)
36
40
  const colorEntry = colors?.get(key) ?? { fill: 'none', stroke: '#888' }
37
41
  return {
38
- d: makeGen()(sorted),
42
+ d: makeGen()(sorted.map(d => d.data)),
39
43
  fill: 'none',
40
44
  stroke: colorEntry.stroke,
41
45
  points: toPoints(sorted),
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Resolve a highlight selector to zero or more row indices.
3
+ * @param {Array<Record<string, unknown>>} rows
4
+ * @param {'first'|'last'|'min'|'max'|number|((row:any,i:number)=>boolean)} selector
5
+ * @param {{ y?: string }} [opts]
6
+ * @returns {number[]}
7
+ */
8
+ export function resolveHighlight(rows: Array<Record<string, unknown>>, selector: "first" | "last" | "min" | "max" | number | ((row: any, i: number) => boolean), opts?: {
9
+ y?: string;
10
+ }): number[];
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Resolve a highlight selector to zero or more row indices.
3
+ * @param {Array<Record<string, unknown>>} rows
4
+ * @param {'first'|'last'|'min'|'max'|number|((row:any,i:number)=>boolean)} selector
5
+ * @param {{ y?: string }} [opts]
6
+ * @returns {number[]}
7
+ */
8
+ export function resolveHighlight(rows, selector, opts = {}) {
9
+ if (!Array.isArray(rows) || rows.length === 0 || selector === null || selector === undefined)
10
+ return []
11
+ const { y } = opts
12
+
13
+ if (typeof selector === 'function') {
14
+ const out = []
15
+ for (let i = 0; i < rows.length; i++) if (selector(rows[i], i)) out.push(i)
16
+ return out
17
+ }
18
+
19
+ if (typeof selector === 'number') {
20
+ const i = selector < 0 ? rows.length + selector : selector
21
+ return i >= 0 && i < rows.length ? [i] : []
22
+ }
23
+
24
+ if (selector === 'first') return [0]
25
+ if (selector === 'last') return [rows.length - 1]
26
+
27
+ if (selector === 'min' || selector === 'max') {
28
+ if (!y) return []
29
+ let best = -1
30
+ let bestVal = null
31
+ for (let i = 0; i < rows.length; i++) {
32
+ const v = Number(rows[i][y])
33
+ if (Number.isNaN(v)) continue
34
+ if (bestVal === null || (selector === 'min' ? v < bestVal : v > bestVal)) {
35
+ bestVal = v
36
+ best = i
37
+ }
38
+ }
39
+ return best === -1 ? [] : [best]
40
+ }
41
+
42
+ return []
43
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Map a value through a d3 scale to its plotted position, centering on band
3
+ * scales (adds `bandwidth/2`). Returns `NaN` when the scale yields `undefined`
4
+ * (value outside the domain) so overlay callers can drop the point via
5
+ * `Number.isFinite`.
6
+ * @param {(v: unknown) => number | undefined} scale
7
+ * @param {unknown} v
8
+ * @returns {number}
9
+ */
10
+ export function scalePos(scale: (v: unknown) => number | undefined, v: unknown): number;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Map a value through a d3 scale to its plotted position, centering on band
3
+ * scales (adds `bandwidth/2`). Returns `NaN` when the scale yields `undefined`
4
+ * (value outside the domain) so overlay callers can drop the point via
5
+ * `Number.isFinite`.
6
+ * @param {(v: unknown) => number | undefined} scale
7
+ * @param {unknown} v
8
+ * @returns {number}
9
+ */
10
+ export function scalePos(scale, v) {
11
+ const p = scale(v)
12
+ if (p === undefined) return NaN
13
+ return typeof scale.bandwidth === 'function' ? p + scale.bandwidth() / 2 : p
14
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Assemble the SelectDetail handed to a chart's onselect callback.
3
+ * @param {Record<string, unknown>} datum
4
+ * @param {number} index index within the geom's rendered data
5
+ * @param {{ x?: string, y?: string }} channels
6
+ * @param {'line'|'point'|'area'|'bar'} geom
7
+ * @param {unknown} series color/group value, or undefined
8
+ * @param {MouseEvent|KeyboardEvent} event
9
+ */
10
+ export function buildSelectDetail(datum: Record<string, unknown>, index: number, channels: {
11
+ x?: string;
12
+ y?: string;
13
+ }, geom: "line" | "point" | "area" | "bar", series: unknown, event: MouseEvent | KeyboardEvent): {
14
+ datum: Record<string, unknown>;
15
+ index: number;
16
+ series: {} | undefined;
17
+ value: unknown;
18
+ x: unknown;
19
+ y: unknown;
20
+ geom: "bar" | "line" | "area" | "point";
21
+ event: MouseEvent | KeyboardEvent;
22
+ };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Assemble the SelectDetail handed to a chart's onselect callback.
3
+ * @param {Record<string, unknown>} datum
4
+ * @param {number} index index within the geom's rendered data
5
+ * @param {{ x?: string, y?: string }} channels
6
+ * @param {'line'|'point'|'area'|'bar'} geom
7
+ * @param {unknown} series color/group value, or undefined
8
+ * @param {MouseEvent|KeyboardEvent} event
9
+ */
10
+ export function buildSelectDetail(datum, index, channels, geom, series, event) {
11
+ const { x, y } = channels
12
+ return {
13
+ datum,
14
+ index,
15
+ series: series ?? undefined,
16
+ value: y ? datum[y] : undefined,
17
+ x: x ? datum[x] : undefined,
18
+ y: y ? datum[y] : undefined,
19
+ geom,
20
+ event
21
+ }
22
+ }
@@ -0,0 +1,27 @@
1
+ export function mean(ys: any): number | null;
2
+ export function median(ys: any): any;
3
+ /** Least-squares line. Returns { m, b } or null (< 2 points or zero x-variance). */
4
+ export function linearRegression(xs: any, ys: any): {
5
+ m: number;
6
+ b: number;
7
+ } | null;
8
+ /** Trailing simple moving average; length preserved (partial window at the head). */
9
+ export function movingAverage(ys: any, window: any): number[];
10
+ /** Exponential moving average; alpha = alpha ?? 2/(span+1). Length preserved. */
11
+ export function ema(ys: any, { span, alpha }?: {}): any[];
12
+ /** Fit y = a·e^(bx) via log-linear least squares. Null if any y ≤ 0 or < 2 points. */
13
+ export function expRegression(xs: any, ys: any): {
14
+ a: number;
15
+ b: number;
16
+ } | null;
17
+ /**
18
+ * Compute a trend for `rows` on channels {x, y}.
19
+ * @returns {{kind:'constant', value:number} | {kind:'series', values:number[]} | null}
20
+ */
21
+ export function computeTrend(rows: any, channels: any, method: any): {
22
+ kind: "constant";
23
+ value: number;
24
+ } | {
25
+ kind: "series";
26
+ values: number[];
27
+ } | null;