@rokkit/chart 1.8.0 → 1.8.2
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/DataTable.svelte +54 -0
- package/dist/Plot/DataTable.svelte.d.ts +12 -0
- package/dist/Plot/SpecGeoms.svelte +78 -0
- package/dist/Plot/SpecGeoms.svelte.d.ts +11 -0
- package/dist/Plot.svelte +42 -174
- package/dist/Plot.svelte.d.ts +2 -2
- package/dist/geoms/Arc.svelte +1 -1
- package/dist/geoms/Bar.svelte +23 -32
- package/dist/geoms/lib/marks/arc.d.ts +1 -0
- package/dist/lib/brewing/marks/arcs.d.ts +6 -1
- package/dist/lib/brewing/marks/arcs.js +9 -1
- package/dist/lib/brewing/polar.js +8 -3
- package/dist/lib/plot/spec.d.ts +59 -0
- package/dist/lib/plot/spec.js +131 -0
- package/package.json +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
type Row = Record<string, unknown>
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
/** The plotted rows. */
|
|
6
|
+
rows: Row[]
|
|
7
|
+
/** Column fields, in order (`tableColumns` in lib/plot/spec.js). */
|
|
8
|
+
columns: string[]
|
|
9
|
+
/** The chart's title — names the table and becomes its caption. */
|
|
10
|
+
title?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let { rows, columns, title = '' }: Props = $props()
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<!-- The chart's data as a table: in the DOM for screen readers, never visible. -->
|
|
17
|
+
{#if rows.length > 0 && columns.length > 0}
|
|
18
|
+
<table class="plot-sr-table" aria-label={title || 'Chart data'}>
|
|
19
|
+
{#if title}
|
|
20
|
+
<caption>{title}</caption>
|
|
21
|
+
{/if}
|
|
22
|
+
<thead>
|
|
23
|
+
<tr>
|
|
24
|
+
{#each columns as col (col)}
|
|
25
|
+
<th scope="col">{col}</th>
|
|
26
|
+
{/each}
|
|
27
|
+
</tr>
|
|
28
|
+
</thead>
|
|
29
|
+
<tbody>
|
|
30
|
+
{#each rows as row, i (i)}
|
|
31
|
+
<tr>
|
|
32
|
+
{#each columns as col (col)}
|
|
33
|
+
<td>{row[col] ?? ''}</td>
|
|
34
|
+
{/each}
|
|
35
|
+
</tr>
|
|
36
|
+
{/each}
|
|
37
|
+
</tbody>
|
|
38
|
+
</table>
|
|
39
|
+
{/if}
|
|
40
|
+
|
|
41
|
+
<style>
|
|
42
|
+
/* Visually hidden — in DOM for screen readers, not visible */
|
|
43
|
+
.plot-sr-table {
|
|
44
|
+
position: absolute;
|
|
45
|
+
width: 1px;
|
|
46
|
+
height: 1px;
|
|
47
|
+
padding: 0;
|
|
48
|
+
margin: -1px;
|
|
49
|
+
overflow: hidden;
|
|
50
|
+
clip: rect(0, 0, 0, 0);
|
|
51
|
+
white-space: nowrap;
|
|
52
|
+
border: 0;
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type Row = Record<string, unknown>;
|
|
2
|
+
type Props = {
|
|
3
|
+
/** The plotted rows. */
|
|
4
|
+
rows: Row[];
|
|
5
|
+
/** Column fields, in order (`tableColumns` in lib/plot/spec.js). */
|
|
6
|
+
columns: string[];
|
|
7
|
+
/** The chart's title — names the table and becomes its caption. */
|
|
8
|
+
title?: string;
|
|
9
|
+
};
|
|
10
|
+
declare const DataTable: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type DataTable = ReturnType<typeof DataTable>;
|
|
12
|
+
export default DataTable;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Component } from 'svelte'
|
|
3
|
+
import type { PlotSpec, PlotHelpers } from '../lib/plot/types.js'
|
|
4
|
+
import { specGeomProps } from '../lib/plot/spec.js'
|
|
5
|
+
import Bar from '../geoms/Bar.svelte'
|
|
6
|
+
import Line from '../geoms/Line.svelte'
|
|
7
|
+
import Area from '../geoms/Area.svelte'
|
|
8
|
+
import Point from '../geoms/Point.svelte'
|
|
9
|
+
import Arc from '../geoms/Arc.svelte'
|
|
10
|
+
import Box from '../geoms/Box.svelte'
|
|
11
|
+
import Violin from '../geoms/Violin.svelte'
|
|
12
|
+
import Heatmap from '../geoms/Heatmap.svelte'
|
|
13
|
+
import Candlestick from '../geoms/Candlestick.svelte'
|
|
14
|
+
import Waterfall from '../geoms/Waterfall.svelte'
|
|
15
|
+
import Hexbin from '../geoms/Hexbin.svelte'
|
|
16
|
+
import Ribbon from '../geoms/Ribbon.svelte'
|
|
17
|
+
import Radar from '../geoms/Radar.svelte'
|
|
18
|
+
import Rule from '../geoms/Rule.svelte'
|
|
19
|
+
import Region from '../geoms/Region.svelte'
|
|
20
|
+
import Hull from '../geoms/Hull.svelte'
|
|
21
|
+
import Contour from '../geoms/Contour.svelte'
|
|
22
|
+
|
|
23
|
+
type Props = {
|
|
24
|
+
/** The chart spec; its `geoms` are drawn, and its channels are what each one inherits. */
|
|
25
|
+
spec?: PlotSpec
|
|
26
|
+
/** `helpers.geoms` replaces a built-in (or an annotation) by type name. */
|
|
27
|
+
helpers?: PlotHelpers
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let { spec = undefined, helpers = {} }: Props = $props()
|
|
31
|
+
|
|
32
|
+
// Geom component resolver for spec-driven mode. Typed as a lookup table, not the union of
|
|
33
|
+
// its members: a spec names geoms by string at runtime, exactly like `helpers.geoms`, and the
|
|
34
|
+
// union of every geom's Props has no single shape a generic call site can satisfy.
|
|
35
|
+
// `any` is the honest type for a heterogeneous table resolved by name — the same one the
|
|
36
|
+
// public `PlotHelpers.geoms` already declares in lib/plot/types.js.
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
const GEOM_COMPONENTS: Record<string, Component<any>> = {
|
|
39
|
+
bar: Bar,
|
|
40
|
+
line: Line,
|
|
41
|
+
area: Area,
|
|
42
|
+
point: Point,
|
|
43
|
+
arc: Arc,
|
|
44
|
+
box: Box,
|
|
45
|
+
violin: Violin,
|
|
46
|
+
heatmap: Heatmap,
|
|
47
|
+
candlestick: Candlestick,
|
|
48
|
+
waterfall: Waterfall,
|
|
49
|
+
hexbin: Hexbin,
|
|
50
|
+
ribbon: Ribbon,
|
|
51
|
+
// Radar reads the generic x/y/color this path passes via its own aliases, and takes
|
|
52
|
+
// its axis order from `options.axes` — see Radar.svelte's Props.
|
|
53
|
+
radar: Radar,
|
|
54
|
+
hull: Hull,
|
|
55
|
+
contour: Contour
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Plane annotations read no data, so they take ONLY their own `props` — never the spec's
|
|
59
|
+
// field channels: a Region's `x`/`y` are ranges, and handing it the field name 'instability'
|
|
60
|
+
// would be a range of one string. A helper registered under the same name is a geom again.
|
|
61
|
+
const ANNOTATIONS = { rule: Rule, region: Region }
|
|
62
|
+
|
|
63
|
+
function resolveGeomComponent(type: string) {
|
|
64
|
+
return helpers?.geoms?.[type] ?? GEOM_COMPONENTS[type]
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<!-- Keyed by position, not type: two regions in one spec are normal, and a type key would be a
|
|
69
|
+
duplicate that aborts the whole render. An unknown type renders nothing. -->
|
|
70
|
+
{#each spec?.geoms ?? [] as geomSpec, i (`${geomSpec.type}-${i}`)}
|
|
71
|
+
{@const Annotation = ANNOTATIONS[geomSpec.type as keyof typeof ANNOTATIONS]}
|
|
72
|
+
{@const GeomComponent = resolveGeomComponent(geomSpec.type)}
|
|
73
|
+
{#if Annotation && !helpers?.geoms?.[geomSpec.type]}
|
|
74
|
+
<Annotation {...geomSpec.props ?? {}} />
|
|
75
|
+
{:else if GeomComponent}
|
|
76
|
+
<GeomComponent {...specGeomProps(geomSpec, spec)} />
|
|
77
|
+
{/if}
|
|
78
|
+
{/each}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
import type { PlotSpec, PlotHelpers } from '../lib/plot/types.js';
|
|
3
|
+
type Props = {
|
|
4
|
+
/** The chart spec; its `geoms` are drawn, and its channels are what each one inherits. */
|
|
5
|
+
spec?: PlotSpec;
|
|
6
|
+
/** `helpers.geoms` replaces a built-in (or an annotation) by type name. */
|
|
7
|
+
helpers?: PlotHelpers;
|
|
8
|
+
};
|
|
9
|
+
declare const SpecGeoms: Component<Props, {}, "">;
|
|
10
|
+
type SpecGeoms = ReturnType<typeof SpecGeoms>;
|
|
11
|
+
export default SpecGeoms;
|
package/dist/Plot.svelte
CHANGED
|
@@ -1,30 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getContext } from 'svelte'
|
|
3
|
-
import type {
|
|
3
|
+
import type { Snippet } from 'svelte'
|
|
4
4
|
import { defaultPreset } from './lib/preset.js'
|
|
5
5
|
import type { PlotSpec, PlotHelpers } from './lib/plot/types.js'
|
|
6
|
+
import { resolvePlotConfig, resolveChrome, tableColumns } from './lib/plot/spec.js'
|
|
6
7
|
import PlotSurface from './PlotSurface.svelte'
|
|
7
8
|
import Axis from './Plot/Axis.svelte'
|
|
8
9
|
import Grid from './Plot/Grid.svelte'
|
|
9
10
|
import Legend from './Plot/Legend.svelte'
|
|
10
11
|
import Tooltip from './Plot/Tooltip.svelte'
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import Area from './geoms/Area.svelte'
|
|
14
|
-
import Point from './geoms/Point.svelte'
|
|
15
|
-
import Arc from './geoms/Arc.svelte'
|
|
16
|
-
import Box from './geoms/Box.svelte'
|
|
17
|
-
import Violin from './geoms/Violin.svelte'
|
|
18
|
-
import Heatmap from './geoms/Heatmap.svelte'
|
|
19
|
-
import Candlestick from './geoms/Candlestick.svelte'
|
|
20
|
-
import Waterfall from './geoms/Waterfall.svelte'
|
|
21
|
-
import Hexbin from './geoms/Hexbin.svelte'
|
|
22
|
-
import Ribbon from './geoms/Ribbon.svelte'
|
|
23
|
-
import Radar from './geoms/Radar.svelte'
|
|
24
|
-
import Rule from './geoms/Rule.svelte'
|
|
25
|
-
import Region from './geoms/Region.svelte'
|
|
26
|
-
import Hull from './geoms/Hull.svelte'
|
|
27
|
-
import Contour from './geoms/Contour.svelte'
|
|
12
|
+
import SpecGeoms from './Plot/SpecGeoms.svelte'
|
|
13
|
+
import DataTable from './Plot/DataTable.svelte'
|
|
28
14
|
import Highlight from './geoms/Highlight.svelte'
|
|
29
15
|
import Trend from './geoms/Trend.svelte'
|
|
30
16
|
|
|
@@ -122,102 +108,43 @@
|
|
|
122
108
|
const chartPresetCtx = getContext<ChartPresetCtx | undefined>('chart-preset')
|
|
123
109
|
const chartPreset = $derived(chartPresetCtx?.current ?? defaultPreset)
|
|
124
110
|
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const chartTitle = $derived(spec?.title ?? title)
|
|
130
|
-
const chartSummary = $derived(spec?.summary ?? summary)
|
|
111
|
+
// Spec-over-prop precedence lives in lib/plot/spec.js — one table, not a `??` per field.
|
|
112
|
+
const chrome = $derived(
|
|
113
|
+
resolveChrome(spec, { grid, legend, title, summary, x, y })
|
|
114
|
+
)
|
|
131
115
|
|
|
132
116
|
// Accessible data table — screen reader fallback
|
|
133
117
|
const tableData = $derived(spec?.data ?? data)
|
|
134
|
-
const
|
|
135
|
-
// Dedupe: a channel may repeat (e.g. x === color), and the table's keyed each
|
|
136
|
-
// requires unique column names.
|
|
137
|
-
const cols = [
|
|
138
|
-
...new Set(
|
|
139
|
-
[spec?.x, spec?.y, spec?.color ?? spec?.fill].filter((c): c is string => Boolean(c))
|
|
140
|
-
)
|
|
141
|
-
]
|
|
142
|
-
if (cols.length > 0) return cols
|
|
143
|
-
const first = tableData[0]
|
|
144
|
-
return first ? Object.keys(first) : []
|
|
145
|
-
})
|
|
118
|
+
const columns = $derived(tableColumns(spec, tableData))
|
|
146
119
|
|
|
147
120
|
// Config for the shared PlotSurface (which owns PlotState, the responsive width, context,
|
|
148
121
|
// animation, patterns, and zoom). `width` is the fallback — PlotSurface observes the container.
|
|
149
122
|
function buildPlotConfig() {
|
|
150
|
-
return
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
continuousCategory: spec?.continuousCategory,
|
|
170
|
-
chartPreset,
|
|
171
|
-
onselect,
|
|
172
|
-
selectable
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Geoms from spec (spec-driven API)
|
|
177
|
-
const specGeoms = $derived(spec?.geoms ?? [])
|
|
178
|
-
|
|
179
|
-
const overlayX = $derived(spec?.x ?? x)
|
|
180
|
-
const overlayY = $derived(spec?.y ?? y)
|
|
181
|
-
|
|
182
|
-
// Geom component resolver for spec-driven mode. Typed as a lookup table, not the union of
|
|
183
|
-
// its members: a spec names geoms by string at runtime, exactly like `helpers.geoms`, and the
|
|
184
|
-
// union of every geom's Props has no single shape a generic call site can satisfy.
|
|
185
|
-
// `any` is the honest type for a heterogeneous table resolved by name — the same one the
|
|
186
|
-
// public `PlotHelpers.geoms` already declares in lib/plot/types.js.
|
|
187
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
188
|
-
const GEOM_COMPONENTS: Record<string, Component<any>> = {
|
|
189
|
-
bar: Bar,
|
|
190
|
-
line: Line,
|
|
191
|
-
area: Area,
|
|
192
|
-
point: Point,
|
|
193
|
-
arc: Arc,
|
|
194
|
-
box: Box,
|
|
195
|
-
violin: Violin,
|
|
196
|
-
heatmap: Heatmap,
|
|
197
|
-
candlestick: Candlestick,
|
|
198
|
-
waterfall: Waterfall,
|
|
199
|
-
hexbin: Hexbin,
|
|
200
|
-
ribbon: Ribbon,
|
|
201
|
-
// Radar reads the generic x/y/color this path passes via its own aliases, and takes
|
|
202
|
-
// its axis order from `options.axes` — see Radar.svelte's Props.
|
|
203
|
-
radar: Radar,
|
|
204
|
-
hull: Hull,
|
|
205
|
-
contour: Contour
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Plane annotations read no data, so they take ONLY their own `props` — never the spec's
|
|
209
|
-
// field channels: a Region's `x`/`y` are ranges, and handing it the field name 'instability'
|
|
210
|
-
// would be a range of one string.
|
|
211
|
-
const ANNOTATIONS = { rule: Rule, region: Region }
|
|
212
|
-
|
|
213
|
-
function resolveGeomComponent(type: string) {
|
|
214
|
-
return helpers?.geoms?.[type] ?? GEOM_COMPONENTS[type]
|
|
123
|
+
return resolvePlotConfig(
|
|
124
|
+
spec,
|
|
125
|
+
{
|
|
126
|
+
data,
|
|
127
|
+
width,
|
|
128
|
+
height,
|
|
129
|
+
mode,
|
|
130
|
+
margin,
|
|
131
|
+
helpers,
|
|
132
|
+
xDomain,
|
|
133
|
+
yDomain,
|
|
134
|
+
orientation,
|
|
135
|
+
axisOrigin,
|
|
136
|
+
axisOffset,
|
|
137
|
+
onselect,
|
|
138
|
+
selectable
|
|
139
|
+
},
|
|
140
|
+
chartPreset
|
|
141
|
+
)
|
|
215
142
|
}
|
|
216
143
|
</script>
|
|
217
144
|
|
|
218
145
|
<div class="plot-root">
|
|
219
|
-
{#if
|
|
220
|
-
<div class="plot-title" data-plot-title>{
|
|
146
|
+
{#if chrome.title}
|
|
147
|
+
<div class="plot-title" data-plot-title>{chrome.title}</div>
|
|
221
148
|
{/if}
|
|
222
149
|
|
|
223
150
|
<!-- The shared root shell (PlotState, responsive width, context, animation, patterns, zoom).
|
|
@@ -226,57 +153,33 @@
|
|
|
226
153
|
config={buildPlotConfig()}
|
|
227
154
|
{animate}
|
|
228
155
|
{zoom}
|
|
229
|
-
ariaLabel={
|
|
230
|
-
summary={
|
|
156
|
+
ariaLabel={chrome.title || 'Chart visualization'}
|
|
157
|
+
summary={chrome.summary}
|
|
231
158
|
bind:selected
|
|
232
159
|
>
|
|
233
160
|
<!-- Grid (behind everything) -->
|
|
234
|
-
{#if showGrid}
|
|
235
|
-
<Grid lines={gridLines} {xTicks} {yTicks} />
|
|
161
|
+
{#if chrome.showGrid}
|
|
162
|
+
<Grid lines={chrome.gridLines} {xTicks} {yTicks} />
|
|
236
163
|
{/if}
|
|
237
164
|
|
|
238
165
|
<!-- Declarative children (geom components) -->
|
|
239
166
|
{@render children?.()}
|
|
240
167
|
|
|
241
|
-
<!-- Spec-driven geoms
|
|
242
|
-
|
|
243
|
-
{#each specGeoms as geomSpec, i (`${geomSpec.type}-${i}`)}
|
|
244
|
-
{@const Annotation = ANNOTATIONS[geomSpec.type as keyof typeof ANNOTATIONS]}
|
|
245
|
-
{@const GeomComponent = resolveGeomComponent(geomSpec.type)}
|
|
246
|
-
{#if Annotation && !helpers?.geoms?.[geomSpec.type]}
|
|
247
|
-
<Annotation {...geomSpec.props ?? {}} />
|
|
248
|
-
{:else if GeomComponent}
|
|
249
|
-
<GeomComponent
|
|
250
|
-
x={geomSpec.x ?? spec?.x}
|
|
251
|
-
y={geomSpec.y ?? spec?.y}
|
|
252
|
-
color={geomSpec.color ?? spec?.color}
|
|
253
|
-
fill={geomSpec.fill ?? spec?.fill}
|
|
254
|
-
pattern={geomSpec.pattern}
|
|
255
|
-
symbol={geomSpec.symbol}
|
|
256
|
-
stat={geomSpec.stat}
|
|
257
|
-
label={geomSpec.label}
|
|
258
|
-
options={{
|
|
259
|
-
...(spec?.stack !== undefined ? { stack: spec.stack } : {}),
|
|
260
|
-
...(spec?.orientation !== undefined ? { orientation: spec.orientation } : {}),
|
|
261
|
-
...(geomSpec.options ?? {})
|
|
262
|
-
}}
|
|
263
|
-
{...geomSpec.props ?? {}}
|
|
264
|
-
/>
|
|
265
|
-
{/if}
|
|
266
|
-
{/each}
|
|
168
|
+
<!-- Spec-driven geoms -->
|
|
169
|
+
<SpecGeoms {spec} {helpers} />
|
|
267
170
|
|
|
268
171
|
<!-- Axes -->
|
|
269
172
|
{#if axes}
|
|
270
173
|
<Axis
|
|
271
174
|
type="x"
|
|
272
|
-
label={
|
|
175
|
+
label={chrome.xLabel}
|
|
273
176
|
format={xFormat}
|
|
274
177
|
ticks={xTicks}
|
|
275
178
|
{minorTicks}
|
|
276
179
|
/>
|
|
277
180
|
<Axis
|
|
278
181
|
type="y"
|
|
279
|
-
label={
|
|
182
|
+
label={chrome.yLabel}
|
|
280
183
|
format={yFormat}
|
|
281
184
|
ticks={yTicks}
|
|
282
185
|
{minorTicks}
|
|
@@ -284,16 +187,16 @@
|
|
|
284
187
|
{/if}
|
|
285
188
|
|
|
286
189
|
{#if trend !== null && trend !== undefined}
|
|
287
|
-
<Trend x={overlayX} y={overlayY} {trend} />
|
|
190
|
+
<Trend x={chrome.overlayX} y={chrome.overlayY} {trend} />
|
|
288
191
|
{/if}
|
|
289
192
|
|
|
290
193
|
{#if (highlight !== null && highlight !== undefined) || selectable || selected.length}
|
|
291
|
-
<Highlight x={overlayX} y={overlayY} {highlight} {label} />
|
|
194
|
+
<Highlight x={chrome.overlayX} y={chrome.overlayY} {highlight} {label} />
|
|
292
195
|
{/if}
|
|
293
196
|
|
|
294
197
|
<!-- HTML overlays — outside the svg but INSIDE the plot-state context. -->
|
|
295
198
|
{#snippet overlay()}
|
|
296
|
-
{#if showLegend}
|
|
199
|
+
{#if chrome.showLegend}
|
|
297
200
|
<Legend labels={spec?.labels ?? {}} />
|
|
298
201
|
{/if}
|
|
299
202
|
|
|
@@ -301,29 +204,7 @@
|
|
|
301
204
|
<Tooltip {tooltip} />
|
|
302
205
|
{/if}
|
|
303
206
|
|
|
304
|
-
{
|
|
305
|
-
<table class="plot-sr-table" aria-label={chartTitle || 'Chart data'}>
|
|
306
|
-
{#if chartTitle}
|
|
307
|
-
<caption>{chartTitle}</caption>
|
|
308
|
-
{/if}
|
|
309
|
-
<thead>
|
|
310
|
-
<tr>
|
|
311
|
-
{#each tableColumns as col (col)}
|
|
312
|
-
<th scope="col">{col}</th>
|
|
313
|
-
{/each}
|
|
314
|
-
</tr>
|
|
315
|
-
</thead>
|
|
316
|
-
<tbody>
|
|
317
|
-
{#each tableData as row, i (i)}
|
|
318
|
-
<tr>
|
|
319
|
-
{#each tableColumns as col (col)}
|
|
320
|
-
<td>{row[col] ?? ''}</td>
|
|
321
|
-
{/each}
|
|
322
|
-
</tr>
|
|
323
|
-
{/each}
|
|
324
|
-
</tbody>
|
|
325
|
-
</table>
|
|
326
|
-
{/if}
|
|
207
|
+
<DataTable rows={tableData} {columns} title={chrome.title} />
|
|
327
208
|
{/snippet}
|
|
328
209
|
</PlotSurface>
|
|
329
210
|
</div>
|
|
@@ -341,17 +222,4 @@
|
|
|
341
222
|
text-align: center;
|
|
342
223
|
margin-bottom: 4px;
|
|
343
224
|
}
|
|
344
|
-
|
|
345
|
-
/* Visually hidden — in DOM for screen readers, not visible */
|
|
346
|
-
.plot-sr-table {
|
|
347
|
-
position: absolute;
|
|
348
|
-
width: 1px;
|
|
349
|
-
height: 1px;
|
|
350
|
-
padding: 0;
|
|
351
|
-
margin: -1px;
|
|
352
|
-
overflow: hidden;
|
|
353
|
-
clip: rect(0, 0, 0, 0);
|
|
354
|
-
white-space: nowrap;
|
|
355
|
-
border: 0;
|
|
356
|
-
}
|
|
357
225
|
</style>
|
package/dist/Plot.svelte.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { PlotSpec, PlotHelpers } from './lib/plot/types.js';
|
|
3
3
|
type Row = Record<string, unknown>;
|
|
4
4
|
type Method = string | number | {
|
|
@@ -59,6 +59,6 @@ type Props = {
|
|
|
59
59
|
animate?: boolean;
|
|
60
60
|
children?: Snippet;
|
|
61
61
|
};
|
|
62
|
-
declare const Plot: Component<Props, {}, "selected">;
|
|
62
|
+
declare const Plot: import("svelte").Component<Props, {}, "selected">;
|
|
63
63
|
type Plot = ReturnType<typeof Plot>;
|
|
64
64
|
export default Plot;
|
package/dist/geoms/Arc.svelte
CHANGED
package/dist/geoms/Bar.svelte
CHANGED
|
@@ -91,26 +91,22 @@
|
|
|
91
91
|
const effectiveOrientation = $derived(options.orientation ?? plotState.orientation)
|
|
92
92
|
const seriesField = $derived(fill ?? color)
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
// cf.version is a $state counter that increments on every filter mutation.
|
|
102
|
-
// Reading it here establishes a reactive dependency so the effect re-runs
|
|
103
|
-
// whenever any filter changes — including changes from sibling FilterBars.
|
|
94
|
+
/** Bars the crossfilter dims, by key. A derivation, so it needs no effect writing state. */
|
|
95
|
+
const dimmedByKey = $derived.by((): Record<string, boolean> => {
|
|
96
|
+
if (!cf) return {}
|
|
97
|
+
// cf.version is a $state counter that increments on every filter mutation. Reading it
|
|
98
|
+
// makes this re-derive whenever any filter changes — including a sibling FilterBar's.
|
|
104
99
|
void cf.version
|
|
105
|
-
const
|
|
100
|
+
const dimmed: Record<string, boolean> = {}
|
|
106
101
|
for (const bar of bars) {
|
|
107
|
-
|
|
108
|
-
const dimmedByY = y ? cf.isDimmed(y, bar.data[y]) : false
|
|
109
|
-
next[bar.key] = dimmedByX || dimmedByY
|
|
102
|
+
dimmed[bar.key] = (x ? cf.isDimmed(x, bar.data[x]) : false) || (y ? cf.isDimmed(y, bar.data[y]) : false)
|
|
110
103
|
}
|
|
111
|
-
|
|
104
|
+
return dimmed
|
|
112
105
|
})
|
|
113
106
|
|
|
107
|
+
/** Whether a bar takes clicks and keys at all. */
|
|
108
|
+
const activatable = $derived(filterable || Boolean(onselect) || keyboard || plotState.interactive)
|
|
109
|
+
|
|
114
110
|
function handleBarClick(barX: unknown) {
|
|
115
111
|
if (!filterable || !x || !cf) return
|
|
116
112
|
cf.toggleCategorical(x, barX)
|
|
@@ -130,20 +126,19 @@
|
|
|
130
126
|
)
|
|
131
127
|
}
|
|
132
128
|
|
|
133
|
-
|
|
129
|
+
/**
|
|
130
|
+
* A bar activated by click, Enter or Space. A filterable bar FILTERS — it does not also emit
|
|
131
|
+
* `onselect`, the rule the keyboard path always had and a click did not; otherwise it
|
|
132
|
+
* selects. The plot-level selection (`selectBar`) runs either way.
|
|
133
|
+
*/
|
|
134
|
+
function activate(bar: { data: Row }, event: MouseEvent | KeyboardEvent) {
|
|
134
135
|
if (filterable && x) handleBarClick(bar.data[x])
|
|
135
|
-
onselect?.(bar.data)
|
|
136
|
+
else onselect?.(bar.data)
|
|
136
137
|
selectBar(bar, event)
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
function handleBarKeyActivate(bar: { data: Row }, event: KeyboardEvent) {
|
|
140
|
-
if (event.key
|
|
141
|
-
if (filterable && x) {
|
|
142
|
-
handleBarClick(bar.data[x])
|
|
143
|
-
} else {
|
|
144
|
-
onselect?.(bar.data)
|
|
145
|
-
}
|
|
146
|
-
selectBar(bar, event)
|
|
141
|
+
if (event.key === 'Enter' || event.key === ' ') activate(bar, event)
|
|
147
142
|
}
|
|
148
143
|
</script>
|
|
149
144
|
|
|
@@ -165,14 +160,10 @@
|
|
|
165
160
|
data-plot-category={bar.data[x ?? '']}
|
|
166
161
|
data-dimmed={dimmedByKey[bar.key] ? true : undefined}
|
|
167
162
|
style:cursor={filterable || onselect || plotState.interactive ? 'pointer' : undefined}
|
|
168
|
-
onclick={
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
? (e) => handleBarKeyActivate(bar, e)
|
|
173
|
-
: undefined}
|
|
174
|
-
role={filterable || onselect || keyboard || plotState.interactive ? 'button' : 'graphics-symbol'}
|
|
175
|
-
tabindex={filterable || onselect || keyboard || plotState.interactive ? 0 : undefined}
|
|
163
|
+
onclick={activatable ? (e) => activate(bar, e) : undefined}
|
|
164
|
+
onkeydown={activatable ? (e) => handleBarKeyActivate(bar, e) : undefined}
|
|
165
|
+
role={activatable ? 'button' : 'graphics-symbol'}
|
|
166
|
+
tabindex={activatable ? 0 : undefined}
|
|
176
167
|
use:keyboardNav={keyboard}
|
|
177
168
|
aria-label="{bar.data[x ?? '']}: {bar.data[y ?? '']}"
|
|
178
169
|
onmouseenter={() => plotState.setHovered(bar.data)}
|
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
* @param {number} height
|
|
8
8
|
* @param {{ innerRadius?: number }} opts
|
|
9
9
|
* @param {Map<unknown, string>} [patterns]
|
|
10
|
-
*
|
|
10
|
+
* `key` is the slice's category — what picks its colour and pattern — so it repeats when rows
|
|
11
|
+
* share a category, and is undefined without a colour field. `id` is unique per slice (the key
|
|
12
|
+
* plus its occurrence), for keyed rendering; stable across updates while categories are unique.
|
|
13
|
+
*
|
|
14
|
+
* @returns {{ id: string, d: string | null, fill: string, stroke: string, key: unknown, patternId: string | null, pct: number, centroid: [number, number], data: Record<string, unknown> }[]}
|
|
11
15
|
*/
|
|
12
16
|
export function buildArcs(data: Record<string, unknown>[], channels: {
|
|
13
17
|
color?: string;
|
|
@@ -16,6 +20,7 @@ export function buildArcs(data: Record<string, unknown>[], channels: {
|
|
|
16
20
|
}, colors: Map<any, any>, width: number, height: number, opts?: {
|
|
17
21
|
innerRadius?: number;
|
|
18
22
|
}, patterns?: Map<unknown, string>): {
|
|
23
|
+
id: string;
|
|
19
24
|
d: string | null;
|
|
20
25
|
fill: string;
|
|
21
26
|
stroke: string;
|
|
@@ -11,7 +11,11 @@ import { literalColor, markEntry } from '../colors.js'
|
|
|
11
11
|
* @param {number} height
|
|
12
12
|
* @param {{ innerRadius?: number }} opts
|
|
13
13
|
* @param {Map<unknown, string>} [patterns]
|
|
14
|
-
*
|
|
14
|
+
* `key` is the slice's category — what picks its colour and pattern — so it repeats when rows
|
|
15
|
+
* share a category, and is undefined without a colour field. `id` is unique per slice (the key
|
|
16
|
+
* plus its occurrence), for keyed rendering; stable across updates while categories are unique.
|
|
17
|
+
*
|
|
18
|
+
* @returns {{ id: string, d: string | null, fill: string, stroke: string, key: unknown, patternId: string | null, pct: number, centroid: [number, number], data: Record<string, unknown> }[]}
|
|
15
19
|
*/
|
|
16
20
|
export function buildArcs(data, channels, colors, width, height, opts = {}, patterns) {
|
|
17
21
|
const { color: lf, y: yf } = channels
|
|
@@ -32,6 +36,7 @@ export function buildArcs(data, channels, colors, width, height, opts = {}, patt
|
|
|
32
36
|
// Label radius: midpoint between inner and outer (or 70% out for solid pie)
|
|
33
37
|
const labelRadius = innerRadius > 0 ? (innerRadius + radius) / 2 : radius * 0.65
|
|
34
38
|
const labelArc = arc().innerRadius(labelRadius).outerRadius(labelRadius)
|
|
39
|
+
const seen = new Map()
|
|
35
40
|
return slices.map((slice, i) => {
|
|
36
41
|
// With a literal color there's no category field, so key each slice by index to keep
|
|
37
42
|
// them distinct (patterns, which are field-keyed, don't apply to a single literal fill).
|
|
@@ -41,7 +46,10 @@ export function buildArcs(data, channels, colors, width, height, opts = {}, patt
|
|
|
41
46
|
!lit && key !== null && key !== undefined && patterns?.has(key) ? toPatternId(String(key)) : null
|
|
42
47
|
const pct = Math.round(((slice.endAngle - slice.startAngle) / total) * 100)
|
|
43
48
|
const [cx, cy] = labelArc.centroid(slice)
|
|
49
|
+
const occurrence = seen.get(key) ?? 0
|
|
50
|
+
seen.set(key, occurrence + 1)
|
|
44
51
|
return {
|
|
52
|
+
id: `${String(key)}#${occurrence}`,
|
|
45
53
|
d: arcGen(slice),
|
|
46
54
|
fill: colorEntry.fill,
|
|
47
55
|
stroke: colorEntry.stroke,
|
|
@@ -43,11 +43,15 @@
|
|
|
43
43
|
* Fills in the derived fields shared by every axis, regardless of which of the two
|
|
44
44
|
* accepted shorthand forms it started from.
|
|
45
45
|
*
|
|
46
|
-
*
|
|
46
|
+
* Only an object is an AxisSpec; any other value — a string, or a number / boolean category
|
|
47
|
+
* read from the data — is the axis key itself. (Treating every non-string as a spec spread a
|
|
48
|
+
* numeric category into `{}`, keying every numeric axis `undefined`.)
|
|
49
|
+
*
|
|
50
|
+
* @param {string|number|AxisSpec} entry
|
|
47
51
|
* @returns {ResolvedAxis}
|
|
48
52
|
*/
|
|
49
53
|
function normaliseAxis(entry) {
|
|
50
|
-
const spec = typeof entry === '
|
|
54
|
+
const spec = typeof entry === 'object' ? entry : { key: entry }
|
|
51
55
|
const label = spec.label ?? spec.key
|
|
52
56
|
return {
|
|
53
57
|
...spec,
|
|
@@ -101,7 +105,8 @@ function axisKeysInData(data, axisField) {
|
|
|
101
105
|
const seen = new Set()
|
|
102
106
|
for (const row of data) {
|
|
103
107
|
const key = row?.[axisField]
|
|
104
|
-
|
|
108
|
+
// A missing category — undefined or null — names no axis.
|
|
109
|
+
if (key !== undefined && key !== null) seen.add(key)
|
|
105
110
|
}
|
|
106
111
|
return [...seen]
|
|
107
112
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The config `PlotSurface` builds its `PlotState` from.
|
|
3
|
+
*
|
|
4
|
+
* @param {Record<string, any> | undefined} spec
|
|
5
|
+
* @param {Record<string, any>} props - the component's props
|
|
6
|
+
* @param {unknown} chartPreset - the resolved preset (context, else the default)
|
|
7
|
+
*/
|
|
8
|
+
export function resolvePlotConfig(spec: Record<string, any> | undefined, props: Record<string, any>, chartPreset: unknown): {
|
|
9
|
+
[k: string]: any;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The chrome around the marks: grid, legend, title, summary, axis labels, and the fields the
|
|
13
|
+
* trend / highlight overlays draw on.
|
|
14
|
+
*
|
|
15
|
+
* `grid`: `false` hides it; `true` is 'auto'; `'x' | 'y' | 'both'` names the lines.
|
|
16
|
+
*
|
|
17
|
+
* @param {Record<string, any> | undefined} spec
|
|
18
|
+
* @param {Record<string, any>} props
|
|
19
|
+
*/
|
|
20
|
+
export function resolveChrome(spec: Record<string, any> | undefined, props: Record<string, any>): {
|
|
21
|
+
showGrid: boolean;
|
|
22
|
+
gridLines: any;
|
|
23
|
+
showLegend: any;
|
|
24
|
+
title: any;
|
|
25
|
+
summary: any;
|
|
26
|
+
overlayX: any;
|
|
27
|
+
overlayY: any;
|
|
28
|
+
xLabel: any;
|
|
29
|
+
yLabel: any;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The accessible table's columns: the spec's channels (deduped — a table's keyed each needs
|
|
33
|
+
* unique names, and `x === color` is normal), else the first row's keys.
|
|
34
|
+
*
|
|
35
|
+
* @param {Record<string, any> | undefined} spec
|
|
36
|
+
* @param {Record<string, unknown>[]} rows
|
|
37
|
+
* @returns {string[]}
|
|
38
|
+
*/
|
|
39
|
+
export function tableColumns(spec: Record<string, any> | undefined, rows: Record<string, unknown>[]): string[];
|
|
40
|
+
/**
|
|
41
|
+
* The props for one `spec.geoms` entry: the spec's channels unless the geom names its own,
|
|
42
|
+
* the spec's stack / orientation under the geom's options, and the geom's `props` over all.
|
|
43
|
+
*
|
|
44
|
+
* @param {Record<string, any>} geomSpec
|
|
45
|
+
* @param {Record<string, any> | undefined} spec
|
|
46
|
+
*/
|
|
47
|
+
export function specGeomProps(geomSpec: Record<string, any>, spec: Record<string, any> | undefined): any;
|
|
48
|
+
/**
|
|
49
|
+
* The PlotState config fields read straight from spec and/or props, in config order.
|
|
50
|
+
* @type {Array<[string, keyof typeof SOURCES]>}
|
|
51
|
+
*/
|
|
52
|
+
export const PLOT_CONFIG_FIELDS: Array<[string, keyof typeof SOURCES]>;
|
|
53
|
+
declare namespace SOURCES {
|
|
54
|
+
function spec(key: any, spec: any): any;
|
|
55
|
+
function prop(key: any, _spec: any, props: any): any;
|
|
56
|
+
function specFirst(key: any, spec: any, props: any): any;
|
|
57
|
+
function propFirst(key: any, spec: any, props: any): any;
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How `PlotChart` reads its inputs: a `spec` object and component props, which overlap.
|
|
3
|
+
*
|
|
4
|
+
* The rule is "a spec value overrides its prop", with one declared exception — `orientation`,
|
|
5
|
+
* where the prop wins so a wrapper can force direction over a shared spec. Written as a table
|
|
6
|
+
* so the exception is visible rather than one `??` in twenty turned the other way. Pure: the
|
|
7
|
+
* component derives from these, and they are tested without rendering.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Where a config field comes from. A null/undefined value falls through to the other source. */
|
|
11
|
+
const SOURCES = {
|
|
12
|
+
spec: (key, spec) => spec?.[key],
|
|
13
|
+
prop: (key, _spec, props) => props[key],
|
|
14
|
+
specFirst: (key, spec, props) => spec?.[key] ?? props[key],
|
|
15
|
+
propFirst: (key, spec, props) => props[key] ?? spec?.[key]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The PlotState config fields read straight from spec and/or props, in config order.
|
|
20
|
+
* @type {Array<[string, keyof typeof SOURCES]>}
|
|
21
|
+
*/
|
|
22
|
+
export const PLOT_CONFIG_FIELDS = [
|
|
23
|
+
['data', 'specFirst'],
|
|
24
|
+
['width', 'specFirst'],
|
|
25
|
+
['height', 'specFirst'],
|
|
26
|
+
['mode', 'prop'],
|
|
27
|
+
['margin', 'prop'],
|
|
28
|
+
['helpers', 'prop'],
|
|
29
|
+
['xDomain', 'specFirst'],
|
|
30
|
+
['yDomain', 'specFirst'],
|
|
31
|
+
['colorDomain', 'spec'],
|
|
32
|
+
['colorScale', 'spec'],
|
|
33
|
+
['colorScheme', 'spec'],
|
|
34
|
+
['colorMidpoint', 'spec'],
|
|
35
|
+
['orientation', 'propFirst'],
|
|
36
|
+
['axisOrigin', 'specFirst'],
|
|
37
|
+
['axisOffset', 'specFirst'],
|
|
38
|
+
['sort', 'spec'],
|
|
39
|
+
['continuousCategory', 'spec'],
|
|
40
|
+
['onselect', 'prop'],
|
|
41
|
+
['selectable', 'prop']
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The config `PlotSurface` builds its `PlotState` from.
|
|
46
|
+
*
|
|
47
|
+
* @param {Record<string, any> | undefined} spec
|
|
48
|
+
* @param {Record<string, any>} props - the component's props
|
|
49
|
+
* @param {unknown} chartPreset - the resolved preset (context, else the default)
|
|
50
|
+
*/
|
|
51
|
+
export function resolvePlotConfig(spec, props, chartPreset) {
|
|
52
|
+
const config = Object.fromEntries(
|
|
53
|
+
PLOT_CONFIG_FIELDS.map(([key, source]) => [key, SOURCES[source](key, spec, props)])
|
|
54
|
+
)
|
|
55
|
+
// A spec names its channels; the prop-driven form has none here (children declare them).
|
|
56
|
+
config.channels = spec ? { x: spec.x, y: spec.y, color: spec.color ?? spec.fill } : {}
|
|
57
|
+
config.labels = spec?.labels ?? {}
|
|
58
|
+
config.chartPreset = chartPreset
|
|
59
|
+
return config
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** An axis label: the spec's label for the field on that axis, else blank. */
|
|
63
|
+
const axisLabel = (spec, field) => spec?.labels?.[field ?? ''] ?? ''
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The chrome around the marks: grid, legend, title, summary, axis labels, and the fields the
|
|
67
|
+
* trend / highlight overlays draw on.
|
|
68
|
+
*
|
|
69
|
+
* `grid`: `false` hides it; `true` is 'auto'; `'x' | 'y' | 'both'` names the lines.
|
|
70
|
+
*
|
|
71
|
+
* @param {Record<string, any> | undefined} spec
|
|
72
|
+
* @param {Record<string, any>} props
|
|
73
|
+
*/
|
|
74
|
+
export function resolveChrome(spec, props) {
|
|
75
|
+
const grid = spec?.grid ?? props.grid
|
|
76
|
+
return {
|
|
77
|
+
showGrid: grid !== false,
|
|
78
|
+
gridLines: typeof grid === 'boolean' ? 'auto' : grid,
|
|
79
|
+
showLegend: spec?.legend ?? props.legend,
|
|
80
|
+
title: spec?.title ?? props.title,
|
|
81
|
+
summary: spec?.summary ?? props.summary,
|
|
82
|
+
overlayX: spec?.x ?? props.x,
|
|
83
|
+
overlayY: spec?.y ?? props.y,
|
|
84
|
+
xLabel: axisLabel(spec, spec?.x),
|
|
85
|
+
yLabel: axisLabel(spec, spec?.y)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The accessible table's columns: the spec's channels (deduped — a table's keyed each needs
|
|
91
|
+
* unique names, and `x === color` is normal), else the first row's keys.
|
|
92
|
+
*
|
|
93
|
+
* @param {Record<string, any> | undefined} spec
|
|
94
|
+
* @param {Record<string, unknown>[]} rows
|
|
95
|
+
* @returns {string[]}
|
|
96
|
+
*/
|
|
97
|
+
export function tableColumns(spec, rows) {
|
|
98
|
+
const channels = [spec?.x, spec?.y, spec?.color ?? spec?.fill].filter(Boolean)
|
|
99
|
+
if (channels.length > 0) return [...new Set(channels)]
|
|
100
|
+
return rows[0] ? Object.keys(rows[0]) : []
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Spec-level options every geom inherits — only when the spec sets them. */
|
|
104
|
+
function inheritedOptions(spec) {
|
|
105
|
+
const options = {}
|
|
106
|
+
if (spec?.stack !== undefined) options.stack = spec.stack
|
|
107
|
+
if (spec?.orientation !== undefined) options.orientation = spec.orientation
|
|
108
|
+
return options
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The props for one `spec.geoms` entry: the spec's channels unless the geom names its own,
|
|
113
|
+
* the spec's stack / orientation under the geom's options, and the geom's `props` over all.
|
|
114
|
+
*
|
|
115
|
+
* @param {Record<string, any>} geomSpec
|
|
116
|
+
* @param {Record<string, any> | undefined} spec
|
|
117
|
+
*/
|
|
118
|
+
export function specGeomProps(geomSpec, spec) {
|
|
119
|
+
return {
|
|
120
|
+
x: geomSpec.x ?? spec?.x,
|
|
121
|
+
y: geomSpec.y ?? spec?.y,
|
|
122
|
+
color: geomSpec.color ?? spec?.color,
|
|
123
|
+
fill: geomSpec.fill ?? spec?.fill,
|
|
124
|
+
pattern: geomSpec.pattern,
|
|
125
|
+
symbol: geomSpec.symbol,
|
|
126
|
+
stat: geomSpec.stat,
|
|
127
|
+
label: geomSpec.label,
|
|
128
|
+
options: { ...inheritedOptions(spec), ...(geomSpec.options ?? {}) },
|
|
129
|
+
...(geomSpec.props ?? {})
|
|
130
|
+
}
|
|
131
|
+
}
|