@rokkit/chart 1.3.5 → 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.
- package/dist/Plot/Axis.svelte +2 -0
- package/dist/Plot/Grid.svelte +32 -13
- package/dist/Plot/Grid.svelte.d.ts +6 -1
- package/dist/Plot.svelte +51 -4
- package/dist/Plot.svelte.d.ts +14 -2
- package/dist/PlotState.svelte.d.ts +7 -0
- package/dist/PlotState.svelte.js +37 -0
- package/dist/charts/AreaChart.svelte +33 -3
- package/dist/charts/AreaChart.svelte.d.ts +11 -2
- package/dist/charts/LineChart.svelte +34 -3
- package/dist/charts/LineChart.svelte.d.ts +11 -2
- package/dist/geoms/Area.svelte +18 -1
- package/dist/geoms/Bar.svelte +40 -7
- package/dist/geoms/Highlight.svelte +105 -0
- package/dist/geoms/Highlight.svelte.d.ts +10 -0
- package/dist/geoms/Line.svelte +25 -8
- package/dist/geoms/Point.svelte +30 -10
- package/dist/geoms/Trend.svelte +66 -0
- package/dist/geoms/Trend.svelte.d.ts +12 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -1
- package/dist/lib/brewing/brewer.svelte.d.ts +1 -0
- package/dist/lib/brewing/marks/lines.d.ts +2 -1
- package/dist/lib/brewing/marks/lines.js +11 -7
- package/dist/lib/highlight.d.ts +10 -0
- package/dist/lib/highlight.js +43 -0
- package/dist/lib/scale.d.ts +10 -0
- package/dist/lib/scale.js +14 -0
- package/dist/lib/select.d.ts +22 -0
- package/dist/lib/select.js +22 -0
- package/dist/lib/trend.d.ts +27 -0
- package/dist/lib/trend.js +113 -0
- package/dist/patterns/README.md +1 -1
- package/package.json +1 -1
|
@@ -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;
|
package/dist/geoms/Line.svelte
CHANGED
|
@@ -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 (
|
|
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.
|
|
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.
|
|
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
|
|
134
|
-
|
|
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}
|
package/dist/geoms/Point.svelte
CHANGED
|
@@ -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 ? () =>
|
|
128
|
-
onkeydown={onselect
|
|
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 ? () =>
|
|
148
|
-
onkeydown={onselect
|
|
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'
|
|
@@ -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
|
|
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
|
|
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 = (
|
|
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) =>
|
|
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;
|