svelteplot 0.2.0 → 0.2.1

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/Mark.svelte CHANGED
@@ -18,6 +18,7 @@
18
18
  ScaledDataRecord,
19
19
  ScaleType
20
20
  } from './types.js';
21
+ import { isEqual } from 'es-toolkit';
21
22
  import { getUsedScales, projectXY, projectX, projectY } from './helpers/scales.js';
22
23
  import { testFilter, isValid } from './helpers/index.js';
23
24
  import { resolveChannel, resolveProp } from './helpers/resolve.js';
@@ -105,6 +106,13 @@
105
106
 
106
107
  let added = false;
107
108
 
109
+ $effect(() => {
110
+ const prevOptions = untrack(() => mark.options);
111
+ if (!isEqual(prevOptions, optionsWithAutoFacet)) {
112
+ mark.options = optionsWithAutoFacet;
113
+ }
114
+ });
115
+
108
116
  $effect(() => {
109
117
  if (added) return;
110
118
  // without using untrack() here we end up with inexplicable
@@ -118,6 +126,7 @@
118
126
  );
119
127
  mark.data = untrack(() => data);
120
128
  mark.options = untrack(() => optionsWithAutoFacet);
129
+
121
130
  addMark(mark);
122
131
  added = true;
123
132
  });
@@ -225,6 +234,7 @@
225
234
  usedScales.y,
226
235
  suffix
227
236
  );
237
+
228
238
  out[`x${suffix}`] = x;
229
239
  out[`y${suffix}`] = y;
230
240
  out.valid =
@@ -243,7 +253,7 @@
243
253
  ScaleName
244
254
  ][]) {
245
255
  // check if the mark has defined an accessor for this channel
246
- if (options?.[channel] !== undefined && out[channel] === undefined) {
256
+ if (options?.[channel] != null && out[channel] === undefined) {
247
257
  // resolve value
248
258
  const value = row[channel];
249
259
 
@@ -256,6 +266,7 @@
256
266
  : value;
257
267
 
258
268
  out.valid = out.valid && isValid(value);
269
+
259
270
  // apply dx/dy transform
260
271
  out[channel] =
261
272
  scale === 'x' && Number.isFinite(scaled) ? (scaled as number) + dx : scaled;
@@ -16,3 +16,4 @@ export declare const POSITION_CHANNELS: Set<ChannelName>;
16
16
  export declare function parseInset(inset: number | string, width: number): number;
17
17
  export declare function omit<T extends {}, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K>;
18
18
  export declare function identity<T>(x: T): T;
19
+ export declare const GEOJSON_PREFER_STROKE: Set<string>;
@@ -53,3 +53,4 @@ export function omit(obj, ...keys) {
53
53
  export function identity(x) {
54
54
  return x;
55
55
  }
56
+ export const GEOJSON_PREFER_STROKE = new Set(['MultiLineString', 'LineString']);
@@ -4,16 +4,17 @@ import isRawValue from './isRawValue.js';
4
4
  import { isValid } from './isValid.js';
5
5
  import { pick } from 'es-toolkit';
6
6
  import { getBaseStylesObject } from './getBaseStyles.js';
7
+ import { RAW_VALUE } from '../transforms/recordize.js';
7
8
  export function resolveProp(accessor, datum, _defaultValue = null) {
8
9
  if (typeof accessor === 'function') {
9
- // datum.___orig___ exists if an array of raw values was used as dataset and got
10
+ // datum[RAW_VALUE] exists if an array of raw values was used as dataset and got
10
11
  // "recordized" by the recordize transform. We want to hide this wrapping to the user
11
12
  // so we're passing the original value to accessor functions instead of our wrapped record
12
13
  return datum == null
13
14
  ? accessor()
14
- : accessor(datum.___orig___ != null ? datum.___orig___ : datum);
15
+ : accessor(datum[RAW_VALUE] != null ? datum[RAW_VALUE] : datum);
15
16
  }
16
- else if (typeof accessor === 'string' && datum && datum[accessor] !== undefined) {
17
+ else if ((typeof accessor === 'string' || typeof accessor === 'symbol') && datum && datum[accessor] !== undefined) {
17
18
  return datum[accessor];
18
19
  }
19
20
  return isRawValue(accessor) ? accessor : _defaultValue;
@@ -46,10 +47,10 @@ function resolve(datum, accessor, channel, scale) {
46
47
  if (isDataRecord(datum)) {
47
48
  // use accessor function
48
49
  if (typeof accessor === 'function')
49
- // datum.___orig___ exists if an array of raw values was used as dataset and got
50
+ // datum[RAW_VALUE] exists if an array of raw values was used as dataset and got
50
51
  // "recordized" by the recordize transform. We want to hide this wrapping to the user
51
52
  // so we're passing the original value to accessor functions instead of our wrapped record
52
- return accessor(datum.___orig___ != null ? datum.___orig___ : datum);
53
+ return accessor(datum[RAW_VALUE] != null ? datum[RAW_VALUE] : datum);
53
54
  // use accessor string
54
55
  if ((typeof accessor === 'string' || typeof accessor === 'symbol') && datum[accessor] !== undefined)
55
56
  return datum[accessor];
@@ -1,4 +1,4 @@
1
- import type { ChannelAccessor, GenericMarkOptions, Mark, MarkType, PlotDefaults, PlotOptions, PlotScales, PlotState, RawValue, ScaleName, ScaleOptions, ScaleType, ScaledChannelName } from '../types.js';
1
+ import type { ChannelAccessor, GenericMarkOptions, Mark, MarkType, PlotDefaults, PlotOptions, PlotScales, PlotState, RawValue, ScaleName, ScaleOptions, ScaleType, ScaledChannelName, UsedScales } from '../types.js';
2
2
  /**
3
3
  * compute the plot scales
4
4
  */
@@ -35,7 +35,7 @@ export declare function inferScaleType(name: ScaleName, dataValues: RawValue[],
35
35
  * scales, we need to check if the the scale is supposed to be used
36
36
  * not. That's what this function is used for.
37
37
  */
38
- export declare function getUsedScales(plot: PlotState, options: GenericMarkOptions, mark: Mark<GenericMarkOptions>): { [k in ScaledChannelName]: boolean; };
38
+ export declare function getUsedScales(plot: PlotState, options: GenericMarkOptions, mark: Mark<GenericMarkOptions>): UsedScales;
39
39
  export declare function looksLikeANumber(input: string | number): boolean;
40
40
  export declare function projectXY(scales: PlotScales, x: RawValue, y: RawValue, useXScale?: boolean, useYScale?: boolean): [number, number];
41
41
  export declare function projectX(channel: 'x' | 'x1' | 'x2', scales: PlotScales, value: RawValue): number;
@@ -113,7 +113,7 @@ export function createScale(name, scaleOptions, marks, plotOptions, plotWidth, p
113
113
  for (const datum of mark.data) {
114
114
  const value = resolveProp(channelOptions.value, datum);
115
115
  dataValues.add(value);
116
- if (name === 'color' && scaleOptions.type === 'quantile') {
116
+ if (name === 'color' && scaleOptions.type === 'quantile' || scaleOptions.type === 'quantile-cont') {
117
117
  allDataValues.push(value);
118
118
  }
119
119
  }
@@ -146,6 +146,7 @@ export function createScale(name, scaleOptions, marks, plotOptions, plotWidth, p
146
146
  if (isOrdinal && sortOrdinalDomain) {
147
147
  valueArr.sort(ascending);
148
148
  }
149
+ const valueArray = type === 'quantile' || type === 'quantile-cont' ? allDataValues.toSorted() : valueArr;
149
150
  const domain = scaleOptions.domain
150
151
  ? isOrdinal
151
152
  ? scaleOptions.domain
@@ -157,9 +158,9 @@ export function createScale(name, scaleOptions, marks, plotOptions, plotWidth, p
157
158
  type === 'quantile' ||
158
159
  type === 'quantile-cont'
159
160
  ? name === 'y'
160
- ? valueArr.toReversed()
161
- : valueArr
162
- : extent(scaleOptions.zero ? [0, ...valueArr] : valueArr);
161
+ ? valueArray.toReversed()
162
+ : valueArray
163
+ : extent(scaleOptions.zero ? [0, ...valueArray] : valueArray);
163
164
  if (!scaleOptions.scale) {
164
165
  throw new Error(`No scale function defined for ${name}`);
165
166
  }
@@ -26,16 +26,20 @@ export function isSymbolOrNull(v) {
26
26
  return v == null || ((typeof v === 'string' || typeof v === 'object') && isSymbol(v));
27
27
  }
28
28
  export function isColorOrNull(v) {
29
- return (v == null ||
30
- (typeof v === 'string' &&
31
- (v === 'currentColor' ||
32
- CSS_VAR.test(v) ||
33
- CSS_COLOR.test(v) ||
34
- CSS_COLOR_MIX.test(v) ||
35
- CSS_COLOR_CONTRAST.test(v) ||
36
- CSS_RGBA.test(v) ||
37
- CSS_URL.test(v) ||
38
- color(v) !== null)));
29
+ if (v == null)
30
+ return true;
31
+ if (typeof v === 'string') {
32
+ v = `${v}`.toLowerCase();
33
+ return (v === 'currentcolor' ||
34
+ CSS_VAR.test(v) ||
35
+ CSS_COLOR.test(v) ||
36
+ CSS_COLOR_MIX.test(v) ||
37
+ CSS_COLOR_CONTRAST.test(v) ||
38
+ CSS_RGBA.test(v) ||
39
+ CSS_URL.test(v) ||
40
+ color(v) !== null);
41
+ }
42
+ return false;
39
43
  }
40
44
  export function isOpacityOrNull(v) {
41
45
  return v == null || (typeof v === 'number' && Number.isFinite(v) && v >= 0 && v <= 1);
@@ -2,7 +2,7 @@
2
2
  import { getContext } from 'svelte';
3
3
  import { Plot, AxisX, Frame } from '../index.js';
4
4
  import { symbol as d3Symbol, symbol } from 'd3-shape';
5
- import { range as d3Range } from 'd3-array';
5
+ import { range as d3Range, extent } from 'd3-array';
6
6
  import { maybeSymbol } from '../helpers/symbols.js';
7
7
 
8
8
  import type { DefaultOptions, PlotContext } from '../types.js';
@@ -72,7 +72,7 @@
72
72
  </div>
73
73
  {/each}
74
74
  {:else if scaleType === 'quantile' || scaleType === 'quantize' || scaleType === 'threshold'}
75
- {@const domain = plot.scales.color.domain}
75
+ {@const domain = extent(plot.scales.color.fn.domain())}
76
76
  {@const range = plot.scales.color.range}
77
77
  {@const tickLabels =
78
78
  scaleType === 'quantile'
@@ -85,7 +85,6 @@
85
85
  domain[1],
86
86
  (domain[1] - domain[0]) / range.length
87
87
  ).slice(1)}
88
-
89
88
  <Plot
90
89
  maxWidth="240px"
91
90
  margins={1}
@@ -112,16 +111,13 @@
112
111
  </linearGradient>
113
112
  </defs>
114
113
  <Frame dy={-5} stroke={null} fill="url(#gradient-{randId})" />
115
- <AxisX tickSize={18} dy={-17} />
114
+ <AxisX tickSize={18} dy={-17} tickFormat={(d, i) => tickFormat(tickLabels[i])} />
116
115
  </Plot>
117
116
  {:else}
118
117
  <!--- continuous -->
119
- {@const domain = plot.scales.color.domain}
120
- {@const ticks = new Set([
121
- domain[0],
122
- ...(plot.scales.color?.fn?.ticks?.(Math.ceil(width / 5)) ?? []),
123
- domain[1]
124
- ])}
118
+ {@const domain = extent(plot.scales.color.domain)}
119
+ {@const ticks = d3Range(domain[0], domain[1], (domain[1] - domain[0]) / 7).slice(1)}
120
+
125
121
  <Plot
126
122
  maxWidth="240px"
127
123
  margins={1}
@@ -53,7 +53,7 @@
53
53
  }: DotProps = $props();
54
54
 
55
55
  const { getPlotState } = getContext<PlotContext>('svelteplot');
56
- let plot = $derived(getPlotState());
56
+ const plot = $derived(getPlotState());
57
57
 
58
58
  function getSymbolPath(symbolType, size) {
59
59
  return d3Symbol(maybeSymbol(symbolType), size)();
@@ -96,7 +96,7 @@
96
96
  {#snippet children({ mark, usedScales, scaledData })}
97
97
  <g class="dots {className || ''}">
98
98
  {#if canvas}
99
- <DotCanvas data={args.data} {mark} {plot} {testFacet} {usedScales} />
99
+ <DotCanvas data={scaledData} {mark} />
100
100
  {:else}
101
101
  {#each scaledData as d}
102
102
  {#if d.valid && isValid(d.r)}
@@ -1,15 +1,22 @@
1
1
  <script lang="ts">
2
2
  import { getContext } from 'svelte';
3
- import type { DataRecord, PlotContext, BaseMarkProps } from '../types.js';
3
+ import type {
4
+ DataRecord,
5
+ PlotContext,
6
+ BaseMarkProps,
7
+ FacetContext,
8
+ ConstantAccessor,
9
+ UsedScales
10
+ } from '../types.js';
4
11
  import Mark from '../Mark.svelte';
5
12
  import { geoPath } from 'd3-geo';
6
- import { resolveChannel, resolveProp, resolveScaledStyles } from '../helpers/resolve.js';
7
- import { getUsedScales } from '../helpers/scales.js';
13
+ import { resolveChannel, resolveProp, resolveStyles } from '../helpers/resolve.js';
8
14
  import callWithProps from '../helpers/callWithProps.js';
9
15
  import { sort } from '../index.js';
10
- import { testFilter } from '../helpers/index.js';
11
16
  import { addEventHandlers } from './helpers/events.js';
12
17
  import GeoCanvas from './helpers/GeoCanvas.svelte';
18
+ import { recordize } from '../transforms/recordize.js';
19
+ import { GEOJSON_PREFER_STROKE } from '../helpers/index.js';
13
20
 
14
21
  const { getPlotState } = getContext<PlotContext>('svelteplot');
15
22
  const plot = $derived(getPlotState());
@@ -19,6 +26,8 @@
19
26
  geoType?: 'sphere' | 'graticule';
20
27
  dragRotate: boolean;
21
28
  canvas: boolean;
29
+ href: ConstantAccessor<string>;
30
+ target: ConstantAccessor<string>;
22
31
  } & BaseMarkProps;
23
32
 
24
33
  let {
@@ -39,60 +48,60 @@
39
48
  );
40
49
 
41
50
  const args = $derived(
42
- sort({
43
- data,
44
- ...(options.r ? { sort: { channel: '-r' } } : {}),
45
- ...options
46
- })
51
+ sort(
52
+ recordize({
53
+ data,
54
+ ...(options.r ? { sort: { channel: '-r' } } : {}),
55
+ ...options
56
+ })
57
+ )
47
58
  );
48
- const preferStroke = new Set(['MultiLineString', 'LineString']);
49
-
50
- const { getTestFacet } = getContext<FacetContext>('svelteplot/facet');
51
- const testFacet = $derived(getTestFacet());
52
59
  </script>
53
60
 
54
61
  <Mark
55
62
  type="geo"
56
63
  channels={['fill', 'stroke', 'opacity', 'fillOpacity', 'strokeOpacity', 'r']}
57
64
  {...args}>
58
- {#snippet children({ mark, usedScales })}
65
+ {#snippet children({ mark, scaledData, usedScales })}
66
+ {#snippet el(d)}
67
+ {@const title = resolveProp(args.title, d.datum, '')}
68
+ {@const geometry = resolveProp(args.geometry, d.datum, d.datum)}
69
+ {@const [style, styleClass] = resolveStyles(
70
+ plot,
71
+ d,
72
+ args,
73
+ GEOJSON_PREFER_STROKE.has(geometry.type) ? 'stroke' : 'fill',
74
+ usedScales
75
+ )}
76
+ <path
77
+ d={path(geometry)}
78
+ {style}
79
+ class={[styleClass]}
80
+ use:addEventHandlers={{
81
+ getPlotState,
82
+ options: args,
83
+ datum: d.datum
84
+ }}>
85
+ {#if title}<title>{title}</title>{/if}
86
+ </path>
87
+ {/snippet}
59
88
  <g
60
89
  aria-label="geo"
61
90
  class={['geo', geoType && `geo-${geoType}`, className]}
62
91
  style="fill:currentColor">
63
92
  {#if canvas}
64
- <GeoCanvas data={args.data} {mark} {plot} {testFacet} {usedScales} {path} />
93
+ <GeoCanvas data={scaledData} {path} {mark} {usedScales} />
65
94
  {:else}
66
- {#each args.data as datum}
67
- {#if testFilter(datum, mark.options) && testFacet(datum, mark.options)}
68
- {#snippet el(datum)}
69
- {@const title = resolveProp(args.title, datum, '')}
70
- {@const geometry = resolveProp(args.geometry, datum, datum)}
71
- <path
72
- d={path(geometry)}
73
- style={resolveScaledStyles(
74
- datum,
75
- args,
76
- usedScales,
77
- plot,
78
- preferStroke.has(geometry.type) ? 'stroke' : 'fill'
79
- )}
80
- use:addEventHandlers={{
81
- getPlotState,
82
- options: args,
83
- datum
84
- }}>
85
- {#if title}<title>{title}</title>{/if}
86
- </path>
87
- {/snippet}
95
+ {#each scaledData as d}
96
+ {#if d.valid}
88
97
  {#if options.href}
89
98
  <a
90
- href={resolveProp(args.href, datum, '')}
91
- target={resolveProp(args.target, datum, '_self')}>
92
- {@render el(datum)}
99
+ href={resolveProp(args.href, d.datum, '')}
100
+ target={resolveProp(args.target, d.datum, '_self')}>
101
+ {@render el(d)}
93
102
  </a>
94
103
  {:else}
95
- {@render el(datum)}
104
+ {@render el(d)}
96
105
  {/if}
97
106
  {/if}
98
107
  {/each}
@@ -1,9 +1,11 @@
1
- import type { DataRecord, BaseMarkProps } from '../types.js';
1
+ import type { DataRecord, BaseMarkProps, ConstantAccessor } from '../types.js';
2
2
  type GeoMarkProps = {
3
3
  data: DataRecord[];
4
4
  geoType?: 'sphere' | 'graticule';
5
5
  dragRotate: boolean;
6
6
  canvas: boolean;
7
+ href: ConstantAccessor<string>;
8
+ target: ConstantAccessor<string>;
7
9
  } & BaseMarkProps;
8
10
  declare const Geo: import("svelte").Component<GeoMarkProps, {}, "">;
9
11
  type Geo = ReturnType<typeof Geo>;
@@ -39,9 +39,9 @@
39
39
 
40
40
  <Mark
41
41
  type="gridX"
42
- data={data.length ? data.map((tick) => ({ __x: tick })) : []}
42
+ data={data.length ? data.map((tick) => ({ [RAW_VALUE]: tick })) : []}
43
43
  channels={['y1', 'y2', 'x', 'stroke', 'strokeOpacity']}
44
- {...{ ...options, x: '__x' }}
44
+ {...{ ...options, x: RAW_VALUE }}
45
45
  {automatic}>
46
46
  {#snippet children({ usedScales })}
47
47
  <g class="grid-x">
@@ -36,9 +36,9 @@
36
36
 
37
37
  <Mark
38
38
  type="gridY"
39
- data={data.length ? data.map((tick) => ({ ___orig___: tick })) : []}
39
+ data={data.length ? data.map((tick) => ({ [RAW_VALUE]: tick })) : []}
40
40
  channels={['x1', 'x2', 'y', 'stroke', 'strokeOpacity']}
41
- {...{ ...options, y: '___orig___' }}
41
+ {...{ ...options, y: RAW_VALUE }}
42
42
  {automatic}>
43
43
  {#snippet children({ usedScales })}
44
44
  <g class="grid-y">
@@ -31,6 +31,7 @@
31
31
  import { quadtree } from 'd3-quadtree';
32
32
  import { projectXY } from '../helpers/scales.js';
33
33
  import isDataRecord from '../helpers/isDataRecord.js';
34
+ import { RAW_VALUE } from '../transforms/recordize.js';
34
35
 
35
36
  let {
36
37
  data = [{}],
@@ -109,7 +110,7 @@
109
110
  true
110
111
  );
111
112
  return {
112
- ...(isDataRecord(d) ? d : { ___orig___: d }),
113
+ ...(isDataRecord(d) ? d : { [RAW_VALUE]: d }),
113
114
  __pointerX: px,
114
115
  __pointerY: py
115
116
  };
@@ -1,18 +1,12 @@
1
1
  <script lang="ts">
2
- let {
3
- canvas = $bindable(),
4
- devicePixelRatio = $bindable(1),
5
- plot
6
- }: {
7
- canvas: HTMLCanvasElement;
8
- devicePixelRatio: number;
9
- plot: PlotState;
10
- } = $props();
2
+ import { getContext } from 'svelte';
3
+ import type { PlotContext } from '../../types';
4
+ import { devicePixelRatio } from 'svelte/reactivity/window';
11
5
 
12
- $effect(() => {
13
- devicePixelRatio = window.devicePixelRatio || 1;
14
- const ctx = canvas.getContext('2d');
15
- });
6
+ let restProps: {} = $props();
7
+
8
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
9
+ const plot = $derived(getPlotState());
16
10
  </script>
17
11
 
18
12
  <!--
@@ -24,9 +18,9 @@
24
18
  <foreignObject x="0" y="0" width={plot.width} height={plot.height}>
25
19
  <canvas
26
20
  xmlns="http://www.w3.org/1999/xhtml"
27
- bind:this={canvas}
28
- width={plot.width * devicePixelRatio}
29
- height={plot.height * devicePixelRatio}
21
+ {...restProps}
22
+ width={plot.width * (devicePixelRatio.current ?? 1)}
23
+ height={plot.height * (devicePixelRatio.current ?? 1)}
30
24
  style="width: {plot.width}px; height: {plot.height}px;"></canvas>
31
25
  </foreignObject>
32
26
 
@@ -1,13 +1,9 @@
1
- type $$ComponentProps = {
2
- canvas: HTMLCanvasElement;
3
- devicePixelRatio: number;
4
- plot: PlotState;
5
- };
1
+ type $$ComponentProps = {};
6
2
  /**
7
3
  * The CanvasLayer component is a helper component that inserts a
8
4
  * canvas element inside a foreignObject for use in a plot and takes care of
9
5
  * scaling it to the device pixel ratio.
10
6
  */
11
- declare const CanvasLayer: import("svelte").Component<$$ComponentProps, {}, "canvas" | "devicePixelRatio">;
7
+ declare const CanvasLayer: import("svelte").Component<$$ComponentProps, {}, "">;
12
8
  type CanvasLayer = ReturnType<typeof CanvasLayer>;
13
9
  export default CanvasLayer;
@@ -1,29 +1,30 @@
1
1
  <script lang="ts">
2
- import type { PlotState, Mark, DataRecord, BaseMarkProps } from '../../types.js';
2
+ import type {
3
+ PlotState,
4
+ Mark,
5
+ BaseMarkProps,
6
+ ScaledDataRecord,
7
+ PlotContext
8
+ } from '../../types.js';
3
9
  import { CSS_VAR } from '../../constants.js';
4
- import { isValid, testFilter } from '../../helpers/index.js';
5
- import { resolveChannel, resolveProp, resolveScaledStyleProps } from '../../helpers/resolve.js';
6
- import { projectXY } from '../../helpers/scales.js';
10
+ import { resolveProp } from '../../helpers/resolve.js';
7
11
  import { maybeSymbol } from '../../helpers/symbols.js';
8
12
  import { symbol as d3Symbol } from 'd3-shape';
9
- import { untrack } from 'svelte';
10
- import { isEqual } from 'es-toolkit';
13
+ import type { Attachment } from 'svelte/attachments';
14
+ import CanvasLayer from './CanvasLayer.svelte';
15
+ import { getContext } from 'svelte';
16
+ import { devicePixelRatio } from 'svelte/reactivity/window';
11
17
 
12
- let canvas: HTMLCanvasElement | undefined = $state();
13
- let devicePixelRatio = $state(1);
18
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
19
+ const plot = $derived(getPlotState());
14
20
 
15
21
  let {
16
22
  mark,
17
- plot,
18
- data,
19
- testFacet,
20
- usedScales
23
+ data
21
24
  }: {
22
25
  mark: Mark<BaseMarkProps>;
23
26
  plot: PlotState;
24
- data: DataRecord[];
25
- testFacet: any;
26
- usedScales: any;
27
+ data: ScaledDataRecord[];
27
28
  } = $props();
28
29
 
29
30
  function drawSymbolPath(symbolType: string, size: number, context) {
@@ -31,154 +32,76 @@
31
32
  return d3Symbol(maybeSymbol(symbolType), size).context(context)();
32
33
  }
33
34
 
34
- function scaleHash(scale) {
35
- return { domain: scale.domain, type: scale.type, range: scale.range };
36
- }
37
-
38
- let _plotSize = $state([plot.width, plot.height]);
39
- let _usedScales = $state(usedScales);
40
35
  let _markOptions = $state(mark.options);
41
- const xScale = $derived(scaleHash(plot.scales.x));
42
- const yScale = $derived(scaleHash(plot.scales.y));
43
- const rScale = $derived(scaleHash(plot.scales.r));
44
- let _xScale = $state(xScale);
45
- let _yScale = $state(yScale);
46
- let _rScale = $state(rScale);
47
-
48
- const filteredData = $derived(
49
- data.filter((datum) => testFilter(datum, _markOptions) && testFacet(datum, _markOptions))
50
- );
51
-
52
- let _filteredData: DataRecord[] = $state([]);
53
-
54
- $effect(() => {
55
- // update _usedScales only if changed
56
- if (!isEqual(usedScales, _usedScales)) _usedScales = usedScales;
57
- if (!isEqual(mark.options, _markOptions)) _markOptions = mark.options;
58
-
59
- const plotSize = [plot.width, plot.height];
60
- if (!isEqual(plotSize, _plotSize)) _plotSize = plotSize;
61
-
62
- if (
63
- _markOptions.filter
64
- ? !isEqual(filteredData, _filteredData)
65
- : filteredData.length !== _filteredData.length
66
- ) {
67
- _filteredData = filteredData;
68
- }
69
- if (!isEqual(xScale, _xScale)) _xScale = xScale;
70
- if (!isEqual(yScale, _yScale)) _yScale = yScale;
71
- if (!isEqual(rScale, _rScale)) _rScale = rScale;
72
- });
73
-
74
- $effect(() => {
75
- // track plot size, since we're untracking the scales
76
- _plotSize;
77
- _markOptions;
78
- _xScale;
79
- _yScale;
80
- _rScale;
81
- const plotScales = untrack(() => plot.scales);
36
+
37
+ const renderDots: Attachment = (canvas: HTMLCanvasElement) => {
82
38
  const context = canvas.getContext('2d');
83
- if (context === null) return;
84
- // this will re-run whenever `color` or `size` change
85
- context.resetTransform();
86
- context.scale(devicePixelRatio, devicePixelRatio);
87
-
88
- for (const datum of _filteredData) {
89
- // untrack the filter test to avoid redrawing when not necessary
90
- const x = resolveChannel('x', datum, _markOptions);
91
- const y = resolveChannel('y', datum, _markOptions);
92
- const r = resolveChannel('r', datum, _markOptions) || 2;
93
- const symbol_ = resolveChannel('symbol', datum, {
94
- symbol: 'circle',
95
- ..._markOptions
96
- });
97
- const symbol = _usedScales.symbol ? plotScales.symbol.fn(symbol_) : symbol_;
98
-
99
- if (isValid(x) && isValid(y) && isValid(r)) {
100
- const [px, py] = projectXY(plotScales, x, y, true, true);
101
-
102
- const r_ = _usedScales.r ? plotScales.r.fn(r) : r;
103
- const size = r_ * r_ * Math.PI * devicePixelRatio;
104
- let { stroke, strokeOpacity, fillOpacity, fill, opacity } = resolveScaledStyleProps(
105
- datum,
106
- _markOptions,
107
- _usedScales,
108
- untrack(() => plot),
109
- 'stroke'
110
- );
111
39
 
112
- if (`${fill}`.toLowerCase() === 'currentcolor')
113
- fill = getComputedStyle(canvas.parentElement.parentElement).getPropertyValue(
114
- 'color'
115
- );
116
- if (`${stroke}`.toLowerCase() === 'currentcolor')
117
- stroke = getComputedStyle(canvas.parentElement.parentElement).getPropertyValue(
118
- 'color'
119
- );
120
- if (CSS_VAR.test(fill))
121
- fill = getComputedStyle(canvas).getPropertyValue(fill.slice(4, -1));
122
- if (CSS_VAR.test(stroke))
123
- stroke = getComputedStyle(canvas).getPropertyValue(stroke.slice(4, -1));
124
-
125
- if (stroke && stroke !== 'none') {
126
- const strokeWidth = resolveProp(_markOptions.strokeWidth, datum, 1.6);
127
- context.lineWidth = strokeWidth;
40
+ $effect(() => {
41
+ if (context) {
42
+ context.resetTransform();
43
+ context.scale(devicePixelRatio.current ?? 1, devicePixelRatio.current ?? 1);
44
+
45
+ for (const datum of data) {
46
+ if (datum.valid) {
47
+ let { fill, stroke } = datum;
48
+
49
+ if (`${fill}`.toLowerCase() === 'currentcolor')
50
+ fill = getComputedStyle(
51
+ canvas?.parentElement?.parentElement
52
+ ).getPropertyValue('color');
53
+ if (`${stroke}`.toLowerCase() === 'currentcolor')
54
+ stroke = getComputedStyle(
55
+ canvas?.parentElement?.parentElement
56
+ ).getPropertyValue('color');
57
+
58
+ if (CSS_VAR.test(fill))
59
+ fill = getComputedStyle(canvas).getPropertyValue(fill.slice(4, -1));
60
+ if (CSS_VAR.test(stroke))
61
+ stroke = getComputedStyle(canvas).getPropertyValue(stroke.slice(4, -1));
62
+
63
+ if (stroke && stroke !== 'none') {
64
+ const strokeWidth = resolveProp(
65
+ _markOptions.strokeWidth,
66
+ datum.datum,
67
+ 1.6
68
+ );
69
+ context.lineWidth = strokeWidth;
70
+ }
71
+
72
+ context.fillStyle = fill ? fill : 'none';
73
+ context.strokeStyle = stroke ? stroke : 'none';
74
+ context.translate(datum.x, datum.y);
75
+
76
+ const size = datum.r * datum.r * Math.PI;
77
+
78
+ context.beginPath();
79
+ drawSymbolPath(datum.symbol, size, context);
80
+ context.closePath();
81
+
82
+ const { opacity = 1, fillOpacity = 1, strokeOpacity = 1 } = datum;
83
+
84
+ if (opacity != null) context.globalAlpha = opacity ?? 1;
85
+ if (fillOpacity != null) context.globalAlpha = (opacity ?? 1) * fillOpacity;
86
+ if (fill && fill !== 'none') context.fill();
87
+ if (strokeOpacity != null)
88
+ context.globalAlpha = (opacity ?? 1) * strokeOpacity;
89
+ if (stroke && stroke !== 'none') context.stroke();
90
+ context.translate(-datum.x, -datum.y);
91
+ }
128
92
  }
129
- context.fillStyle = fill ? fill : 'none';
130
- context.strokeStyle = stroke ? stroke : 'none';
131
- context.translate(px, py);
132
-
133
- context.beginPath();
134
- drawSymbolPath(symbol, size, context);
135
- context.closePath();
136
-
137
- if (opacity != null) context.globalAlpha = opacity ?? 1;
138
- if (fillOpacity != null) context.globalAlpha = (opacity ?? 1) * fillOpacity;
139
- if (fill && fill !== 'none') context.fill();
140
- if (strokeOpacity != null) context.globalAlpha = (opacity ?? 1) * strokeOpacity;
141
- if (stroke && stroke !== 'none') context.stroke();
142
- context.translate(-px, -py);
143
93
  }
144
- }
145
- return () => {
146
- canvas?.getContext('2d')?.clearRect(0, 0, canvas?.width, canvas?.height);
147
- };
148
- });
149
-
150
- // code from https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio
151
- let remove: null | (() => void) = null;
152
-
153
- function updatePixelRatio() {
154
- if (remove != null) {
155
- remove();
156
- }
157
- const mqString = `(resolution: ${window.devicePixelRatio}dppx)`;
158
- const media = matchMedia(mqString);
159
- media.addEventListener('change', updatePixelRatio);
160
- remove = () => {
161
- media.removeEventListener('change', updatePixelRatio);
162
- };
163
- devicePixelRatio = window.devicePixelRatio;
164
- }
165
- $effect(() => {
166
- updatePixelRatio();
167
- });
94
+
95
+ return () => {
96
+ context?.clearRect(
97
+ 0,
98
+ 0,
99
+ plot.width * (devicePixelRatio.current ?? 1),
100
+ plot.height * (devicePixelRatio.current ?? 1)
101
+ );
102
+ };
103
+ });
104
+ };
168
105
  </script>
169
106
 
170
- <foreignObject x="0" y="0" width={plot.width} height={plot.height}>
171
- <canvas
172
- xmlns="http://www.w3.org/1999/xhtml"
173
- bind:this={canvas}
174
- width={plot.width * devicePixelRatio}
175
- height={plot.height * devicePixelRatio}
176
- style="width: {plot.width}px; height: {plot.height}px;"></canvas>
177
- </foreignObject>
178
-
179
- <style>
180
- foreignObject,
181
- canvas {
182
- color: currentColor;
183
- }
184
- </style>
107
+ <CanvasLayer {@attach renderDots} />
@@ -1,10 +1,8 @@
1
- import type { PlotState, Mark, DataRecord, BaseMarkProps } from '../../types.js';
1
+ import type { PlotState, Mark, BaseMarkProps, ScaledDataRecord } from '../../types.js';
2
2
  type $$ComponentProps = {
3
3
  mark: Mark<BaseMarkProps>;
4
4
  plot: PlotState;
5
- data: DataRecord[];
6
- testFacet: any;
7
- usedScales: any;
5
+ data: ScaledDataRecord[];
8
6
  };
9
7
  declare const DotCanvas: import("svelte").Component<$$ComponentProps, {}, "">;
10
8
  type DotCanvas = ReturnType<typeof DotCanvas>;
@@ -1,165 +1,115 @@
1
1
  <script lang="ts">
2
- import type { PlotState, Mark, DataRecord, BaseMarkProps } from '../../types.js';
2
+ import type {
3
+ Mark,
4
+ BaseMarkProps,
5
+ PlotContext,
6
+ ScaledDataRecord,
7
+ UsedScales
8
+ } from '../../types.js';
3
9
  import { CSS_VAR } from '../../constants.js';
4
- import { testFilter } from '../../helpers/index.js';
5
10
  import { resolveProp, resolveScaledStyleProps } from '../../helpers/resolve.js';
6
- import { untrack } from 'svelte';
7
- import { isEqual } from 'es-toolkit';
11
+ import { getContext, untrack } from 'svelte';
8
12
  import { type GeoPath } from 'd3-geo';
9
-
10
- let canvas: HTMLCanvasElement | undefined = $state();
11
- let devicePixelRatio = $state(1);
13
+ import CanvasLayer from './CanvasLayer.svelte';
14
+ import type { Attachment } from 'svelte/attachments';
15
+ import { devicePixelRatio } from 'svelte/reactivity/window';
16
+ import { GEOJSON_PREFER_STROKE } from '../../helpers/index.js';
12
17
 
13
18
  let {
14
19
  mark,
15
- plot,
16
20
  data,
17
- testFacet,
18
- usedScales,
19
- path
21
+ path,
22
+ usedScales
20
23
  }: {
21
24
  mark: Mark<BaseMarkProps>;
22
- plot: PlotState;
23
- data: DataRecord[];
24
- testFacet: any;
25
- usedScales: any;
25
+ data: ScaledDataRecord[];
26
26
  path: GeoPath;
27
+ usedScales: UsedScales;
27
28
  } = $props();
28
29
 
29
- function scaleHash(scale) {
30
- return { domain: scale.domain, type: scale.type, range: scale.range };
31
- }
32
-
33
- let _plotSize = $state([plot.width, plot.height]);
34
- let _usedScales = $state(usedScales);
35
- let _markOptions = $state(mark.options);
36
-
37
- const filteredData = $derived(
38
- data.filter((datum) => testFilter(datum, _markOptions) && testFacet(datum, _markOptions))
39
- );
40
-
41
- let _filteredData: DataRecord[] = $state([]);
42
-
43
- $effect(() => {
44
- // update _usedScales only if changed
45
- if (!isEqual(usedScales, _usedScales)) _usedScales = usedScales;
46
- if (!isEqual(mark.options, _markOptions)) _markOptions = mark.options;
30
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
31
+ const plot = $derived(getPlotState());
47
32
 
48
- const plotSize = [plot.width, plot.height];
49
- if (!isEqual(plotSize, _plotSize)) _plotSize = plotSize;
50
-
51
- if (
52
- _markOptions.filter
53
- ? !isEqual(filteredData, _filteredData)
54
- : filteredData.length !== _filteredData.length
55
- ) {
56
- _filteredData = filteredData;
57
- }
58
- });
59
-
60
- $effect(() => {
61
- // track plot size, since we're untracking the scales
62
- _plotSize;
63
- _markOptions;
33
+ function maybeOpacity(value) {
34
+ return value == null ? 1 : +value;
35
+ }
64
36
 
65
- const plotScales = untrack(() => plot.scales);
37
+ const render: Attachment = (canvas: HTMLCanvasElement) => {
66
38
  const context = canvas.getContext('2d');
67
- if (context === null) return;
68
- // this will re-run whenever `color` or `size` change
69
- context.resetTransform();
70
- context.scale(devicePixelRatio, devicePixelRatio);
71
-
72
- let currentColor;
73
-
74
- path.context(context);
75
-
76
- const plot_ = untrack(() => plot);
77
39
 
78
- for (const datum of _filteredData) {
79
- // untrack the filter test to avoid redrawing when not necessary
80
- let { stroke, fill, opacity, ...restStyles } = resolveScaledStyleProps(
81
- datum,
82
- _markOptions,
83
- _usedScales,
84
- plot_,
85
- 'fill'
86
- );
87
-
88
- const fillOpacity = restStyles['fill-opacity'];
89
- const strokeOpacity = restStyles['stroke-opacity'];
90
-
91
- if (`${fill}`.toLowerCase() === 'currentcolor')
92
- fill =
93
- currentColor ||
94
- (currentColor = getComputedStyle(
95
- canvas?.parentElement?.parentElement
96
- ).getPropertyValue('color'));
97
- if (`${stroke}`.toLowerCase() === 'currentcolor')
98
- stroke =
99
- currentColor ||
100
- (currentColor = getComputedStyle(
101
- canvas?.parentElement?.parentElement
102
- ).getPropertyValue('color'));
103
- if (CSS_VAR.test(fill))
104
- fill = getComputedStyle(canvas).getPropertyValue(fill.slice(4, -1));
105
- if (CSS_VAR.test(stroke))
106
- stroke = getComputedStyle(canvas).getPropertyValue(stroke.slice(4, -1));
107
-
108
- if (stroke && stroke !== 'none') {
109
- const strokeWidth = resolveProp(_markOptions.strokeWidth, datum, 1.6);
110
- context.lineWidth = strokeWidth;
40
+ $effect(() => {
41
+ path.context(context);
42
+ if (context) {
43
+ context.resetTransform();
44
+ context.scale(devicePixelRatio.current ?? 1, devicePixelRatio.current ?? 1);
45
+ let currentColor;
46
+
47
+ for (const d of data) {
48
+ if (!d.valid) continue;
49
+ const geometry = resolveProp(mark.options.geometry, d.datum, d.datum);
50
+ // untrack the filter test to avoid redrawing when not necessary
51
+ let { stroke, fill, ...restStyles } = resolveScaledStyleProps(
52
+ d.datum,
53
+ mark.options,
54
+ usedScales,
55
+ plot,
56
+ GEOJSON_PREFER_STROKE.has(geometry.type) ? 'stroke' : 'fill'
57
+ );
58
+
59
+ const opacity = maybeOpacity(restStyles['opacity']);
60
+ const fillOpacity = maybeOpacity(restStyles['fill-opacity']);
61
+ const strokeOpacity = maybeOpacity(restStyles['stroke-opacity']);
62
+
63
+ if (`${fill}`.toLowerCase() === 'currentcolor')
64
+ fill =
65
+ currentColor ||
66
+ (currentColor = getComputedStyle(
67
+ canvas?.parentElement?.parentElement
68
+ ).getPropertyValue('color'));
69
+ if (`${stroke}`.toLowerCase() === 'currentcolor')
70
+ stroke =
71
+ currentColor ||
72
+ (currentColor = getComputedStyle(
73
+ canvas?.parentElement?.parentElement
74
+ ).getPropertyValue('color'));
75
+ if (CSS_VAR.test(fill))
76
+ fill = getComputedStyle(canvas).getPropertyValue(fill.slice(4, -1));
77
+ if (CSS_VAR.test(stroke))
78
+ stroke = getComputedStyle(canvas).getPropertyValue(stroke.slice(4, -1));
79
+
80
+ if (stroke && stroke !== 'none') {
81
+ const strokeWidth = resolveProp(mark.options.strokeWidth, d.datum, 1);
82
+ context.lineWidth = strokeWidth ?? 1;
83
+ }
84
+
85
+ context.fillStyle = fill ? fill : 'none';
86
+ context.strokeStyle = stroke ? stroke : 'none';
87
+ context.lineJoin = 'round';
88
+ context.beginPath();
89
+
90
+ path(geometry);
91
+ context.closePath();
92
+
93
+ if (opacity != null) context.globalAlpha = opacity;
94
+ if (fillOpacity != null) context.globalAlpha = opacity * fillOpacity;
95
+
96
+ if (fill && fill !== 'none') context.fill();
97
+ if (strokeOpacity != null) context.globalAlpha = opacity * strokeOpacity;
98
+ if (stroke && stroke !== 'none') context.stroke();
99
+ }
111
100
  }
112
- context.fillStyle = fill ? fill : 'none';
113
- context.strokeStyle = stroke ? stroke : 'none';
114
-
115
- context.beginPath();
116
- path(datum);
117
- context.closePath();
118
-
119
- if (opacity != null) context.globalAlpha = opacity ?? 1;
120
- if (fillOpacity != null) context.globalAlpha = (opacity ?? 1) * fillOpacity;
121
-
122
- if (fill && fill !== 'none') context.fill();
123
- if (strokeOpacity != null) context.globalAlpha = (opacity ?? 1) * strokeOpacity;
124
- if (stroke && stroke !== 'none') context.stroke();
125
- }
126
- return () => {
127
- canvas?.getContext('2d')?.clearRect(0, 0, canvas?.width, canvas?.height);
128
- };
129
- });
130
-
131
- // code from https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio
132
- let remove: null | (() => void) = null;
133
-
134
- function updatePixelRatio() {
135
- if (remove != null) {
136
- remove();
137
- }
138
- const mqString = `(resolution: ${window.devicePixelRatio}dppx)`;
139
- const media = matchMedia(mqString);
140
- media.addEventListener('change', updatePixelRatio);
141
- remove = () => {
142
- media.removeEventListener('change', updatePixelRatio);
143
- };
144
- devicePixelRatio = window.devicePixelRatio;
145
- }
146
- $effect(() => {
147
- updatePixelRatio();
148
- });
101
+ // reset path context in case we switch back to SVG
102
+ path.context(null);
103
+ return () => {
104
+ context?.clearRect(
105
+ 0,
106
+ 0,
107
+ plot.width * (devicePixelRatio.current ?? 1),
108
+ plot.height * (devicePixelRatio.current ?? 1)
109
+ );
110
+ };
111
+ });
112
+ };
149
113
  </script>
150
114
 
151
- <foreignObject x="0" y="0" width={plot.width} height={plot.height}>
152
- <canvas
153
- xmlns="http://www.w3.org/1999/xhtml"
154
- bind:this={canvas}
155
- width={plot.width * devicePixelRatio}
156
- height={plot.height * devicePixelRatio}
157
- style="width: {plot.width}px; height: {plot.height}px;"></canvas>
158
- </foreignObject>
159
-
160
- <style>
161
- foreignObject,
162
- canvas {
163
- color: currentColor;
164
- }
165
- </style>
115
+ <CanvasLayer {@attach render} />
@@ -1,12 +1,10 @@
1
- import type { PlotState, Mark, DataRecord, BaseMarkProps } from '../../types.js';
1
+ import type { Mark, BaseMarkProps, ScaledDataRecord, UsedScales } from '../../types.js';
2
2
  import { type GeoPath } from 'd3-geo';
3
3
  type $$ComponentProps = {
4
4
  mark: Mark<BaseMarkProps>;
5
- plot: PlotState;
6
- data: DataRecord[];
7
- testFacet: any;
8
- usedScales: any;
5
+ data: ScaledDataRecord[];
9
6
  path: GeoPath;
7
+ usedScales: UsedScales;
10
8
  };
11
9
  declare const GeoCanvas: import("svelte").Component<$$ComponentProps, {}, "">;
12
10
  type GeoCanvas = ReturnType<typeof GeoCanvas>;
@@ -13,3 +13,4 @@ export declare function recordizeY({ data, ...channels }: TransformArgsRow, { wi
13
13
  * the rest of our code doesn't have to deal with this case anymore.
14
14
  */
15
15
  export declare function recordizeXY({ data, ...channels }: TransformArgsRow): TransformArgsRecord;
16
+ export declare function recordize({ data, ...channels }: TransformArgsRow): TransformArgsRecord;
@@ -11,13 +11,12 @@ export function recordizeX({ data, ...channels }, { withIndex } = { withIndex: t
11
11
  return {
12
12
  data: data.map((value, index) => ({
13
13
  __value: value,
14
- ...(withIndex ? { __index: index } : {}),
14
+ ...(withIndex ? { [INDEX]: index } : {}),
15
15
  [RAW_VALUE]: value,
16
- ___orig___: value
17
16
  })),
18
17
  ...channels,
19
- x: '__value',
20
- ...(withIndex ? { y: '__index' } : {})
18
+ x: RAW_VALUE,
19
+ ...(withIndex ? { y: INDEX } : {})
21
20
  };
22
21
  }
23
22
  return { data: data, ...channels };
@@ -35,7 +34,6 @@ export function recordizeY({ data, ...channels }, { withIndex } = { withIndex: t
35
34
  data: Array.from(data).map((value, index) => ({
36
35
  ...(withIndex ? { __index: index } : {}),
37
36
  [RAW_VALUE]: value,
38
- ___orig___: value
39
37
  })),
40
38
  ...channels,
41
39
  ...(withIndex ? { x: '__index' } : {}),
@@ -76,3 +74,16 @@ export function recordizeXY({ data, ...channels }) {
76
74
  }
77
75
  return { data, ...channels };
78
76
  }
77
+ export function recordize({ data, ...channels }) {
78
+ if (!data)
79
+ return { data, ...channels };
80
+ if (!isDataRecord(data[0])) {
81
+ return {
82
+ data: data.map((d) => ({
83
+ [RAW_VALUE]: d,
84
+ })),
85
+ ...channels,
86
+ };
87
+ }
88
+ return { data, ...channels };
89
+ }
@@ -2,6 +2,9 @@ import isDataRecord from '../helpers/isDataRecord.js';
2
2
  import { resolveChannel } from '../helpers/resolve.js';
3
3
  import { stack, stackOffsetExpand, stackOffsetSilhouette, stackOffsetWiggle, stackOrderAppearance, stackOrderAscending, stackOrderInsideOut, stackOrderNone, stackOffsetDiverging } from 'd3-shape';
4
4
  import { index, union, groups as d3Groups } from 'd3-array';
5
+ import { RAW_VALUE } from './recordize';
6
+ const GROUP = Symbol('group');
7
+ const FACET = Symbol('group');
5
8
  const DEFAULT_STACK_OPTIONS = {
6
9
  order: null,
7
10
  offset: null,
@@ -39,8 +42,8 @@ function stackXY(byDim, data, channels, options) {
39
42
  const resolvedData = data.map((d) => ({
40
43
  ...(isDataRecord(d) ? d : { __orig: d }),
41
44
  [`__${secondDim}`]: resolveChannel(secondDim, d, channels),
42
- __group: groupBy === true ? 'G' : resolveChannel(groupBy, d, channels),
43
- __facet: groupFacetsBy.length > 0
45
+ [GROUP]: groupBy === true ? 'G' : resolveChannel(groupBy, d, channels),
46
+ [FACET]: groupFacetsBy.length > 0
44
47
  ? groupFacetsBy
45
48
  .map((channel) => String(resolveChannel(channel, d, channels)))
46
49
  .join('---')
@@ -51,11 +54,11 @@ function stackXY(byDim, data, channels, options) {
51
54
  const out = [];
52
55
  // first we group the dataset by facets to avoid stacking of rows that are
53
56
  // in separate panels
54
- const groups = d3Groups(resolvedData, (d) => d.__facet);
57
+ const groups = d3Groups(resolvedData, (d) => d[FACET]);
55
58
  for (const [, facetData] of groups) {
56
59
  // now we index the data on the second dimension, e.g. over x
57
60
  // when stacking over y
58
- const indexed = index(facetData, (d) => d[`__${secondDim}`], (d) => d.__group);
61
+ const indexed = index(facetData, (d) => d[`__${secondDim}`], (d) => d[GROUP]);
59
62
  const stackOrder = (series) => {
60
63
  const f = STACK_ORDER[options.order || 'none'];
61
64
  return options.reverse ? f(series).reverse() : f(series);
@@ -64,7 +67,7 @@ function stackXY(byDim, data, channels, options) {
64
67
  const series = stack()
65
68
  .order(stackOrder)
66
69
  .offset(STACK_OFFSET[options.offset])
67
- .keys(union(facetData.map((d) => d.__group)))
70
+ .keys(union(facetData.map((d) => d[GROUP])))
68
71
  .value(([, group], key) => (group.get(key) ? group.get(key)[`__${byDim}`] : 0))(indexed);
69
72
  // and combine it all back into a flat array
70
73
  const newData = series
@@ -75,8 +78,8 @@ function stackXY(byDim, data, channels, options) {
75
78
  .map((d) => {
76
79
  const datum = d.data[1].get(groupKey);
77
80
  // cleanup our internal keys
78
- delete datum.__group;
79
- delete datum.__facet;
81
+ delete datum[GROUP];
82
+ delete datum[FACET];
80
83
  return { ...datum, [`__${byLow}`]: d[0], [`__${byHigh}`]: d[1] };
81
84
  });
82
85
  })
package/dist/types.d.ts CHANGED
@@ -246,11 +246,11 @@ export type PlotOptions = {
246
246
  /**
247
247
  * Options for the shared radius scale
248
248
  */
249
- r: ScaleOptions;
249
+ r: Partial<ScaleOptions>;
250
250
  color: Partial<ColorScaleOptions>;
251
- opacity: ScaleOptions;
252
- symbol: LegendScaleOptions;
253
- length: ScaleOptions;
251
+ opacity: Partial<ScaleOptions>;
252
+ symbol: Partial<LegendScaleOptions>;
253
+ length: Partial<ScaleOptions>;
254
254
  fx: Partial<ScaleOptions>;
255
255
  fy: Partial<ScaleOptions>;
256
256
  children: Snippet<[
@@ -652,4 +652,5 @@ export type MapIndexObject = {
652
652
  };
653
653
  export type MapMethod = 'cumsum' | 'rank' | 'quantile' | ((I: number[], S: number[]) => number[]) | MapIndexObject;
654
654
  export type MapOptions = Partial<Record<ScaledChannelName, MapMethod>>;
655
+ export type UsedScales = Record<ScaledChannelName, boolean>;
655
656
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteplot",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "license": "ISC",
5
5
  "author": {
6
6
  "name": "Gregor Aisch",
@@ -47,15 +47,15 @@
47
47
  "devDependencies": {
48
48
  "@aitodotai/json-stringify-pretty-compact": "^1.3.0",
49
49
  "@emotion/css": "^11.13.5",
50
- "@sveltejs/adapter-auto": "^6.0.0",
50
+ "@sveltejs/adapter-auto": "^6.0.1",
51
51
  "@sveltejs/adapter-static": "^3.0.8",
52
52
  "@sveltejs/eslint-config": "^8.2.0",
53
- "@sveltejs/kit": "^2.20.8",
53
+ "@sveltejs/kit": "^2.21.0",
54
54
  "@sveltejs/package": "^2.3.11",
55
55
  "@sveltejs/vite-plugin-svelte": "5.0.3",
56
- "@sveltepress/theme-default": "^6.0.2",
57
- "@sveltepress/twoslash": "^1.2.1",
58
- "@sveltepress/vite": "^1.2.1",
56
+ "@sveltepress/theme-default": "^6.0.3",
57
+ "@sveltepress/twoslash": "^1.2.2",
58
+ "@sveltepress/vite": "^1.2.2",
59
59
  "@testing-library/svelte": "^5.2.7",
60
60
  "@testing-library/user-event": "^14.6.1",
61
61
  "@types/d3-array": "^3.2.1",
@@ -68,32 +68,33 @@
68
68
  "@types/d3-scale": "^4.0.9",
69
69
  "@types/d3-scale-chromatic": "^3.1.0",
70
70
  "@types/d3-shape": "^3.1.7",
71
- "@typescript-eslint/eslint-plugin": "^8.31.1",
72
- "@typescript-eslint/parser": "^8.31.1",
71
+ "@typescript-eslint/eslint-plugin": "^8.32.1",
72
+ "@typescript-eslint/parser": "^8.32.1",
73
73
  "csstype": "^3.1.3",
74
74
  "d3-dsv": "^3.0.1",
75
75
  "d3-fetch": "^3.0.1",
76
76
  "d3-force": "^3.0.0",
77
77
  "eslint": "^9.26.0",
78
- "eslint-config-prettier": "^10.1.2",
79
- "eslint-plugin-svelte": "3.5.1",
78
+ "eslint-config-prettier": "^10.1.5",
79
+ "eslint-plugin-svelte": "3.7.0",
80
80
  "jsdom": "^26.1.0",
81
81
  "prettier": "^3.5.3",
82
- "prettier-plugin-svelte": "^3.3.3",
82
+ "prettier-plugin-svelte": "^3.4.0",
83
83
  "remark-code-extra": "^1.0.1",
84
84
  "remark-code-frontmatter": "^1.0.0",
85
85
  "resize-observer-polyfill": "^1.5.1",
86
- "sass": "^1.87.0",
87
- "svelte-check": "^4.1.7",
88
- "svelte-eslint-parser": "1.1.3",
86
+ "sass": "^1.89.0",
87
+ "svelte-check": "^4.2.1",
88
+ "svelte-eslint-parser": "1.2.0",
89
89
  "svelte-highlight": "^7.8.3",
90
90
  "topojson-client": "^3.1.0",
91
91
  "tslib": "^2.8.1",
92
92
  "typedoc": "^0.28.4",
93
93
  "typedoc-plugin-markdown": "^4.6.3",
94
94
  "typescript": "^5.8.3",
95
- "vite": "^6.3.4",
96
- "vitest": "^3.1.2"
95
+ "vite": "^6.3.5",
96
+ "vitest": "^3.1.3",
97
+ "vitest-matchmedia-mock": "^2.0.3"
97
98
  },
98
99
  "types": "./dist/index.d.ts",
99
100
  "type": "module",
@@ -114,6 +115,6 @@
114
115
  "es-toolkit": "^1.37.2",
115
116
  "fast-equals": "^5.2.2",
116
117
  "merge-deep": "^3.0.3",
117
- "svelte": "5.28.2"
118
+ "svelte": "5.30.1"
118
119
  }
119
120
  }