svelteplot 0.12.0 → 0.13.0

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.
@@ -210,11 +210,8 @@
210
210
  (fixedWidth || width) - plotOptions.marginLeft - plotOptions.marginRight
211
211
  );
212
212
 
213
- // Width used for aspectRatio/projection height computation only.
214
- // Excludes reactive auto-margins to prevent a feedback loop:
215
- // height → plotHeight → y-tick density → y-label widths → autoMarginLeft → plotWidth → height
216
- // Using explicit user-specified margins (0 when set to 'auto') keeps height stable
217
- // while still updating when the container width or user-specified margins change.
213
+ // Width used for geo-projection aspect-ratio height computation only.
214
+ // Excludes reactive auto-margins to prevent a feedback loop in projection mode.
218
215
  const plotWidthForAspectRatio = $derived(
219
216
  (fixedWidth || width) -
220
217
  maybeMargin(
@@ -267,7 +264,7 @@
267
264
  preScales.x,
268
265
  preScales.y,
269
266
  plotOptions.aspectRatio,
270
- plotWidthForAspectRatio,
267
+ plotWidth,
271
268
  plotOptions.marginTop,
272
269
  plotOptions.marginBottom
273
270
  )
@@ -151,6 +151,14 @@ export function createScale(name, scaleOptions, marks, plotOptions, plotWidth, p
151
151
  !mark.options[ORIGINAL_NAME_KEYS[name]].startsWith('__')) {
152
152
  propNames.add(mark.options[ORIGINAL_NAME_KEYS[name]]);
153
153
  }
154
+ // marks that use a Symbol as fill accessor (e.g. Contour) pass the
155
+ // original field name via ORIGINAL_NAME_KEYS.fill so the color scale
156
+ // can still derive an autoTitle
157
+ if (name === 'color' &&
158
+ mark.options[ORIGINAL_NAME_KEYS.fill] &&
159
+ !mark.options[ORIGINAL_NAME_KEYS.fill].startsWith('__')) {
160
+ propNames.add(mark.options[ORIGINAL_NAME_KEYS.fill]);
161
+ }
154
162
  }
155
163
  else {
156
164
  // also skip marks without data to prevent exceptions
@@ -0,0 +1,13 @@
1
+ import { pathRound as path } from 'd3-path';
2
+ type D3Path = ReturnType<typeof path>;
3
+ export type ShapeRenderer = {
4
+ draw(context: D3Path, l: number, r: number): void;
5
+ };
6
+ declare const defaultRadius = 3;
7
+ export declare const shapeArrow: ShapeRenderer;
8
+ export declare const shapeSpike: ShapeRenderer;
9
+ export declare const shapeArrowFilled: ShapeRenderer;
10
+ export declare function isShapeObject(value: unknown): value is ShapeRenderer;
11
+ export declare function maybeShape(shape: string | ShapeRenderer): ShapeRenderer;
12
+ export declare function shapePath(shape: string | ShapeRenderer, l: number, r: number): string;
13
+ export { defaultRadius };
@@ -0,0 +1,57 @@
1
+ import { pathRound as path } from 'd3-path';
2
+ const defaultRadius = 3;
3
+ const wingRatio = defaultRadius * 5;
4
+ export const shapeArrow = {
5
+ draw(context, l, r) {
6
+ const wing = (l * r) / wingRatio;
7
+ context.moveTo(0, 0);
8
+ context.lineTo(0, -l);
9
+ context.moveTo(-wing, wing - l);
10
+ context.lineTo(0, -l);
11
+ context.lineTo(wing, wing - l);
12
+ }
13
+ };
14
+ export const shapeSpike = {
15
+ draw(context, l, r) {
16
+ context.moveTo(-r, 0);
17
+ context.lineTo(0, -l);
18
+ context.lineTo(r, 0);
19
+ }
20
+ };
21
+ export const shapeArrowFilled = {
22
+ draw(context, l, r) {
23
+ const headLength = Math.max(3, l * 0.3);
24
+ const headSpike = headLength * 0.2;
25
+ const headWidth = Math.max(2, l * 0.3);
26
+ const tailWidth = Math.max(2, l * 0.3) * 0.3;
27
+ context.moveTo(0, 0);
28
+ context.lineTo(tailWidth * 0.5, -l + headLength - headSpike);
29
+ context.lineTo(headWidth * 0.5, -l + headLength);
30
+ context.lineTo(0, -l);
31
+ context.lineTo(-headWidth * 0.5, -l + headLength);
32
+ context.lineTo(-tailWidth * 0.5, -l + headLength - headSpike);
33
+ context.closePath();
34
+ }
35
+ };
36
+ const shapes = new Map([
37
+ ['arrow', shapeArrow],
38
+ ['arrow-filled', shapeArrowFilled],
39
+ ['spike', shapeSpike]
40
+ ]);
41
+ export function isShapeObject(value) {
42
+ return value != null && typeof value.draw === 'function';
43
+ }
44
+ export function maybeShape(shape) {
45
+ if (isShapeObject(shape))
46
+ return shape;
47
+ const value = shapes.get(`${shape}`.toLowerCase());
48
+ if (value)
49
+ return value;
50
+ throw new Error(`invalid shape: ${shape}`);
51
+ }
52
+ export function shapePath(shape, l, r) {
53
+ const context = path();
54
+ maybeShape(shape).draw(context, l, r);
55
+ return context.toString();
56
+ }
57
+ export { defaultRadius };
@@ -43,6 +43,8 @@
43
43
  inset?: ConstantAccessor<number, Datum>;
44
44
  /** controls the sweep direction of the arrow arc; 1 or -1 */
45
45
  sweep?: SweepOption;
46
+ /** if true, renders using Canvas instead of SVG */
47
+ canvas?: boolean;
46
48
  }
47
49
  import type {
48
50
  DataRecord,
@@ -62,6 +64,7 @@
62
64
  } from '../helpers/arrowPath.js';
63
65
  import { replaceChannels } from '../transforms/rename.js';
64
66
  import { addEventHandlers } from './helpers/events.js';
67
+ import ArrowCanvas from './helpers/ArrowCanvas.svelte';
65
68
  import GroupMultiple from './helpers/GroupMultiple.svelte';
66
69
  import { sort } from '../transforms/sort.js';
67
70
  import { indexData } from '../transforms/recordize.js';
@@ -79,6 +82,7 @@
79
82
 
80
83
  const {
81
84
  data = [{} as Datum],
85
+ canvas = false,
82
86
  class: className = '',
83
87
  ...options
84
88
  }: ArrowMarkProps = $derived({
@@ -106,66 +110,73 @@
106
110
  {#snippet children({ usedScales, scaledData })}
107
111
  {@const sweep = maybeSweep(args.sweep) as SweepFunc}
108
112
  <GroupMultiple class="arrow" length={scaledData.length}>
109
- {#each scaledData as d, i (i)}
110
- {#if d.valid}
111
- {@const datum = d.datum as unknown as Datum}
112
- {@const inset = resolveProp(args.inset, datum, 0)}
113
- {@const insetStart = resolveProp(args.insetStart, datum)}
114
- {@const insetEnd = resolveProp(args.insetEnd, datum)}
115
- {@const headAngle = (resolveProp(args.headAngle, datum, 60) ?? 60) as number}
116
- {@const headLength = (resolveProp(args.headLength, datum, 8) ?? 8) as number}
117
- {@const bendVal =
118
- args.bend === true
119
- ? 22.5
120
- : (resolveProp(
121
- args.bend as ConstantAccessor<number, Datum>,
122
- datum,
123
- 0
124
- ) ?? 0)}
125
- {@const strokeWidth = (resolveProp(args.strokeWidth, datum, 1) ?? 1) as number}
126
- {@const arrPath = arrowPath(
127
- d.x1 ?? 0,
128
- d.y1 ?? 0,
129
- d.x2 ?? 0,
130
- d.y2 ?? 0,
131
- maybeNumber(coalesce(insetStart, inset)) ?? 0,
132
- maybeNumber(coalesce(insetEnd, inset)) ?? 0,
133
- headAngle,
134
- headLength,
135
- bendVal,
136
- strokeWidth,
137
- sweep
138
- )}
139
- {@const [style, styleClass] = resolveStyles(
140
- plot,
141
- d,
142
- {
143
- strokeLinecap: 'round',
144
- strokeLinejoin: 'round',
145
- ...args,
146
- strokeWidth: strokeWidth ?? 1.6
147
- },
148
- 'stroke',
149
- usedScales
150
- )}
151
- <g
152
- class={[className]}
153
- {@attach addEventHandlers({
113
+ {#if canvas}
114
+ <ArrowCanvas data={scaledData} options={args as any} {usedScales} />
115
+ {:else}
116
+ {#each scaledData as d, i (i)}
117
+ {#if d.valid}
118
+ {@const datum = d.datum as unknown as Datum}
119
+ {@const inset = resolveProp(args.inset, datum, 0)}
120
+ {@const insetStart = resolveProp(args.insetStart, datum)}
121
+ {@const insetEnd = resolveProp(args.insetEnd, datum)}
122
+ {@const headAngle = (resolveProp(args.headAngle, datum, 60) ??
123
+ 60) as number}
124
+ {@const headLength = (resolveProp(args.headLength, datum, 8) ??
125
+ 8) as number}
126
+ {@const bendVal =
127
+ args.bend === true
128
+ ? 22.5
129
+ : (resolveProp(
130
+ args.bend as ConstantAccessor<number, Datum>,
131
+ datum,
132
+ 0
133
+ ) ?? 0)}
134
+ {@const strokeWidth = (resolveProp(args.strokeWidth, datum, 1) ??
135
+ 1) as number}
136
+ {@const arrPath = arrowPath(
137
+ d.x1 ?? 0,
138
+ d.y1 ?? 0,
139
+ d.x2 ?? 0,
140
+ d.y2 ?? 0,
141
+ maybeNumber(coalesce(insetStart, inset)) ?? 0,
142
+ maybeNumber(coalesce(insetEnd, inset)) ?? 0,
143
+ headAngle,
144
+ headLength,
145
+ bendVal,
146
+ strokeWidth,
147
+ sweep
148
+ )}
149
+ {@const [style, styleClass] = resolveStyles(
154
150
  plot,
155
- options: options as any,
156
- datum: d?.datum
157
- })}>
158
- {#if options.onmouseenter || options.onclick}
159
- <!-- add invisible path in bg for easier mouse access -->
160
- <path
161
- d={arrPath}
162
- style="fill:none;stroke-width: {(strokeWidth || 1) +
163
- 10}; stroke: red; stroke-opacity:0" />
164
- {/if}
165
- <path class={[styleClass]} d={arrPath} {style} />
166
- </g>
167
- {/if}
168
- {/each}
151
+ d,
152
+ {
153
+ strokeLinecap: 'round',
154
+ strokeLinejoin: 'round',
155
+ ...args,
156
+ strokeWidth: strokeWidth ?? 1.6
157
+ },
158
+ 'stroke',
159
+ usedScales
160
+ )}
161
+ <g
162
+ class={[className]}
163
+ {@attach addEventHandlers({
164
+ plot,
165
+ options: options as any,
166
+ datum: d?.datum
167
+ })}>
168
+ {#if options.onmouseenter || options.onclick}
169
+ <!-- add invisible path in bg for easier mouse access -->
170
+ <path
171
+ d={arrPath}
172
+ style="fill:none;stroke-width: {(strokeWidth || 1) +
173
+ 10}; stroke: red; stroke-opacity:0" />
174
+ {/if}
175
+ <path class={[styleClass]} d={arrPath} {style} />
176
+ </g>
177
+ {/if}
178
+ {/each}
179
+ {/if}
169
180
  </GroupMultiple>
170
181
  {/snippet}
171
182
  </Mark>
@@ -177,6 +177,8 @@ declare function $$render<Datum = DataRecord | GeoJSON.GeoJsonObject>(): {
177
177
  inset?: ConstantAccessor<number, Datum>;
178
178
  /** controls the sweep direction of the arrow arc; 1 or -1 */
179
179
  sweep?: SweepOption;
180
+ /** if true, renders using Canvas instead of SVG */
181
+ canvas?: boolean;
180
182
  };
181
183
  exports: {};
182
184
  bindings: "";
@@ -19,7 +19,7 @@
19
19
 
20
20
  const DEFAULTS = getPlotDefaults();
21
21
 
22
- const legendTitle = $derived(plot.options.color.label);
22
+ const legendTitle = $derived(plot.options.color.label ?? plot.scales.color?.autoTitle);
23
23
  const scaleType = $derived(plot.scales.color.type);
24
24
  const tickFormat = $derived(
25
25
  typeof plot.options.color?.tickFormat === 'function'
@@ -158,12 +158,12 @@
158
158
  <style>
159
159
  .color-legend {
160
160
  text-align: left;
161
- font-size: 12px;
161
+ font-size: 11px;
162
162
  display: inline-block;
163
163
  margin-right: 2em;
164
164
  }
165
165
  .title {
166
- font-weight: 500;
166
+ opacity: 0.8;
167
167
  }
168
168
  .item {
169
169
  margin: 0 1em 0.5ex 0;