@rokkit/chart 1.0.0-next.150 → 1.0.0-next.151
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/PlotState.svelte.d.ts +5 -3
- package/dist/crossfilter/createCrossFilter.svelte.d.ts +13 -15
- package/dist/lib/brewing/brewer.svelte.d.ts +5 -36
- package/dist/lib/brewing/stats.d.ts +5 -13
- package/dist/lib/chart.d.ts +5 -7
- package/package.json +1 -1
- package/src/AnimatedPlot.svelte +10 -9
- package/src/Chart.svelte +15 -18
- package/src/Plot.svelte +15 -23
- package/src/PlotState.svelte.js +62 -63
- package/src/Sparkline.svelte +1 -1
- package/src/crossfilter/createCrossFilter.svelte.js +20 -13
- package/src/geoms/Box.svelte +5 -3
- package/src/geoms/Line.svelte +1 -2
- package/src/geoms/Point.svelte +12 -7
- package/src/geoms/Violin.svelte +5 -3
- package/src/lib/brewing/CartesianBrewer.svelte.js +2 -1
- package/src/lib/brewing/brewer.svelte.js +31 -30
- package/src/lib/brewing/scales.js +7 -7
- package/src/lib/brewing/stats.js +5 -1
- package/src/lib/chart.js +13 -6
- package/src/patterns/PatternDef.svelte +1 -1
- package/src/patterns/README.md +0 -3
|
@@ -9,12 +9,12 @@ export class PlotState {
|
|
|
9
9
|
fill: string;
|
|
10
10
|
stroke: string;
|
|
11
11
|
}>;
|
|
12
|
-
patterns: Map<any, any>;
|
|
13
|
-
symbols: Map<any, any>;
|
|
12
|
+
patterns: Map<unknown, string> | SvelteMap<any, any>;
|
|
13
|
+
symbols: Map<unknown, string> | SvelteMap<any, any>;
|
|
14
14
|
colorField: any;
|
|
15
15
|
patternField: any;
|
|
16
16
|
symbolField: any;
|
|
17
|
-
geomTypes:
|
|
17
|
+
geomTypes: SvelteSet<any>;
|
|
18
18
|
xAxisY: any;
|
|
19
19
|
yAxisX: any;
|
|
20
20
|
update(config: any): void;
|
|
@@ -45,3 +45,5 @@ export class PlotState {
|
|
|
45
45
|
clearHovered(): void;
|
|
46
46
|
#private;
|
|
47
47
|
}
|
|
48
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
49
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*/
|
|
15
|
-
export function createCrossFilter(): CrossFilter;
|
|
1
|
+
export function createCrossFilter(): {
|
|
2
|
+
/** @readonly — reactive Map of current filter state */
|
|
3
|
+
readonly filters: SvelteMap<any, any>;
|
|
4
|
+
/** @readonly — increments on every mutation; read inside $effect to react to any filter change */
|
|
5
|
+
readonly version: number;
|
|
6
|
+
isFiltered: (dimension: string) => boolean;
|
|
7
|
+
isDimmed: (dimension: string, value: unknown) => boolean;
|
|
8
|
+
toggleCategorical: (dimension: string, value: unknown) => void;
|
|
9
|
+
setRange: (dimension: string, range: [number, number]) => void;
|
|
10
|
+
clearFilter: (dimension: string) => void;
|
|
11
|
+
clearAll: () => void;
|
|
12
|
+
};
|
|
13
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
@@ -1,31 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* @param {{ fill?: string, color?: string, pattern?: string, symbol?: string }} channels
|
|
6
|
-
* `fill` takes precedence over `color` for polygon charts (bars, areas, pie slices).
|
|
7
|
-
* @param {Map<unknown, {fill:string, stroke:string}>} colorMap
|
|
8
|
-
* @param {Map<unknown, string>} patternMap
|
|
9
|
-
* @param {Map<unknown, string>} symbolMap
|
|
10
|
-
* @returns {{ field: string, items: { label: string, fill: string|null, stroke: string|null, patternId: string|null, shape: string|null }[] }[]}
|
|
11
|
-
*/
|
|
12
|
-
export function buildLegendGroups(channels: {
|
|
13
|
-
fill?: string;
|
|
14
|
-
color?: string;
|
|
15
|
-
pattern?: string;
|
|
16
|
-
symbol?: string;
|
|
17
|
-
}, colorMap: Map<unknown, {
|
|
18
|
-
fill: string;
|
|
19
|
-
stroke: string;
|
|
20
|
-
}>, patternMap: Map<unknown, string>, symbolMap: Map<unknown, string>): {
|
|
21
|
-
field: string;
|
|
22
|
-
items: {
|
|
23
|
-
label: string;
|
|
24
|
-
fill: string | null;
|
|
25
|
-
stroke: string | null;
|
|
26
|
-
patternId: string | null;
|
|
27
|
-
shape: string | null;
|
|
28
|
-
}[];
|
|
1
|
+
export function buildLegendGroups(channels: any, colorMap: any, patternMap: any, symbolMap: any): {
|
|
2
|
+
field: any;
|
|
3
|
+
items: any;
|
|
29
4
|
}[];
|
|
30
5
|
export class ChartBrewer {
|
|
31
6
|
/**
|
|
@@ -107,14 +82,8 @@ export class ChartBrewer {
|
|
|
107
82
|
key: any;
|
|
108
83
|
}[];
|
|
109
84
|
legendGroups: {
|
|
110
|
-
field:
|
|
111
|
-
items:
|
|
112
|
-
label: string;
|
|
113
|
-
fill: string | null;
|
|
114
|
-
stroke: string | null;
|
|
115
|
-
patternId: string | null;
|
|
116
|
-
shape: string | null;
|
|
117
|
-
}[];
|
|
85
|
+
field: any;
|
|
86
|
+
items: any;
|
|
118
87
|
}[];
|
|
119
88
|
get margin(): {
|
|
120
89
|
top: number;
|
|
@@ -11,19 +11,11 @@ export function applyBoxStat(data: Object[], channels: {
|
|
|
11
11
|
y?: string;
|
|
12
12
|
color?: string;
|
|
13
13
|
}): Object[];
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* @param {{ by: string[], value: string, stat: string|Function }} opts
|
|
20
|
-
* @returns {Object[]}
|
|
21
|
-
*/
|
|
22
|
-
export function applyAggregate(data: Object[], { by, value, stat }: {
|
|
23
|
-
by: string[];
|
|
24
|
-
value: string;
|
|
25
|
-
stat: string | Function;
|
|
26
|
-
}): Object[];
|
|
14
|
+
export function applyAggregate(data: any, { by, value, stat }: {
|
|
15
|
+
by: any;
|
|
16
|
+
value: any;
|
|
17
|
+
stat: any;
|
|
18
|
+
}): any;
|
|
27
19
|
/**
|
|
28
20
|
* Built-in reduction functions. Each receives an array of numeric values.
|
|
29
21
|
* @type {Record<string, (values: number[]) => number>}
|
package/dist/lib/chart.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export function chart(data: any, aes: any): Chart;
|
|
2
2
|
declare class Chart {
|
|
3
3
|
constructor(data: any, opts: any);
|
|
4
|
-
width: number;
|
|
5
|
-
height: number;
|
|
4
|
+
width: number | undefined;
|
|
5
|
+
height: number | undefined;
|
|
6
6
|
flipCoords: any;
|
|
7
7
|
x: any;
|
|
8
8
|
y: any;
|
|
@@ -13,13 +13,10 @@ declare class Chart {
|
|
|
13
13
|
color: any;
|
|
14
14
|
shape: any;
|
|
15
15
|
padding(value: any): this;
|
|
16
|
-
spacing: number;
|
|
16
|
+
spacing: number | undefined;
|
|
17
17
|
margin(value: any): this;
|
|
18
|
-
domain: {
|
|
19
|
-
x: any[];
|
|
20
|
-
y: any[];
|
|
21
|
-
};
|
|
22
18
|
stat: any;
|
|
19
|
+
domain: any;
|
|
23
20
|
data: any;
|
|
24
21
|
refresh(): this;
|
|
25
22
|
range: {
|
|
@@ -36,5 +33,6 @@ declare class Chart {
|
|
|
36
33
|
} | undefined;
|
|
37
34
|
aggregate(value: any, stat: any): void;
|
|
38
35
|
ticks(axis: any, count: any, fontSize?: number): any;
|
|
36
|
+
#private;
|
|
39
37
|
}
|
|
40
38
|
export {};
|
package/package.json
CHANGED
package/src/AnimatedPlot.svelte
CHANGED
|
@@ -99,19 +99,20 @@
|
|
|
99
99
|
let lastTime = 0
|
|
100
100
|
let rafId = 0
|
|
101
101
|
|
|
102
|
+
function advanceFrame() {
|
|
103
|
+
currentIndex = currentIndex + 1
|
|
104
|
+
if (currentIndex >= frameKeys.length) {
|
|
105
|
+
if (animate.loop ?? false) currentIndex = 0
|
|
106
|
+
else playing = false
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
function tick(time) {
|
|
103
111
|
if (!playing) return
|
|
104
112
|
if (time - lastTime >= msPerFrame) {
|
|
105
113
|
lastTime = time
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
if (animate.loop ?? false) {
|
|
109
|
-
currentIndex = 0
|
|
110
|
-
} else {
|
|
111
|
-
playing = false
|
|
112
|
-
return
|
|
113
|
-
}
|
|
114
|
-
}
|
|
114
|
+
advanceFrame()
|
|
115
|
+
if (!playing) return
|
|
115
116
|
}
|
|
116
117
|
rafId = requestAnimationFrame(tick)
|
|
117
118
|
}
|
package/src/Chart.svelte
CHANGED
|
@@ -40,27 +40,24 @@
|
|
|
40
40
|
const brewer = new ChartBrewer()
|
|
41
41
|
setContext('chart-brewer', brewer)
|
|
42
42
|
|
|
43
|
+
function buildChannels() {
|
|
44
|
+
const channels = {}
|
|
45
|
+
if (x) channels.x = x
|
|
46
|
+
if (y) channels.y = y
|
|
47
|
+
if (color) channels.color = color
|
|
48
|
+
if (pattern) channels.pattern = pattern
|
|
49
|
+
if (fill) channels.fill = fill
|
|
50
|
+
if (size) channels.size = size
|
|
51
|
+
if (label) channels.label = label
|
|
52
|
+
if (symbol) channels.symbol = symbol
|
|
53
|
+
return channels
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
$effect(() => {
|
|
44
57
|
if (spec) {
|
|
45
|
-
brewer.update({
|
|
46
|
-
data: spec.data,
|
|
47
|
-
channels: spec.channels,
|
|
48
|
-
width,
|
|
49
|
-
height,
|
|
50
|
-
mode,
|
|
51
|
-
layers: spec.layers
|
|
52
|
-
})
|
|
58
|
+
brewer.update({ data: spec.data, channels: spec.channels, width, height, mode, layers: spec.layers })
|
|
53
59
|
} else {
|
|
54
|
-
|
|
55
|
-
if (x) channels.x = x
|
|
56
|
-
if (y) channels.y = y
|
|
57
|
-
if (color) channels.color = color
|
|
58
|
-
if (pattern) channels.pattern = pattern
|
|
59
|
-
if (fill) channels.fill = fill
|
|
60
|
-
if (size) channels.size = size
|
|
61
|
-
if (label) channels.label = label
|
|
62
|
-
if (symbol) channels.symbol = symbol
|
|
63
|
-
brewer.update({ data, channels, width, height, mode })
|
|
60
|
+
brewer.update({ data, channels: buildChannels(), width, height, mode })
|
|
64
61
|
}
|
|
65
62
|
})
|
|
66
63
|
|
package/src/Plot.svelte
CHANGED
|
@@ -47,27 +47,8 @@
|
|
|
47
47
|
children
|
|
48
48
|
} = $props()
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// the $effect below handles all subsequent reactive updates.
|
|
53
|
-
const plotState = untrack(() => new PlotState({
|
|
54
|
-
data: spec?.data ?? data,
|
|
55
|
-
width: spec?.width ?? width,
|
|
56
|
-
height: spec?.height ?? height,
|
|
57
|
-
mode,
|
|
58
|
-
margin,
|
|
59
|
-
channels: spec ? { x: spec.x, y: spec.y, color: spec.color ?? spec.fill } : {},
|
|
60
|
-
labels: spec?.labels ?? {},
|
|
61
|
-
helpers,
|
|
62
|
-
xDomain: spec?.xDomain,
|
|
63
|
-
yDomain: spec?.yDomain,
|
|
64
|
-
colorDomain: spec?.colorDomain
|
|
65
|
-
}))
|
|
66
|
-
setContext('plot-state', plotState)
|
|
67
|
-
|
|
68
|
-
// Keep state in sync when reactive config changes
|
|
69
|
-
$effect(() => {
|
|
70
|
-
plotState.update({
|
|
50
|
+
function buildPlotConfig() {
|
|
51
|
+
return {
|
|
71
52
|
data: spec?.data ?? data,
|
|
72
53
|
width: spec?.width ?? width,
|
|
73
54
|
height: spec?.height ?? height,
|
|
@@ -79,7 +60,18 @@
|
|
|
79
60
|
xDomain: spec?.xDomain,
|
|
80
61
|
yDomain: spec?.yDomain,
|
|
81
62
|
colorDomain: spec?.colorDomain
|
|
82
|
-
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Create PlotState with initial values and provide as context.
|
|
67
|
+
// untrack() suppresses "captures initial value" warnings — intentional:
|
|
68
|
+
// the $effect below handles all subsequent reactive updates.
|
|
69
|
+
const plotState = untrack(() => new PlotState(buildPlotConfig()))
|
|
70
|
+
setContext('plot-state', plotState)
|
|
71
|
+
|
|
72
|
+
// Keep state in sync when reactive config changes
|
|
73
|
+
$effect(() => {
|
|
74
|
+
plotState.update(buildPlotConfig())
|
|
83
75
|
})
|
|
84
76
|
|
|
85
77
|
const svgWidth = $derived(spec?.width ?? width)
|
|
@@ -131,7 +123,7 @@
|
|
|
131
123
|
{@render children?.()}
|
|
132
124
|
|
|
133
125
|
<!-- Spec-driven geoms -->
|
|
134
|
-
{#each specGeoms as geomSpec}
|
|
126
|
+
{#each specGeoms as geomSpec (geomSpec.type)}
|
|
135
127
|
{@const GeomComponent = resolveGeomComponent(geomSpec.type)}
|
|
136
128
|
{#if GeomComponent}
|
|
137
129
|
<GeomComponent
|
package/src/PlotState.svelte.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { untrack } from 'svelte'
|
|
2
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity'
|
|
2
3
|
import { applyGeomStat } from './lib/plot/stat.js'
|
|
3
4
|
import { inferFieldType, inferOrientation, buildUnifiedXScale, buildUnifiedYScale, inferColorScaleType } from './lib/plot/scales.js'
|
|
4
5
|
import { resolvePreset } from './lib/plot/preset.js'
|
|
@@ -38,20 +39,29 @@ export class PlotState {
|
|
|
38
39
|
|
|
39
40
|
// Effective channels: prefer top-level channels; fall back to first geom's channels
|
|
40
41
|
// for the declarative API where no spec is provided.
|
|
42
|
+
#mergeGeomChannels(tc, geom) {
|
|
43
|
+
return {
|
|
44
|
+
x: tc.x ?? geom.channels?.x,
|
|
45
|
+
y: tc.y ?? geom.channels?.y,
|
|
46
|
+
color: tc.color ?? geom.channels?.color,
|
|
47
|
+
pattern: tc.pattern ?? geom.channels?.pattern,
|
|
48
|
+
symbol: tc.symbol ?? geom.channels?.symbol,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
#effectiveChannels = $derived.by(() => {
|
|
42
53
|
const tc = this.#channels
|
|
43
54
|
if (tc.x && tc.y) return tc
|
|
44
55
|
const firstGeom = this.#geoms[0]
|
|
45
56
|
if (!firstGeom) return tc
|
|
46
|
-
return
|
|
47
|
-
x: tc.x ?? firstGeom.channels?.x,
|
|
48
|
-
y: tc.y ?? firstGeom.channels?.y,
|
|
49
|
-
color: tc.color ?? firstGeom.channels?.color,
|
|
50
|
-
pattern: tc.pattern ?? firstGeom.channels?.pattern,
|
|
51
|
-
symbol: tc.symbol ?? firstGeom.channels?.symbol,
|
|
52
|
-
}
|
|
57
|
+
return this.#mergeGeomChannels(tc, firstGeom)
|
|
53
58
|
})
|
|
54
59
|
|
|
60
|
+
#resolveXType(rawXType, yType) {
|
|
61
|
+
const hasBarGeom = this.#geoms.some((g) => g.type === 'bar')
|
|
62
|
+
return (hasBarGeom && rawXType === 'continuous' && yType === 'continuous') ? 'band' : rawXType
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
orientation = $derived.by(() => {
|
|
56
66
|
const xField = this.#effectiveChannels.x
|
|
57
67
|
const yField = this.#effectiveChannels.y
|
|
@@ -59,9 +69,7 @@ export class PlotState {
|
|
|
59
69
|
const rawXType = inferFieldType(this.#data, xField)
|
|
60
70
|
const yType = inferFieldType(this.#data, yField)
|
|
61
71
|
// Bar geoms treat numeric X as categorical (e.g. year on X → vertical bars).
|
|
62
|
-
|
|
63
|
-
const xType = (hasBarGeom && rawXType === 'continuous' && yType === 'continuous') ? 'band' : rawXType
|
|
64
|
-
return inferOrientation(xType, yType)
|
|
72
|
+
return inferOrientation(this.#resolveXType(rawXType, yType), yType)
|
|
65
73
|
})
|
|
66
74
|
|
|
67
75
|
colorScaleType = $derived.by(() => {
|
|
@@ -91,6 +99,44 @@ export class PlotState {
|
|
|
91
99
|
})
|
|
92
100
|
})
|
|
93
101
|
|
|
102
|
+
// For box/violin geoms, compute y domain from iqr_min/iqr_max instead of raw y values.
|
|
103
|
+
#resolveBoxDomain() {
|
|
104
|
+
const boxGeom = this.#geoms.find((g) => g.type === 'box' || g.type === 'violin')
|
|
105
|
+
if (!boxGeom) return null
|
|
106
|
+
const boxData = this.geomData(boxGeom.id)
|
|
107
|
+
const isValid = (v) => v !== null && v !== undefined && !isNaN(v)
|
|
108
|
+
const mins = boxData.map((d) => d.iqr_min).filter(isValid)
|
|
109
|
+
const maxs = boxData.map((d) => d.iqr_max).filter(isValid)
|
|
110
|
+
return (mins.length > 0 && maxs.length > 0) ? [Math.min(...mins), Math.max(...maxs)] : null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// For stacked bars, compute y domain from per-x column totals.
|
|
114
|
+
#resolveStackDomain(field) {
|
|
115
|
+
const stackGeom = this.#geoms.find((g) => g.options?.stack)
|
|
116
|
+
if (!stackGeom) return null
|
|
117
|
+
const xField = this.#effectiveChannels.x
|
|
118
|
+
const stackData = this.geomData(stackGeom.id)
|
|
119
|
+
if (!xField || stackData.length === 0) return null
|
|
120
|
+
// Mirror buildStackedBars/subBandFields: stack dimension is the first
|
|
121
|
+
// non-x field among [color, pattern]. Summing all raw rows (stat=identity)
|
|
122
|
+
// would overcount when multiple rows share the same (x, stack) key.
|
|
123
|
+
const colorField = this.#effectiveChannels.color
|
|
124
|
+
const patternField = this.#effectiveChannels.pattern
|
|
125
|
+
const stackField = [colorField, patternField].find((f) => f && f !== xField) ?? colorField
|
|
126
|
+
const lookup = new SvelteMap()
|
|
127
|
+
for (const d of stackData) {
|
|
128
|
+
const xVal = d[xField]
|
|
129
|
+
const cKey = stackField ? String(d[stackField]) : '_'
|
|
130
|
+
if (!lookup.has(xVal)) lookup.set(xVal, new SvelteMap())
|
|
131
|
+
lookup.get(xVal).set(cKey, Number(d[field]) || 0)
|
|
132
|
+
}
|
|
133
|
+
const totals = new SvelteMap()
|
|
134
|
+
for (const [xVal, colorMap] of lookup) {
|
|
135
|
+
totals.set(xVal, [...colorMap.values()].reduce((s, v) => s + v, 0))
|
|
136
|
+
}
|
|
137
|
+
return [0, Math.max(0, ...totals.values())]
|
|
138
|
+
}
|
|
139
|
+
|
|
94
140
|
yScale = $derived.by(() => {
|
|
95
141
|
const field = this.#effectiveChannels.y
|
|
96
142
|
if (!field) return null
|
|
@@ -98,55 +144,8 @@ export class PlotState {
|
|
|
98
144
|
? this.#geoms.map((g) => this.geomData(g.id))
|
|
99
145
|
: [this.#rawData]
|
|
100
146
|
const includeZero = this.orientation === 'vertical'
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// per-row max — otherwise bars overflow the plot area.
|
|
104
|
-
// For box/violin geoms, the processed data has iqr_min/iqr_max instead of raw y values.
|
|
105
|
-
let yDomain = this.#yDomain
|
|
106
|
-
if (!yDomain) {
|
|
107
|
-
const boxGeom = this.#geoms.find((g) => g.type === 'box' || g.type === 'violin')
|
|
108
|
-
if (boxGeom) {
|
|
109
|
-
const boxData = this.geomData(boxGeom.id)
|
|
110
|
-
const mins = boxData.map((d) => d.iqr_min).filter((v) => v !== null && v !== undefined && !isNaN(v))
|
|
111
|
-
const maxs = boxData.map((d) => d.iqr_max).filter((v) => v !== null && v !== undefined && !isNaN(v))
|
|
112
|
-
if (mins.length > 0 && maxs.length > 0) {
|
|
113
|
-
yDomain = [Math.min(...mins), Math.max(...maxs)]
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (!yDomain) {
|
|
118
|
-
const stackGeom = this.#geoms.find((g) => g.options?.stack)
|
|
119
|
-
if (stackGeom) {
|
|
120
|
-
const xField = this.#effectiveChannels.x
|
|
121
|
-
const stackData = this.geomData(stackGeom.id)
|
|
122
|
-
if (xField && stackData.length > 0) {
|
|
123
|
-
// Mirror buildStackedBars/subBandFields: stack dimension is the first
|
|
124
|
-
// non-x field among [color, pattern]. Summing all raw rows (stat=identity)
|
|
125
|
-
// would overcount when multiple rows share the same (x, stack) key.
|
|
126
|
-
const colorField = this.#effectiveChannels.color
|
|
127
|
-
const patternField = this.#effectiveChannels.pattern
|
|
128
|
-
const stackField = [colorField, patternField].find((f) => f && f !== xField) ?? colorField
|
|
129
|
-
const lookup = new Map()
|
|
130
|
-
for (const d of stackData) {
|
|
131
|
-
const xVal = d[xField]
|
|
132
|
-
const cKey = stackField ? String(d[stackField]) : '_'
|
|
133
|
-
if (!lookup.has(xVal)) lookup.set(xVal, new Map())
|
|
134
|
-
lookup.get(xVal).set(cKey, Number(d[field]) || 0)
|
|
135
|
-
}
|
|
136
|
-
const totals = new Map()
|
|
137
|
-
for (const [xVal, colorMap] of lookup) {
|
|
138
|
-
totals.set(xVal, [...colorMap.values()].reduce((s, v) => s + v, 0))
|
|
139
|
-
}
|
|
140
|
-
const maxStack = Math.max(0, ...totals.values())
|
|
141
|
-
yDomain = [0, maxStack]
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return buildUnifiedYScale(datasets, field, this.#innerHeight, {
|
|
147
|
-
domain: yDomain,
|
|
148
|
-
includeZero
|
|
149
|
-
})
|
|
147
|
+
const yDomain = this.#yDomain ?? this.#resolveBoxDomain() ?? this.#resolveStackDomain(field)
|
|
148
|
+
return buildUnifiedYScale(datasets, field, this.#innerHeight, { domain: yDomain, includeZero })
|
|
150
149
|
})
|
|
151
150
|
|
|
152
151
|
// Colors: Map<colorKey, { fill, stroke }> for all distinct color field values.
|
|
@@ -162,15 +161,15 @@ export class PlotState {
|
|
|
162
161
|
// and the pattern field is categorical (continuous fields can't be discretely patterned).
|
|
163
162
|
patterns = $derived.by(() => {
|
|
164
163
|
const pf = this.#effectiveChannels.pattern
|
|
165
|
-
if (!pf) return new
|
|
166
|
-
if (inferFieldType(this.#data, pf) === 'continuous') return new
|
|
164
|
+
if (!pf) return new SvelteMap()
|
|
165
|
+
if (inferFieldType(this.#data, pf) === 'continuous') return new SvelteMap()
|
|
167
166
|
return assignPatterns(distinct(this.#data, pf))
|
|
168
167
|
})
|
|
169
168
|
|
|
170
169
|
// Symbols: Map<symbolKey, shapeName> — only populated when a symbol channel is set.
|
|
171
170
|
symbols = $derived.by(() => {
|
|
172
171
|
const sf = this.#effectiveChannels.symbol
|
|
173
|
-
if (!sf) return new
|
|
172
|
+
if (!sf) return new SvelteMap()
|
|
174
173
|
return assignSymbols(distinct(this.#data, sf))
|
|
175
174
|
})
|
|
176
175
|
|
|
@@ -180,7 +179,7 @@ export class PlotState {
|
|
|
180
179
|
symbolField = $derived(this.#effectiveChannels.symbol)
|
|
181
180
|
|
|
182
181
|
// Set of geom types currently registered (used by Legend to pick swatch style)
|
|
183
|
-
geomTypes = $derived(new
|
|
182
|
+
geomTypes = $derived(new SvelteSet(this.#geoms.map((g) => g.type)))
|
|
184
183
|
|
|
185
184
|
xAxisY = $derived.by(() => {
|
|
186
185
|
if (!this.yScale || typeof this.yScale !== 'function') return this.#innerHeight
|
package/src/Sparkline.svelte
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
<path d={areaPath} fill={fillColor} stroke="none" />
|
|
57
57
|
<path d={linePath} fill="none" stroke={strokeColor} stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" />
|
|
58
58
|
{:else if type === 'bar'}
|
|
59
|
-
{#each values as v, i}
|
|
59
|
+
{#each values as v, i (i)}
|
|
60
60
|
<rect
|
|
61
61
|
x={xScale(i) - barWidth / 2}
|
|
62
62
|
y={yScale(v)}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Creates a reactive cross-filter state object.
|
|
3
5
|
*
|
|
@@ -12,9 +14,25 @@
|
|
|
12
14
|
*
|
|
13
15
|
* @returns {CrossFilter}
|
|
14
16
|
*/
|
|
17
|
+
|
|
18
|
+
function toggleCategoricalInMap(filters, dimension, value) {
|
|
19
|
+
const existing = filters.get(dimension)
|
|
20
|
+
const set = existing instanceof Set ? new SvelteSet(existing) : new SvelteSet()
|
|
21
|
+
if (set.has(value)) {
|
|
22
|
+
set.delete(value)
|
|
23
|
+
} else {
|
|
24
|
+
set.add(value)
|
|
25
|
+
}
|
|
26
|
+
if (set.size === 0) {
|
|
27
|
+
filters.delete(dimension)
|
|
28
|
+
} else {
|
|
29
|
+
filters.set(dimension, set)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
export function createCrossFilter() {
|
|
16
34
|
// Map<dimension, Set<unknown> | [number, number]>
|
|
17
|
-
const filters =
|
|
35
|
+
const filters = new SvelteMap()
|
|
18
36
|
|
|
19
37
|
// Simple counter incremented on every mutation. Components read cf.version
|
|
20
38
|
// inside $effect to reactively recompute when any filter changes.
|
|
@@ -58,18 +76,7 @@ export function createCrossFilter() {
|
|
|
58
76
|
* @param {unknown} value
|
|
59
77
|
*/
|
|
60
78
|
function toggleCategorical(dimension, value) {
|
|
61
|
-
|
|
62
|
-
const set = existing instanceof Set ? new Set(existing) : new Set()
|
|
63
|
-
if (set.has(value)) {
|
|
64
|
-
set.delete(value)
|
|
65
|
-
} else {
|
|
66
|
-
set.add(value)
|
|
67
|
-
}
|
|
68
|
-
if (set.size === 0) {
|
|
69
|
-
filters.delete(dimension)
|
|
70
|
-
} else {
|
|
71
|
-
filters.set(dimension, set)
|
|
72
|
-
}
|
|
79
|
+
toggleCategoricalInMap(filters, dimension, value)
|
|
73
80
|
version++
|
|
74
81
|
}
|
|
75
82
|
|
package/src/geoms/Box.svelte
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
let id = $state(null)
|
|
9
9
|
|
|
10
10
|
// fill ?? x drives the colors map for both box interior and whisker strokes
|
|
11
|
+
const fillChannel = $derived(fill ?? x)
|
|
12
|
+
|
|
11
13
|
onMount(() => {
|
|
12
|
-
id = plotState.registerGeom({ type: 'box', channels: { x, y, color:
|
|
14
|
+
id = plotState.registerGeom({ type: 'box', channels: { x, y, color: fillChannel }, stat, options })
|
|
13
15
|
})
|
|
14
16
|
onDestroy(() => { if (id) plotState.unregisterGeom(id) })
|
|
15
17
|
|
|
16
18
|
$effect(() => {
|
|
17
|
-
if (id) plotState.updateGeom(id, { channels: { x, y, color:
|
|
19
|
+
if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
|
|
18
20
|
})
|
|
19
21
|
|
|
20
22
|
const data = $derived(id ? plotState.geomData(id) : [])
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
|
|
25
27
|
const boxes = $derived.by(() => {
|
|
26
28
|
if (!data?.length || !xScale || !yScale) return []
|
|
27
|
-
return buildBoxes(data, { x, fill:
|
|
29
|
+
return buildBoxes(data, { x, fill: fillChannel }, xScale, yScale, colors)
|
|
28
30
|
})
|
|
29
31
|
</script>
|
|
30
32
|
|
package/src/geoms/Line.svelte
CHANGED
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
if (!label) return null
|
|
15
15
|
if (label === true) return String(data[y] ?? '')
|
|
16
16
|
if (typeof label === 'function') return String(label(data) ?? '')
|
|
17
|
-
|
|
18
|
-
return null
|
|
17
|
+
return typeof label === 'string' ? String(data[label] ?? '') : null
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
const plotState = getContext('plot-state')
|
package/src/geoms/Point.svelte
CHANGED
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
if (!label) return null
|
|
15
15
|
if (label === true) return String(data[y] ?? '')
|
|
16
16
|
if (typeof label === 'function') return String(label(data) ?? '')
|
|
17
|
-
|
|
18
|
-
return null
|
|
17
|
+
return typeof label === 'string' ? String(data[label] ?? '') : null
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
const plotState = getContext('plot-state')
|
|
@@ -36,18 +35,24 @@
|
|
|
36
35
|
const colors = $derived(plotState.colors)
|
|
37
36
|
const symbolMap = $derived(plotState.symbols)
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
function buildSizeScale() {
|
|
40
39
|
if (!size || !data?.length) return null
|
|
41
40
|
const vals = data.map((d) => Number(d[size])).filter((v) => !isNaN(v))
|
|
42
41
|
if (!vals.length) return null
|
|
43
|
-
const maxVal = Math.max(...vals)
|
|
44
42
|
const minVal = Math.min(...vals)
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
const maxVal = Math.max(...vals)
|
|
44
|
+
const minRadius = options.minRadius ?? 3
|
|
45
|
+
const maxRadius = options.maxRadius ?? 20
|
|
46
|
+
return scaleSqrt().domain([minVal, maxVal]).range([minRadius, maxRadius])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const sizeScale = $derived.by(() => buildSizeScale())
|
|
50
|
+
|
|
51
|
+
const defaultRadius = $derived(options.radius ?? 4)
|
|
47
52
|
|
|
48
53
|
const points = $derived.by(() => {
|
|
49
54
|
if (!data?.length || !xScale || !yScale) return []
|
|
50
|
-
return buildPoints(data, { x, y, color, size, symbol: symbolField }, xScale, yScale, colors, sizeScale, symbolMap,
|
|
55
|
+
return buildPoints(data, { x, y, color, size, symbol: symbolField }, xScale, yScale, colors, sizeScale, symbolMap, defaultRadius)
|
|
51
56
|
})
|
|
52
57
|
</script>
|
|
53
58
|
|
package/src/geoms/Violin.svelte
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
let id = $state(null)
|
|
9
9
|
|
|
10
10
|
// fill ?? x drives the colors map for both violin interior and outline
|
|
11
|
+
const fillChannel = $derived(fill ?? x)
|
|
12
|
+
|
|
11
13
|
onMount(() => {
|
|
12
|
-
id = plotState.registerGeom({ type: 'violin', channels: { x, y, color:
|
|
14
|
+
id = plotState.registerGeom({ type: 'violin', channels: { x, y, color: fillChannel }, stat, options })
|
|
13
15
|
})
|
|
14
16
|
onDestroy(() => { if (id) plotState.unregisterGeom(id) })
|
|
15
17
|
|
|
16
18
|
$effect(() => {
|
|
17
|
-
if (id) plotState.updateGeom(id, { channels: { x, y, color:
|
|
19
|
+
if (id) plotState.updateGeom(id, { channels: { x, y, color: fillChannel }, stat })
|
|
18
20
|
})
|
|
19
21
|
|
|
20
22
|
const data = $derived(id ? plotState.geomData(id) : [])
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
|
|
25
27
|
const violins = $derived.by(() => {
|
|
26
28
|
if (!data?.length || !xScale || !yScale) return []
|
|
27
|
-
return buildViolins(data, { x, fill:
|
|
29
|
+
return buildViolins(data, { x, fill: fillChannel }, xScale, yScale, colors)
|
|
28
30
|
})
|
|
29
31
|
</script>
|
|
30
32
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
1
2
|
import { ChartBrewer } from './brewer.svelte.js'
|
|
2
3
|
import { applyAggregate } from './stats.js'
|
|
3
4
|
|
|
@@ -10,7 +11,7 @@ export class CartesianBrewer extends ChartBrewer {
|
|
|
10
11
|
if (stat === 'identity' || !channels.x || !channels.y) return data
|
|
11
12
|
// Group by all mapped aesthetic dimensions so they survive aggregation.
|
|
12
13
|
// e.g. x=region, fill=region, pattern=quarter → by=['region','quarter']
|
|
13
|
-
const by = [...new
|
|
14
|
+
const by = [...new SvelteSet([channels.x, channels.fill, channels.color, channels.pattern].filter(Boolean))]
|
|
14
15
|
return applyAggregate(data, { by, value: channels.y, stat })
|
|
15
16
|
}
|
|
16
17
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity'
|
|
1
2
|
import { distinct, assignColors } from './colors.js'
|
|
2
3
|
import { assignPatterns, toPatternId, PATTERN_ORDER } from './patterns.js'
|
|
3
4
|
import { assignSymbols } from './symbols.js'
|
|
@@ -21,39 +22,39 @@ const DEFAULT_MARGIN = { top: 20, right: 20, bottom: 40, left: 50 }
|
|
|
21
22
|
* @param {Map<unknown, string>} symbolMap
|
|
22
23
|
* @returns {{ field: string, items: { label: string, fill: string|null, stroke: string|null, patternId: string|null, shape: string|null }[] }[]}
|
|
23
24
|
*/
|
|
25
|
+
function addAesthetic(byField, field, aesthetic, keys) {
|
|
26
|
+
if (byField.has(field)) {
|
|
27
|
+
byField.get(field).aesthetics.push(aesthetic)
|
|
28
|
+
} else {
|
|
29
|
+
byField.set(field, { aesthetics: [aesthetic], keys })
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildLegendItem(key, aesthetics, colorMap, patternMap, symbolMap) {
|
|
34
|
+
const hasColor = aesthetics.includes('color')
|
|
35
|
+
return {
|
|
36
|
+
label: String(key),
|
|
37
|
+
fill: hasColor ? (colorMap.get(key)?.fill ?? null) : null,
|
|
38
|
+
stroke: hasColor ? (colorMap.get(key)?.stroke ?? null) : null,
|
|
39
|
+
patternId: aesthetics.includes('pattern') && patternMap.has(key) ? toPatternId(key) : null,
|
|
40
|
+
shape: aesthetics.includes('symbol') ? (symbolMap.get(key) ?? 'circle') : null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
export function buildLegendGroups(channels, colorMap, patternMap, symbolMap) {
|
|
25
45
|
const cf = channels.fill ?? channels.color
|
|
26
46
|
const { pattern: pf, symbol: sf } = channels
|
|
27
|
-
const byField = new
|
|
47
|
+
const byField = new SvelteMap()
|
|
28
48
|
|
|
29
|
-
if (cf) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (pf) {
|
|
33
|
-
if (byField.has(pf)) {
|
|
34
|
-
byField.get(pf).aesthetics.push('pattern')
|
|
35
|
-
} else {
|
|
36
|
-
byField.set(pf, { aesthetics: ['pattern'], keys: [...patternMap.keys()] })
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
if (sf) {
|
|
40
|
-
if (byField.has(sf)) {
|
|
41
|
-
byField.get(sf).aesthetics.push('symbol')
|
|
42
|
-
} else {
|
|
43
|
-
byField.set(sf, { aesthetics: ['symbol'], keys: [...symbolMap.keys()] })
|
|
44
|
-
}
|
|
45
|
-
}
|
|
49
|
+
if (cf) byField.set(cf, { aesthetics: ['color'], keys: [...colorMap.keys()] })
|
|
50
|
+
if (pf) addAesthetic(byField, pf, 'pattern', [...patternMap.keys()])
|
|
51
|
+
if (sf) addAesthetic(byField, sf, 'symbol', [...symbolMap.keys()])
|
|
46
52
|
|
|
47
53
|
return [...byField.entries()].map(([field, { aesthetics, keys }]) => ({
|
|
48
54
|
field,
|
|
49
|
-
items: keys
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
stroke: aesthetics.includes('color') ? (colorMap.get(key)?.stroke ?? null) : null,
|
|
53
|
-
patternId:
|
|
54
|
-
aesthetics.includes('pattern') && patternMap.has(key) ? toPatternId(key) : null,
|
|
55
|
-
shape: aesthetics.includes('symbol') ? (symbolMap.get(key) ?? 'circle') : null
|
|
56
|
-
}))
|
|
55
|
+
items: keys
|
|
56
|
+
.filter((k) => k !== null && k !== undefined)
|
|
57
|
+
.map((key) => buildLegendItem(key, aesthetics, colorMap, patternMap, symbolMap))
|
|
57
58
|
})).filter((group) => group.items.length > 0)
|
|
58
59
|
}
|
|
59
60
|
|
|
@@ -93,14 +94,14 @@ export class ChartBrewer {
|
|
|
93
94
|
colorMap = $derived(
|
|
94
95
|
(this.#channels.fill ?? this.#channels.color)
|
|
95
96
|
? assignColors(distinct(this.#rawData, this.#channels.fill ?? this.#channels.color), this.#mode)
|
|
96
|
-
: new
|
|
97
|
+
: new SvelteMap()
|
|
97
98
|
)
|
|
98
99
|
|
|
99
100
|
/** @type {Map<unknown, string>} */
|
|
100
101
|
patternMap = $derived(
|
|
101
102
|
this.#channels.pattern
|
|
102
103
|
? assignPatterns(distinct(this.#rawData, this.#channels.pattern))
|
|
103
|
-
: new
|
|
104
|
+
: new SvelteMap()
|
|
104
105
|
)
|
|
105
106
|
|
|
106
107
|
/**
|
|
@@ -128,7 +129,7 @@ export class ChartBrewer {
|
|
|
128
129
|
for (const [pk, name] of this.patternMap.entries()) {
|
|
129
130
|
defs.push({ id: toPatternId(pk), name, fill: '#ddd', stroke: '#666' })
|
|
130
131
|
}
|
|
131
|
-
const seenComposite = new
|
|
132
|
+
const seenComposite = new SvelteSet()
|
|
132
133
|
for (const d of this.processedData) {
|
|
133
134
|
const fk = d[ff]
|
|
134
135
|
const pk = d[pf]
|
|
@@ -147,7 +148,7 @@ export class ChartBrewer {
|
|
|
147
148
|
symbolMap = $derived(
|
|
148
149
|
this.#channels.symbol
|
|
149
150
|
? assignSymbols(distinct(this.#rawData, this.#channels.symbol))
|
|
150
|
-
: new
|
|
151
|
+
: new SvelteMap()
|
|
151
152
|
)
|
|
152
153
|
|
|
153
154
|
get innerWidth() { return this.#width - this.#margin.left - this.#margin.right }
|
|
@@ -21,6 +21,11 @@ export function buildXScale(data, field, width, opts = {}) {
|
|
|
21
21
|
.padding(opts.padding ?? 0.2)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function maxFromLayer(layer) {
|
|
25
|
+
if (layer.data && layer.y) return max(layer.data, (d) => Number(d[layer.y])) ?? 0
|
|
26
|
+
return 0
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
/**
|
|
25
30
|
* Builds a y linear scale from 0 to max, extended by any layer overrides.
|
|
26
31
|
* @param {Object[]} data
|
|
@@ -29,13 +34,8 @@ export function buildXScale(data, field, width, opts = {}) {
|
|
|
29
34
|
* @param {Array<{data?: Object[], y?: string}>} layers
|
|
30
35
|
*/
|
|
31
36
|
export function buildYScale(data, field, height, layers = []) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (layer.data && layer.y) {
|
|
35
|
-
const layerMax = max(layer.data, (d) => Number(d[layer.y])) ?? 0
|
|
36
|
-
if (layerMax > maxVal) maxVal = layerMax
|
|
37
|
-
}
|
|
38
|
-
}
|
|
37
|
+
const dataMax = max(data, (d) => Number(d[field])) ?? 0
|
|
38
|
+
const maxVal = layers.reduce((m, layer) => Math.max(m, maxFromLayer(layer)), dataMax)
|
|
39
39
|
return scaleLinear().domain([0, maxVal]).range([height, 0]).nice()
|
|
40
40
|
}
|
|
41
41
|
|
package/src/lib/brewing/stats.js
CHANGED
|
@@ -50,8 +50,12 @@ export function applyBoxStat(data, channels) {
|
|
|
50
50
|
* @param {{ by: string[], value: string, stat: string|Function }} opts
|
|
51
51
|
* @returns {Object[]}
|
|
52
52
|
*/
|
|
53
|
+
function isIdentityOrEmpty(stat, by, value) {
|
|
54
|
+
return stat === 'identity' || by.length === 0 || value === null || value === undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
export function applyAggregate(data, { by, value, stat }) {
|
|
54
|
-
if (stat
|
|
58
|
+
if (isIdentityOrEmpty(stat, by, value)) return data
|
|
55
59
|
const fn = typeof stat === 'function' ? stat : STAT_FNS[stat]
|
|
56
60
|
if (fn === null || fn === undefined) return data
|
|
57
61
|
return dataset(data)
|
package/src/lib/chart.js
CHANGED
|
@@ -46,7 +46,7 @@ class Chart {
|
|
|
46
46
|
// padding
|
|
47
47
|
// flipCoords = false
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
#initFields(opts) {
|
|
50
50
|
this.width = Number(opts.width) || 2048
|
|
51
51
|
this.height = Number(opts.height) || 2048
|
|
52
52
|
this.flipCoords = opts.flipCoords || false
|
|
@@ -58,17 +58,18 @@ class Chart {
|
|
|
58
58
|
this.fill = opts.fill || opts.x
|
|
59
59
|
this.color = opts.color || opts.fill
|
|
60
60
|
this.shape = opts.shape || opts.fill
|
|
61
|
-
|
|
62
61
|
this.padding = opts.padding !== undefined ? Number(opts.padding) : 32
|
|
63
|
-
|
|
64
|
-
this.spacing =
|
|
65
|
-
Number(opts.spacing) >= 0 && Number(opts.spacing) <= 0.5 ? Number(opts.spacing) : 0
|
|
62
|
+
this.spacing = (Number(opts.spacing) >= 0 && Number(opts.spacing) <= 0.5) ? Number(opts.spacing) : 0
|
|
66
63
|
this.margin = {
|
|
67
64
|
top: Number(opts.margin?.top) || 0,
|
|
68
65
|
left: Number(opts.margin?.left) || 0,
|
|
69
66
|
right: Number(opts.margin?.right) || 0,
|
|
70
67
|
bottom: Number(opts.margin?.bottom) || 0
|
|
71
68
|
}
|
|
69
|
+
this.stat = opts.stat || 'identity'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#initDomain(data) {
|
|
72
73
|
this.domain = {
|
|
73
74
|
x: [...new Set(data.map((d) => d[this.x]))],
|
|
74
75
|
y: [...new Set(data.map((d) => d[this.y]))]
|
|
@@ -76,8 +77,9 @@ class Chart {
|
|
|
76
77
|
if (this.flipCoords) {
|
|
77
78
|
this.domain = { y: this.domain.x, x: this.domain.y }
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
}
|
|
80
81
|
|
|
82
|
+
#initData(data) {
|
|
81
83
|
this.data = data.map((d) => ({
|
|
82
84
|
x: this.flipCoords ? d[this.y] : d[this.x],
|
|
83
85
|
y: this.flipCoords ? d[this.x] : d[this.y],
|
|
@@ -85,7 +87,12 @@ class Chart {
|
|
|
85
87
|
color: d[this.color]
|
|
86
88
|
// shape: d[this.shape]
|
|
87
89
|
}))
|
|
90
|
+
}
|
|
88
91
|
|
|
92
|
+
constructor(data, opts) {
|
|
93
|
+
this.#initFields(opts)
|
|
94
|
+
this.#initDomain(data)
|
|
95
|
+
this.#initData(data)
|
|
89
96
|
this.refresh()
|
|
90
97
|
}
|
|
91
98
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
<pattern {id} patternUnits="userSpaceOnUse" width={size} height={size}>
|
|
13
13
|
<rect width={size} height={size} fill="none" />
|
|
14
|
-
{#each resolvedMarks as { type, attrs }}
|
|
14
|
+
{#each resolvedMarks as { type, attrs }, i (i)}
|
|
15
15
|
{#if type === 'line'}
|
|
16
16
|
<line {...attrs} />
|
|
17
17
|
{:else if type === 'circle'}
|
package/src/patterns/README.md
DELETED