svelteplot 0.2.1 → 0.2.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.
@@ -3,7 +3,7 @@ import type { ScaleName, ChannelName, ScaledChannelName, ChannelAccessor, DataRo
3
3
  type ChannelAlias = {
4
4
  channel: ScaledChannelName;
5
5
  };
6
- export declare function resolveProp<T>(accessor: ConstantAccessor<T>, datum: DataRecord | null, _defaultValue?: T | null): T | null;
6
+ export declare function resolveProp<T>(accessor: ConstantAccessor<T>, datum: DataRecord | null, _defaultValue?: T | null): T | typeof _defaultValue;
7
7
  type ChannelOptions = {
8
8
  value: ChannelAccessor;
9
9
  scale?: ScaleName | null;
package/dist/index.d.ts CHANGED
@@ -58,10 +58,12 @@ export { normalizeX, normalizeY } from './transforms/normalize.js';
58
58
  export { group, groupX, groupY, groupZ } from './transforms/group.js';
59
59
  export { intervalX, intervalY } from './transforms/interval.js';
60
60
  export { recordizeX, recordizeY } from './transforms/recordize.js';
61
- export { renameChannels } from './transforms/rename.js';
61
+ export { renameChannels, replaceChannels } from './transforms/rename.js';
62
62
  export { select, selectFirst, selectLast, selectMaxX, selectMaxY, selectMinX, selectMinY } from './transforms/select.js';
63
63
  export { shiftX, shiftY } from './transforms/shift.js';
64
64
  export { sort, shuffle, reverse } from './transforms/sort.js';
65
65
  export { stackX, stackY } from './transforms/stack.js';
66
66
  export { windowX, windowY } from './transforms/window.js';
67
67
  export { formatMonth } from './helpers/formats.js';
68
+ export { default as LinearGradientX } from './marks/helpers/LinearGradientX.svelte';
69
+ export { default as LinearGradientY } from './marks/helpers/LinearGradientY.svelte';
package/dist/index.js CHANGED
@@ -61,11 +61,13 @@ export { normalizeX, normalizeY } from './transforms/normalize.js';
61
61
  export { group, groupX, groupY, groupZ } from './transforms/group.js';
62
62
  export { intervalX, intervalY } from './transforms/interval.js';
63
63
  export { recordizeX, recordizeY } from './transforms/recordize.js';
64
- export { renameChannels } from './transforms/rename.js';
64
+ export { renameChannels, replaceChannels } from './transforms/rename.js';
65
65
  export { select, selectFirst, selectLast, selectMaxX, selectMaxY, selectMinX, selectMinY } from './transforms/select.js';
66
66
  export { shiftX, shiftY } from './transforms/shift.js';
67
67
  export { sort, shuffle, reverse } from './transforms/sort.js';
68
68
  export { stackX, stackY } from './transforms/stack.js';
69
69
  export { windowX, windowY } from './transforms/window.js';
70
- // format helpers
70
+ // helpers
71
71
  export { formatMonth } from './helpers/formats.js';
72
+ export { default as LinearGradientX } from './marks/helpers/LinearGradientX.svelte';
73
+ export { default as LinearGradientY } from './marks/helpers/LinearGradientY.svelte';
@@ -6,13 +6,11 @@
6
6
  import Mark from '../Mark.svelte';
7
7
  import { getContext } from 'svelte';
8
8
  import { stackX, recordizeX, intervalX, sort } from '../index.js';
9
- import { resolveProp, resolveStyles } from '../helpers/resolve.js';
10
- import { roundedRect } from '../helpers/roundedRect.js';
11
9
  import type { PlotContext, BaseMarkProps, RectMarkProps, ChannelAccessor } from '../types.js';
12
10
  import type { StackOptions } from '../transforms/stack.js';
13
11
  import type { DataRow } from '../types.js';
14
- import { addEventHandlers } from './helpers/events.js';
15
12
  import GroupMultiple from './helpers/GroupMultiple.svelte';
13
+ import RectPath from './helpers/RectPath.svelte';
16
14
 
17
15
  type BarXProps = BaseMarkProps & {
18
16
  data: DataRow[];
@@ -21,14 +19,6 @@
21
19
  x2?: ChannelAccessor;
22
20
  y?: ChannelAccessor;
23
21
  stack?: StackOptions;
24
- borderRadius?:
25
- | number
26
- | {
27
- topLeft?: number;
28
- topRight?: number;
29
- bottomRight?: number;
30
- bottomLeft?: number;
31
- };
32
22
  } & RectMarkProps;
33
23
 
34
24
  let { data = [{}], class: className = null, stack, ...options }: BarXProps = $props();
@@ -59,33 +49,17 @@
59
49
  {@const bw = plot.scales.y.fn.bandwidth()}
60
50
  {@const minx = Math.min(d.x1, d.x2)}
61
51
  {@const maxx = Math.max(d.x1, d.x2)}
62
- {@const insetLeft = resolveProp(args.insetLeft, d.datum, 0)}
63
- {@const insetRight = resolveProp(args.insetRight, d.datum, 0)}
64
- {@const insetTop = resolveProp(args.insetTop || args.inset, d.datum, 0)}
65
- {@const insetBottom = resolveProp(args.insetBottom || args.inset, d.datum, 0)}
66
- {@const dx = resolveProp(args.dx, d.datum, 0)}
67
- {@const dy = resolveProp(args.dy, d.datum, 0)}
68
52
  {#if d.valid}
69
- {@const [style, styleClass] = resolveStyles(plot, d, args, 'fill', usedScales)}
70
- <path
71
- d={roundedRect(
72
- 0,
73
- 0,
74
- maxx - minx - insetLeft - insetRight,
75
- bw - insetTop - insetBottom,
76
- options.borderRadius
77
- )}
78
- class={[styleClass, className]}
79
- {style}
80
- transform="translate({[
81
- minx + dx + insetLeft,
82
- d.y + insetTop + dy - bw * 0.5
83
- ]})"
84
- use:addEventHandlers={{
85
- getPlotState,
86
- options: args,
87
- datum: d.datum
88
- }} />
53
+ <RectPath
54
+ {usedScales}
55
+ class={className}
56
+ {options}
57
+ datum={d}
58
+ x={minx}
59
+ useInsetAsFallbackHorizontally={false}
60
+ y={d.y - bw * 0.5}
61
+ width={maxx - minx}
62
+ height={bw} />
89
63
  {/if}
90
64
  {/each}
91
65
  </GroupMultiple>
@@ -6,40 +6,25 @@
6
6
  import Mark from '../Mark.svelte';
7
7
  import { getContext } from 'svelte';
8
8
  import { intervalY, stackY, recordizeY, sort } from '../index.js';
9
- import { resolveProp, resolveStyles } from '../helpers/resolve.js';
10
- import { roundedRect } from '../helpers/roundedRect.js';
11
- import {
12
- type PlotContext,
13
- type BaseMarkProps,
14
- type ChannelAccessor,
15
- type DataRow
16
- } from '../types.js';
9
+ import type { PlotContext, BaseMarkProps, BaseRectMarkProps } from '../types.js';
17
10
  import type { StackOptions } from '../transforms/stack.js';
18
- import { maybeData } from '../helpers/index.js';
19
- import { addEventHandlers } from './helpers/events.js';
20
11
  import GroupMultiple from './helpers/GroupMultiple.svelte';
12
+ import RectPath from './helpers/RectPath.svelte';
21
13
 
22
- type BarYProps = BaseMarkProps & {
23
- data: DataRow[];
24
- x?: ChannelAccessor;
25
- y?: ChannelAccessor;
26
- y1?: ChannelAccessor;
27
- y2?: ChannelAccessor;
28
- stack?: StackOptions;
29
- /**
30
- * Converts y into y1/y2 ranges based on the provided interval. Disables the
31
- * implicit stacking
32
- */
33
- interval?: number | string;
34
- borderRadius?:
35
- | number
36
- | {
37
- topLeft?: number;
38
- topRight?: number;
39
- bottomRight?: number;
40
- bottomLeft?: number;
41
- };
42
- };
14
+ type BarYProps = BaseMarkProps &
15
+ BaseRectMarkProps & {
16
+ data: DataRow[];
17
+ x?: ChannelAccessor;
18
+ y?: ChannelAccessor;
19
+ y1?: ChannelAccessor;
20
+ y2?: ChannelAccessor;
21
+ stack?: StackOptions;
22
+ /**
23
+ * Converts y into y1/y2 ranges based on the provided interval. Disables the
24
+ * implicit stacking
25
+ */
26
+ interval?: number | string;
27
+ };
43
28
 
44
29
  let { data = [{}], class: className = null, stack, ...options }: BarYProps = $props();
45
30
 
@@ -63,39 +48,23 @@
63
48
  requiredScales={{ x: ['band'] }}
64
49
  channels={['x', 'y1', 'y2', 'fill', 'stroke', 'opacity', 'fillOpacity', 'strokeOpacity']}
65
50
  {...args}>
66
- {#snippet children({ mark, scaledData, usedScales })}
51
+ {#snippet children({ scaledData, usedScales })}
67
52
  <GroupMultiple class="bar-y" length={scaledData.length}>
68
53
  {#each scaledData as d}
69
54
  {@const bw = plot.scales.x.fn.bandwidth()}
70
55
  {@const miny = Math.min(d.y1, d.y2)}
71
56
  {@const maxy = Math.max(d.y1, d.y2)}
72
- {@const insetLeft = resolveProp(args.insetLeft || args.inset, d.datum, 0)}
73
- {@const insetRight = resolveProp(args.insetRight || args.inset, d.datum, 0)}
74
- {@const insetTop = resolveProp(args.insetTop, d.datum, 0)}
75
- {@const insetBottom = resolveProp(args.insetBottom, d.datum, 0)}
76
- {@const dx = resolveProp(args.dx, d.datum, 0)}
77
- {@const dy = resolveProp(args.dy, d.datum, 0)}
78
57
  {#if d.valid}
79
- {@const [style, styleClass] = resolveStyles(plot, d, args, 'fill', usedScales)}
80
- <path
81
- d={roundedRect(
82
- 0,
83
- 0,
84
- bw - insetLeft - insetRight,
85
- maxy - miny - insetTop - insetBottom,
86
- options.borderRadius
87
- )}
88
- class={[styleClass, className]}
89
- {style}
90
- transform="translate({[
91
- d.x + insetLeft + dx - bw * 0.5,
92
- miny + dy + insetTop
93
- ]})"
94
- use:addEventHandlers={{
95
- getPlotState,
96
- options: args,
97
- datum: d.datum
98
- }} />
58
+ <RectPath
59
+ x={d.x - bw * 0.5}
60
+ y={miny}
61
+ options={args}
62
+ class={className}
63
+ width={bw}
64
+ height={maxy - miny}
65
+ datum={d}
66
+ {usedScales}
67
+ useInsetAsFallbackVertically={false} />
99
68
  {/if}
100
69
  {/each}
101
70
  </GroupMultiple>
@@ -1,6 +1,6 @@
1
- import { type BaseMarkProps, type ChannelAccessor, type DataRow } from '../types.js';
1
+ import type { BaseMarkProps, BaseRectMarkProps } from '../types.js';
2
2
  import type { StackOptions } from '../transforms/stack.js';
3
- type BarYProps = BaseMarkProps & {
3
+ type BarYProps = BaseMarkProps & BaseRectMarkProps & {
4
4
  data: DataRow[];
5
5
  x?: ChannelAccessor;
6
6
  y?: ChannelAccessor;
@@ -12,12 +12,6 @@ type BarYProps = BaseMarkProps & {
12
12
  * implicit stacking
13
13
  */
14
14
  interval?: number | string;
15
- borderRadius?: number | {
16
- topLeft?: number;
17
- topRight?: number;
18
- bottomRight?: number;
19
- bottomLeft?: number;
20
- };
21
15
  };
22
16
  /** For vertical column charts using a band scale as x axis */
23
17
  declare const BarY: import("svelte").Component<BarYProps, {}, "">;
@@ -6,9 +6,7 @@
6
6
  import Mark from '../Mark.svelte';
7
7
  import { getContext } from 'svelte';
8
8
  import { recordizeY, sort } from '../index.js';
9
- import { roundedRect } from '../helpers/roundedRect.js';
10
- import { resolveChannel, resolveProp, resolveStyles } from '../helpers/resolve.js';
11
- import { coalesce, maybeNumber } from '../helpers/index.js';
9
+ import { resolveChannel } from '../helpers/resolve.js';
12
10
  import type {
13
11
  PlotContext,
14
12
  DataRecord,
@@ -17,7 +15,7 @@
17
15
  ChannelAccessor
18
16
  } from '../types.js';
19
17
  import { isValid } from '../helpers/isValid.js';
20
- import { addEventHandlers } from './helpers/events.js';
18
+ import RectPath from './helpers/RectPath.svelte';
21
19
 
22
20
  type CellProps = BaseMarkProps & {
23
21
  data: DataRecord[];
@@ -59,43 +57,21 @@
59
57
  requiredScales={{ x: ['band'], y: ['band'] }}
60
58
  channels={['x', 'y', 'fill', 'stroke', 'opacity', 'fillOpacity', 'strokeOpacity']}
61
59
  {...args}>
62
- {#snippet children({ mark, scaledData, usedScales })}
60
+ {#snippet children({ scaledData, usedScales })}
63
61
  {@const bwx = plot.scales.x.fn.bandwidth()}
64
62
  {@const bwy = plot.scales.y.fn.bandwidth()}
65
63
  <g class="cell {className || ''}" data-fill={usedScales.fillOpacity}>
66
64
  {#each scaledData as d}
67
- {@const inset = resolveProp(args.inset, d.datum, 0)}
68
- {@const insetLeft = resolveProp(args.insetLeft, d.datum)}
69
- {@const insetRight = resolveProp(args.insetRight, d.datum)}
70
- {@const insetTop = resolveProp(args.insetTop, d.datum)}
71
- {@const insetBottom = resolveProp(args.insetBottom, d.datum)}
72
- {@const dx = resolveProp(args.dx, d.datum, 0)}
73
- {@const dy = resolveProp(args.dy, d.datum, 0)}
74
- {@const insetL = maybeNumber(coalesce(insetLeft, inset, 0))}
75
- {@const insetT = maybeNumber(coalesce(insetTop, inset, 0))}
76
- {@const insetR = maybeNumber(coalesce(insetRight, inset, 0))}
77
- {@const insetB = maybeNumber(coalesce(insetBottom, inset, 0))}
78
65
  {#if d.valid && (args.fill == null || isValid(resolveChannel('fill', d.datum, args)))}
79
- {@const [style, styleClass] = resolveStyles(plot, d, args, 'fill', usedScales)}
80
- <path
81
- d={roundedRect(
82
- 0,
83
- 0,
84
- bwx - insetL - insetR,
85
- bwy - insetT - insetB,
86
- options.borderRadius
87
- )}
88
- class={[styleClass]}
89
- {style}
90
- transform="translate({[
91
- d.x + insetL + dx - bwx * 0.5,
92
- d.y + insetT + dy - bwy * 0.5
93
- ]})"
94
- use:addEventHandlers={{
95
- getPlotState,
96
- options: args,
97
- datum: d.datum
98
- }} />
66
+ <RectPath
67
+ datum={d}
68
+ class={className}
69
+ {usedScales}
70
+ options={args}
71
+ x={d.x - bwx * 0.5}
72
+ y={d.y - bwy * 0.5}
73
+ width={bwx}
74
+ height={bwy} />
99
75
  {/if}
100
76
  {/each}
101
77
  </g>
@@ -18,8 +18,9 @@
18
18
  data: DataRecord[];
19
19
  z?: ChannelAccessor;
20
20
  stroke?: ChannelAccessor;
21
- outlineStroke?: ConstantAccessor<string>;
22
- outlineStrokeWidth?: ConstantAccessor<number>;
21
+ outlineStroke?: string;
22
+ outlineStrokeWidth?: number;
23
+ outlineStrokeOpacity?: number;
23
24
  dx?: ConstantAccessor<number>;
24
25
  dy?: ConstantAccessor<number>;
25
26
  curve?: CurveName | CurveFactory;
@@ -31,6 +32,7 @@
31
32
  textStartOffset?: ConstantAccessor<string>;
32
33
  textStrokeWidth?: ConstantAccessor<number>;
33
34
  lineClass?: ConstantAccessor<string>;
35
+ canvas?: boolean;
34
36
  } & MarkerOptions;
35
37
  </script>
36
38
 
@@ -39,11 +41,12 @@
39
41
  import MarkerPath from './helpers/MarkerPath.svelte';
40
42
  import { getContext } from 'svelte';
41
43
  import { resolveProp, resolveStyles } from '../helpers/resolve.js';
42
- import { line, type CurveFactory } from 'd3-shape';
44
+ import { line, type CurveFactory, type Line } from 'd3-shape';
43
45
  import { geoPath } from 'd3-geo';
44
46
  import callWithProps from '../helpers/callWithProps.js';
45
47
  import { maybeCurve } from '../helpers/curves.js';
46
48
  import { pick } from 'es-toolkit';
49
+ import LineCanvas from './helpers/LineCanvas.svelte';
47
50
 
48
51
  type LineMarkProps = BaseMarkProps & {
49
52
  x?: ChannelAccessor;
@@ -61,6 +64,7 @@
61
64
  curve = 'auto',
62
65
  tension = 0,
63
66
  text,
67
+ canvas = false,
64
68
  class: className = null,
65
69
  lineClass = null,
66
70
  ...options
@@ -96,9 +100,7 @@
96
100
  const { getPlotState } = getContext<PlotContext>('svelteplot');
97
101
  const plot = $derived(getPlotState());
98
102
 
99
- type LinePath = (dr: DataRecord[]) => string;
100
-
101
- const linePath: LinePath = $derived(
103
+ const linePath: Line<ScaledDataRecord> = $derived(
102
104
  plot.scales.projection && curve === 'auto'
103
105
  ? sphereLine(plot.scales.projection)
104
106
  : callWithProps(line, [], {
@@ -111,7 +113,7 @@
111
113
 
112
114
  function sphereLine(projection) {
113
115
  const path = geoPath(projection);
114
- return (lineData: ScaledDataRecord[]) => {
116
+ const fn = (lineData: ScaledDataRecord[]) => {
115
117
  let line = [];
116
118
  const lines = [line];
117
119
  for (const { x, y } of lineData) {
@@ -125,6 +127,8 @@
125
127
  }
126
128
  return path({ type: 'MultiLineString', coordinates: lines });
127
129
  };
130
+ fn.context = path.context;
131
+ return fn;
128
132
  }
129
133
  </script>
130
134
 
@@ -135,88 +139,102 @@
135
139
  {...args}>
136
140
  {#snippet children({ mark, usedScales, scaledData })}
137
141
  {#if scaledData.length > 0}
138
- <g class={['lines', className]}>
139
- {#each groupIndex(scaledData, groupByKey) as lineData, i}
140
- {@const pathString = linePath(lineData)}
141
- {#if pathString}
142
- <GroupMultiple class={resolveProp(lineClass, lineData[0])}>
143
- {#if options.outlineStroke}
144
- {@const [outlineStyle, outlineStyleClass] = resolveStyles(
142
+ {@const groupedLineData = groupIndex(scaledData, groupByKey)};
143
+ {#if canvas}
144
+ <LineCanvas {groupedLineData} {mark} {usedScales} {linePath} {groupByKey} />
145
+ {:else}
146
+ <g class={['lines', className]}>
147
+ {#each groupedLineData as lineData, i}
148
+ {@const pathString = linePath(lineData)}
149
+ {#if pathString}
150
+ <GroupMultiple class={resolveProp(lineClass, lineData[0])}>
151
+ {#if options.outlineStroke}
152
+ {@const [outlineStyle, outlineStyleClass] = resolveStyles(
153
+ plot,
154
+ { ...lineData[0], stroke: options.outlineStroke },
155
+ {
156
+ strokeLinejoin: 'round',
157
+ ...args,
158
+ stroke: options.outlineStroke,
159
+ strokeOpacity: options.outlineStrokeOpacity ?? 1,
160
+ strokeWidth:
161
+ options.outlineStrokeWidth ||
162
+ resolveProp(
163
+ options.strokeWidth,
164
+ lineData[0].datum,
165
+ 1.4
166
+ ) + 2
167
+ },
168
+ 'stroke',
169
+ usedScales
170
+ )}
171
+ <path
172
+ d={pathString}
173
+ style={outlineStyle}
174
+ class={['is-outline', outlineStyleClass]} />
175
+ {/if}
176
+ {@const [style, styleClass] = resolveStyles(
145
177
  plot,
146
- { ...lineData[0], stroke: options.outlineStroke },
178
+ lineData[0],
147
179
  {
180
+ strokeWidth: 1.4,
148
181
  strokeLinejoin: 'round',
149
182
  ...args,
150
- stroke: options.outlineStroke,
151
- strokeWidth:
152
- options.outlineStrokeWidth ||
153
- (+options.strokeWidth || 1.4) + 2
183
+ stroke: lineData[0].stroke
154
184
  },
155
185
  'stroke',
156
186
  usedScales
157
187
  )}
158
- <path
188
+ {@const [textStyle, textStyleClass] = resolveStyles(
189
+ plot,
190
+ lineData[0],
191
+ {
192
+ textAnchor: 'middle',
193
+ ...pick(args, [
194
+ 'fontSize',
195
+ 'fontWeight',
196
+ 'fontStyle',
197
+ 'textAnchor'
198
+ ]),
199
+ strokeWidth: args.textStrokeWidth
200
+ ? args.textStrokeWidth
201
+ : args.textStroke
202
+ ? 2
203
+ : 0,
204
+ fill: args.textFill || args.stroke,
205
+ stroke: args.textStroke
206
+ },
207
+ 'fill',
208
+ usedScales,
209
+ true
210
+ )}
211
+ <MarkerPath
212
+ {mark}
213
+ scales={plot.scales}
214
+ markerStart={args.markerStart}
215
+ markerMid={args.markerMid}
216
+ markerEnd={args.markerEnd}
217
+ marker={args.marker}
218
+ strokeWidth={args.strokeWidth}
219
+ datum={lineData[0]}
159
220
  d={pathString}
160
- style={outlineStyle}
161
- class={['is-outline', outlineStyleClass]} />
162
- {/if}
163
- {@const [style, styleClass] = resolveStyles(
164
- plot,
165
- lineData[0],
166
- {
167
- strokeWidth: 1.4,
168
- strokeLinejoin: 'round',
169
- ...args,
170
- stroke: lineData[0].stroke
171
- },
172
- 'stroke',
173
- usedScales
174
- )}
175
- {@const [textStyle, textStyleClass] = resolveStyles(
176
- plot,
177
- lineData[0],
178
- {
179
- textAnchor: 'middle',
180
- ...pick(args, [
181
- 'fontSize',
182
- 'fontWeight',
183
- 'fontStyle',
184
- 'textAnchor'
185
- ]),
186
- strokeWidth: args.textStrokeWidth
187
- ? args.textStrokeWidth
188
- : args.textStroke
189
- ? 2
190
- : 0,
191
- fill: args.textFill || args.stroke,
192
- stroke: args.textStroke
193
- },
194
- 'fill',
195
- usedScales,
196
- true
197
- )}
198
- <MarkerPath
199
- {mark}
200
- scales={plot.scales}
201
- markerStart={args.markerStart}
202
- markerMid={args.markerMid}
203
- markerEnd={args.markerEnd}
204
- marker={args.marker}
205
- strokeWidth={args.strokeWidth}
206
- datum={lineData[0]}
207
- d={pathString}
208
- dInv={text ? linePath(lineData.toReversed()) : null}
209
- color={lineData[0].stroke || 'currentColor'}
210
- {style}
211
- class={styleClass}
212
- text={text ? resolveProp(text, lineData[0]) : null}
213
- startOffset={resolveProp(args.textStartOffset, lineData[0], '50%')}
214
- {textStyle}
215
- {textStyleClass} />
216
- </GroupMultiple>
217
- {/if}
218
- {/each}
219
- </g>
221
+ dInv={text ? linePath(lineData.toReversed()) : null}
222
+ color={lineData[0].stroke || 'currentColor'}
223
+ {style}
224
+ class={styleClass}
225
+ text={text ? resolveProp(text, lineData[0]) : null}
226
+ startOffset={resolveProp(
227
+ args.textStartOffset,
228
+ lineData[0],
229
+ '50%'
230
+ )}
231
+ {textStyle}
232
+ {textStyleClass} />
233
+ </GroupMultiple>
234
+ {/if}
235
+ {/each}
236
+ </g>
237
+ {/if}
220
238
  {/if}
221
239
  {/snippet}
222
240
  </Mark>
@@ -3,8 +3,9 @@ export type BaseLineMarkProps = {
3
3
  data: DataRecord[];
4
4
  z?: ChannelAccessor;
5
5
  stroke?: ChannelAccessor;
6
- outlineStroke?: ConstantAccessor<string>;
7
- outlineStrokeWidth?: ConstantAccessor<number>;
6
+ outlineStroke?: string;
7
+ outlineStrokeWidth?: number;
8
+ outlineStrokeOpacity?: number;
8
9
  dx?: ConstantAccessor<number>;
9
10
  dy?: ConstantAccessor<number>;
10
11
  curve?: CurveName | CurveFactory;
@@ -18,8 +19,9 @@ export type BaseLineMarkProps = {
18
19
  textStartOffset?: ConstantAccessor<string>;
19
20
  textStrokeWidth?: ConstantAccessor<number>;
20
21
  lineClass?: ConstantAccessor<string>;
22
+ canvas?: boolean;
21
23
  } & MarkerOptions;
22
- import { type CurveFactory } from 'd3-shape';
24
+ import { type CurveFactory, type Line } from 'd3-shape';
23
25
  import type { RawValue } from '../types.js';
24
26
  type LineMarkProps = BaseMarkProps & {
25
27
  x?: ChannelAccessor;
@@ -17,6 +17,7 @@
17
17
  } from '../types.js';
18
18
  import { addEventHandlers } from './helpers/events.js';
19
19
  import GroupMultiple from './helpers/GroupMultiple.svelte';
20
+ import RectPath from './helpers/RectPath.svelte';
20
21
 
21
22
  type RectMarkProps = BaseMarkProps & {
22
23
  data: DataRecord[];
@@ -57,31 +58,16 @@
57
58
  {@const maxy = Math.max(y1, y2)}
58
59
  {@const minx = Math.min(x1, x2)}
59
60
  {@const maxx = Math.max(x1, x2)}
60
- {@const inset = resolveProp(args.inset, d.datum, 0)}
61
- {@const insetLeft = resolveProp(args.insetLeft, d.datum)}
62
- {@const insetRight = resolveProp(args.insetRight, d.datum)}
63
- {@const insetTop = resolveProp(args.insetTop, d.datum)}
64
- {@const insetBottom = resolveProp(args.insetBottom, d.datum)}
65
- {@const insetL = maybeNumber(coalesce(insetLeft, inset, 0)) ?? 0}
66
- {@const insetT = maybeNumber(coalesce(insetTop, inset, 0)) ?? 0}
67
- {@const insetR = maybeNumber(coalesce(insetRight, inset, 0)) ?? 0}
68
- {@const insetB = maybeNumber(coalesce(insetBottom, inset, 0)) ?? 0}
69
61
 
70
- {@const [style, styleClass] = resolveStyles(plot, d, args, 'fill', usedScales)}
71
- <rect
72
- class={[scaledData.length === 1 && className, styleClass]}
73
- {style}
74
- x={minx + insetL}
75
- y={miny + insetT}
76
- width={maxx - minx - insetL - insetR}
77
- height={maxy - miny - insetT - insetB}
78
- rx={resolveProp(args.rx, d.datum, null)}
79
- ry={resolveProp(args.ry, d.datum, null)}
80
- use:addEventHandlers={{
81
- getPlotState,
82
- options: args,
83
- datum: d.datum
84
- }} />
62
+ <RectPath
63
+ datum={d}
64
+ class={scaledData.length === 1 ? className : null}
65
+ x={minx}
66
+ y={miny}
67
+ width={maxx - minx}
68
+ height={maxy - miny}
69
+ options={args}
70
+ {usedScales} />
85
71
  {/if}
86
72
  {/each}
87
73
  </GroupMultiple>
@@ -14,6 +14,7 @@
14
14
  import CanvasLayer from './CanvasLayer.svelte';
15
15
  import { getContext } from 'svelte';
16
16
  import { devicePixelRatio } from 'svelte/reactivity/window';
17
+ import { resolveColor } from './canvas';
17
18
 
18
19
  const { getPlotState } = getContext<PlotContext>('svelteplot');
19
20
  const plot = $derived(getPlotState());
@@ -46,19 +47,8 @@
46
47
  if (datum.valid) {
47
48
  let { fill, stroke } = datum;
48
49
 
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));
50
+ fill = resolveColor(fill, canvas);
51
+ stroke = resolveColor(stroke, canvas);
62
52
 
63
53
  if (stroke && stroke !== 'none') {
64
54
  const strokeWidth = resolveProp(
@@ -0,0 +1,116 @@
1
+ <script lang="ts">
2
+ import type {
3
+ Mark,
4
+ BaseMarkProps,
5
+ PlotContext,
6
+ ScaledDataRecord,
7
+ UsedScales
8
+ } from '../../types.js';
9
+ import { resolveProp, resolveScaledStyleProps } from '../../helpers/resolve.js';
10
+ import { getContext } from 'svelte';
11
+ import { type Line } from 'd3-shape';
12
+ import CanvasLayer from './CanvasLayer.svelte';
13
+ import type { Attachment } from 'svelte/attachments';
14
+ import { devicePixelRatio } from 'svelte/reactivity/window';
15
+ import { resolveColor } from './canvas';
16
+
17
+ let {
18
+ mark,
19
+ groupedLineData,
20
+ usedScales,
21
+ linePath
22
+ }: {
23
+ mark: Mark<BaseMarkProps>;
24
+ groupedLineData: ScaledDataRecord[][];
25
+ usedScales: UsedScales;
26
+ linePath: Line<ScaledDataRecord>;
27
+ groupByKey?: unknown;
28
+ } = $props();
29
+
30
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
31
+ const plot = $derived(getPlotState());
32
+
33
+ function maybeOpacity(value: unknown) {
34
+ return value == null ? 1 : +value;
35
+ }
36
+
37
+ const render = ((canvas: HTMLCanvasElement) => {
38
+ const context = canvas.getContext('2d');
39
+
40
+ $effect(() => {
41
+ if (context) {
42
+ linePath.context(context);
43
+ context.resetTransform();
44
+ context.scale(devicePixelRatio.current ?? 1, devicePixelRatio.current ?? 1);
45
+ context.lineJoin = 'round';
46
+ context.lineCap = 'round';
47
+
48
+ for (const group of groupedLineData) {
49
+ if (group.length < 2) continue;
50
+
51
+ // Get the first point to determine line styles
52
+ const firstPoint = group[0];
53
+ if (!firstPoint || !firstPoint.valid) continue;
54
+
55
+ let { stroke, ...restStyles } = resolveScaledStyleProps(
56
+ firstPoint.datum,
57
+ mark.options,
58
+ usedScales,
59
+ plot,
60
+ 'stroke'
61
+ );
62
+
63
+ const opacity = maybeOpacity(restStyles['opacity']);
64
+ const strokeOpacity = maybeOpacity(restStyles['stroke-opacity']);
65
+
66
+ const strokeWidth = resolveProp(
67
+ mark.options.strokeWidth,
68
+ firstPoint.datum,
69
+ 1.4
70
+ ) as number;
71
+
72
+ if (mark.options.outlineStroke) {
73
+ // draw stroke outline first
74
+ const outlineStroke = resolveColor(mark.options.outlineStroke, canvas);
75
+ const outlineStrokeWidth =
76
+ mark.options.outlineStrokeWidth ?? strokeWidth + 2;
77
+ const outlineStrokeOpacity = mark.options.outlineStrokeOpacity ?? 1;
78
+
79
+ context.lineWidth = outlineStrokeWidth;
80
+ context.strokeStyle = outlineStroke;
81
+ context.globalAlpha = opacity * outlineStrokeOpacity;
82
+ context.beginPath();
83
+ linePath(group);
84
+ context.stroke();
85
+ }
86
+
87
+ stroke = resolveColor(stroke, canvas);
88
+
89
+ if (stroke && stroke !== 'none') {
90
+ context.lineWidth = strokeWidth ?? 1.4;
91
+ }
92
+
93
+ context.strokeStyle = stroke ? stroke : 'currentColor';
94
+ context.globalAlpha = opacity * strokeOpacity;
95
+
96
+ // Start drawing the line
97
+ context.beginPath();
98
+ linePath(group);
99
+ context.stroke();
100
+ }
101
+ linePath.context(null);
102
+ }
103
+
104
+ return () => {
105
+ context?.clearRect(
106
+ 0,
107
+ 0,
108
+ plot.width * (devicePixelRatio.current ?? 1),
109
+ plot.height * (devicePixelRatio.current ?? 1)
110
+ );
111
+ };
112
+ });
113
+ }) as Attachment;
114
+ </script>
115
+
116
+ <CanvasLayer {@attach render} />
@@ -0,0 +1,12 @@
1
+ import type { Mark, BaseMarkProps, ScaledDataRecord, UsedScales } from '../../types.js';
2
+ import { type Line } from 'd3-shape';
3
+ type $$ComponentProps = {
4
+ mark: Mark<BaseMarkProps>;
5
+ groupedLineData: ScaledDataRecord[][];
6
+ usedScales: UsedScales;
7
+ linePath: Line<ScaledDataRecord>;
8
+ groupByKey?: unknown;
9
+ };
10
+ declare const LineCanvas: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type LineCanvas = ReturnType<typeof LineCanvas>;
12
+ export default LineCanvas;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte';
3
+ import type { PlotContext, RawValue } from '../../types';
4
+
5
+ let {
6
+ id,
7
+ stops
8
+ }: {
9
+ id: string;
10
+ stops: { x: RawValue; color: string }[];
11
+ } = $props();
12
+
13
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
14
+ const plot = $derived(getPlotState());
15
+
16
+ const projectedStops = $derived(
17
+ stops
18
+ .map((d) => ({ ...d, px: plot.scales.x.fn(d.x) / plot.width }))
19
+ .sort((a, b) => a.px - b.px)
20
+ );
21
+ </script>
22
+
23
+ <linearGradient {id} gradientUnits="userSpaceOnUse" x1={0} y2={0} y1={0} x2={plot.width}>
24
+ {#each projectedStops as { px, color }}
25
+ <stop stop-color={color} offset={px} />
26
+ {/each}
27
+ </linearGradient>
@@ -0,0 +1,11 @@
1
+ import type { RawValue } from '../../types';
2
+ type $$ComponentProps = {
3
+ id: string;
4
+ stops: {
5
+ x: RawValue;
6
+ color: string;
7
+ }[];
8
+ };
9
+ declare const LinearGradientX: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type LinearGradientX = ReturnType<typeof LinearGradientX>;
11
+ export default LinearGradientX;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte';
3
+ import type { PlotContext, RawValue } from '../../types';
4
+
5
+ let {
6
+ id,
7
+ stops
8
+ }: {
9
+ id: string;
10
+ stops: { y: RawValue; color: string }[];
11
+ } = $props();
12
+
13
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
14
+ const plot = $derived(getPlotState());
15
+
16
+ const projectedStops = $derived(
17
+ stops
18
+ .map((d) => ({ ...d, py: plot.scales.y.fn(d.y) / plot.height }))
19
+ .sort((a, b) => a.py - b.py)
20
+ );
21
+ </script>
22
+
23
+ <linearGradient {id} gradientUnits="userSpaceOnUse" x1={0} x2={0} y1={0} y2={plot.height}>
24
+ {#each projectedStops as { py, color }}
25
+ <stop stop-color={color} offset={py} />
26
+ {/each}
27
+ </linearGradient>
@@ -0,0 +1,11 @@
1
+ import type { RawValue } from '../../types';
2
+ type $$ComponentProps = {
3
+ id: string;
4
+ stops: {
5
+ y: RawValue;
6
+ color: string;
7
+ }[];
8
+ };
9
+ declare const LinearGradientY: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type LinearGradientY = ReturnType<typeof LinearGradientY>;
11
+ export default LinearGradientY;
@@ -0,0 +1,129 @@
1
+ <!-- @component
2
+ Helper component for rendering rectangular marks in SVG
3
+ -->
4
+ <script lang="ts">
5
+ import { resolveProp, resolveStyles } from '../../helpers/resolve';
6
+ import { roundedRect } from '../../helpers/roundedRect';
7
+ import type {
8
+ BaseMarkProps,
9
+ BaseRectMarkProps,
10
+ BorderRadius,
11
+ ScaledDataRecord,
12
+ UsedScales,
13
+ PlotContext
14
+ } from '../../types';
15
+ import { addEventHandlers } from './events';
16
+ import { getContext } from 'svelte';
17
+
18
+ let {
19
+ datum,
20
+ options,
21
+ class: className = null,
22
+ x,
23
+ y,
24
+ width,
25
+ height,
26
+ useInsetAsFallbackVertically = true,
27
+ useInsetAsFallbackHorizontally = true,
28
+ usedScales
29
+ }: {
30
+ datum: ScaledDataRecord;
31
+ class: string | null;
32
+ x: number;
33
+ y: number;
34
+ width: number;
35
+ height: number;
36
+ options: BaseRectMarkProps & BaseMarkProps;
37
+ /**
38
+ * By default, the `inset` property is applied to all four insets. Mark components
39
+ * can tweak this behavior for insetTop and insetBottom by setting the
40
+ * useInsetAsFallbackVertically prop to false.
41
+ */
42
+ useInsetAsFallbackVertically?: boolean;
43
+ /**
44
+ * By default, the `inset` property is applied to all four insets. Mark components
45
+ * can tweak this behavior for insetLeft and insetRight by setting the
46
+ * useInsetAsFallbackHorizontally prop to false.
47
+ */
48
+ useInsetAsFallbackHorizontally?: boolean;
49
+ usedScales: UsedScales;
50
+ } = $props();
51
+
52
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
53
+ const plot = $derived(getPlotState());
54
+
55
+ const dx = $derived(+(resolveProp(options.dx, datum.datum, 0) as number));
56
+ const dy = $derived(+(resolveProp(options.dy, datum.datum, 0) as number));
57
+ const inset = $derived(+(resolveProp(options.inset, datum.datum, 0) as number));
58
+ const insetLeft = $derived(
59
+ +(resolveProp(
60
+ options.insetLeft,
61
+ datum.datum,
62
+ useInsetAsFallbackHorizontally ? inset : 0
63
+ ) as number)
64
+ );
65
+ const insetRight = $derived(
66
+ +(resolveProp(
67
+ options.insetRight,
68
+ datum.datum,
69
+ useInsetAsFallbackHorizontally ? inset : 0
70
+ ) as number)
71
+ );
72
+ const insetTop = $derived(
73
+ +(resolveProp(
74
+ options.insetTop,
75
+ datum.datum,
76
+ useInsetAsFallbackVertically ? inset : 0
77
+ ) as number)
78
+ );
79
+ const insetBottom = $derived(
80
+ +(resolveProp(
81
+ options.insetBottom,
82
+ datum.datum,
83
+ useInsetAsFallbackVertically ? inset : 0
84
+ ) as number)
85
+ );
86
+ const borderRadius = $derived((options.borderRadius ?? 0) as BorderRadius);
87
+ const hasBorderRadius = $derived(
88
+ (typeof borderRadius === 'number' && borderRadius > 0) ||
89
+ (typeof borderRadius === 'object' &&
90
+ Math.max(
91
+ borderRadius.topRight ?? 0,
92
+ borderRadius.bottomRight ?? 0,
93
+ borderRadius.topLeft ?? 0,
94
+ borderRadius.bottomLeft ?? 0
95
+ ) > 0)
96
+ );
97
+ const [style, styleClass] = $derived(resolveStyles(plot, datum, options, 'fill', usedScales));
98
+ </script>
99
+
100
+ {#if hasBorderRadius}
101
+ <path
102
+ transform="translate({[x + dx + insetLeft, y + insetBottom + dy]})"
103
+ d={roundedRect(
104
+ 0,
105
+ 0,
106
+ width - insetLeft - insetRight,
107
+ height - insetTop - insetBottom,
108
+ borderRadius
109
+ )}
110
+ class={[styleClass, className]}
111
+ {style}
112
+ use:addEventHandlers={{
113
+ getPlotState,
114
+ options,
115
+ datum: datum.datum
116
+ }} />
117
+ {:else}
118
+ <rect
119
+ transform="translate({[x + dx + insetLeft, y + insetBottom + dy]})"
120
+ width={width - insetLeft - insetRight}
121
+ height={height - insetTop - insetBottom}
122
+ class={[styleClass, className]}
123
+ {style}
124
+ use:addEventHandlers={{
125
+ getPlotState,
126
+ options,
127
+ datum: datum.datum
128
+ }} />
129
+ {/if}
@@ -0,0 +1,27 @@
1
+ import type { BaseMarkProps, BaseRectMarkProps, ScaledDataRecord, UsedScales } from '../../types';
2
+ type $$ComponentProps = {
3
+ datum: ScaledDataRecord;
4
+ class: string | null;
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ options: BaseRectMarkProps & BaseMarkProps;
10
+ /**
11
+ * By default, the `inset` property is applied to all four insets. Mark components
12
+ * can tweak this behavior for insetTop and insetBottom by setting the
13
+ * useInsetAsFallbackVertically prop to false.
14
+ */
15
+ useInsetAsFallbackVertically?: boolean;
16
+ /**
17
+ * By default, the `inset` property is applied to all four insets. Mark components
18
+ * can tweak this behavior for insetLeft and insetRight by setting the
19
+ * useInsetAsFallbackHorizontally prop to false.
20
+ */
21
+ useInsetAsFallbackHorizontally?: boolean;
22
+ usedScales: UsedScales;
23
+ };
24
+ /** Helper component for rendering rectangular marks in SVG */
25
+ declare const RectPath: import("svelte").Component<$$ComponentProps, {}, "">;
26
+ type RectPath = ReturnType<typeof RectPath>;
27
+ export default RectPath;
@@ -0,0 +1 @@
1
+ export declare function resolveColor(color: string, canvas: HTMLCanvasElement): string | CanvasGradient;
@@ -0,0 +1,34 @@
1
+ import { CSS_URL, CSS_VAR } from "../../constants";
2
+ export function resolveColor(color, canvas) {
3
+ if (`${color}`.toLowerCase() === 'currentcolor') {
4
+ color = getComputedStyle(canvas?.parentElement?.parentElement).getPropertyValue('color');
5
+ }
6
+ if (CSS_VAR.test(color)) {
7
+ color = getComputedStyle(canvas).getPropertyValue(color.slice(4, -1));
8
+ }
9
+ if (CSS_URL.test(color)) {
10
+ // might be a gradient we can parse!
11
+ const m = color.match(/^url\((#[^\)]+)\)/);
12
+ const gradientId = m[1];
13
+ const gradient = canvas.ownerDocument.querySelector(gradientId);
14
+ if (gradient) {
15
+ // parse gradient
16
+ if (gradient.nodeName.toLowerCase() === 'lineargradient') {
17
+ const x0 = +gradient.getAttribute('x1');
18
+ const x1 = +gradient.getAttribute('x2');
19
+ const y0 = +gradient.getAttribute('y1');
20
+ const y1 = +gradient.getAttribute('y2');
21
+ const ctxGradient = canvas
22
+ .getContext('2d')
23
+ .createLinearGradient(x0, y0, x1, y1);
24
+ for (const stop of gradient.querySelectorAll('stop')) {
25
+ const offset = +stop.getAttribute('offset');
26
+ const color = resolveColor(stop.getAttribute('stop-color'), canvas);
27
+ ctxGradient.addColorStop(offset, color);
28
+ }
29
+ return ctxGradient;
30
+ }
31
+ }
32
+ }
33
+ return color;
34
+ }
package/dist/types.d.ts CHANGED
@@ -555,14 +555,19 @@ export type BaseMarkProps = Partial<{
555
555
  class: string;
556
556
  cursor: ConstantAccessor<CSS.Property.Cursor>;
557
557
  }>;
558
+ export type BorderRadius = number | {
559
+ topLeft?: number;
560
+ topRight?: number;
561
+ bottomRight?: number;
562
+ bottomLeft?: number;
563
+ };
558
564
  export type BaseRectMarkProps = {
559
- rx?: ConstantAccessor<number>;
560
- ry?: ConstantAccessor<number>;
561
565
  inset?: ConstantAccessor<number>;
562
566
  insetLeft?: ConstantAccessor<number>;
563
567
  insetTop?: ConstantAccessor<number>;
564
568
  insetRight?: ConstantAccessor<number>;
565
569
  insetBottom?: ConstantAccessor<number>;
570
+ borderRadius?: BorderRadius;
566
571
  };
567
572
  export type Channels = Record<string, ChannelAccessor | ConstantAccessor<string | number | boolean | symbol>>;
568
573
  export type TransformArg<K> = Channels & {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteplot",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "license": "ISC",
5
5
  "author": {
6
6
  "name": "Gregor Aisch",
@@ -87,6 +87,7 @@
87
87
  "svelte-check": "^4.2.1",
88
88
  "svelte-eslint-parser": "1.2.0",
89
89
  "svelte-highlight": "^7.8.3",
90
+ "svg-path-parser": "^1.1.0",
90
91
  "topojson-client": "^3.1.0",
91
92
  "tslib": "^2.8.1",
92
93
  "typedoc": "^0.28.4",