svelteplot 0.4.3 → 0.4.4

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/README.md CHANGED
@@ -5,3 +5,44 @@
5
5
  SveltePlot is a visualization framework based on the [layered grammar of graphics](https://vita.had.co.nz/papers/layered-grammar.html) ideas. It's API is heavily inspired by [Observable Plot](https://github.com/observablehq/plot). Created by Gregor Aisch.
6
6
 
7
7
  <img src="static/logo.png" alt="SveltePlot logo" width="401" />
8
+
9
+ ## Development
10
+
11
+ Clone the repo and install dependencies:
12
+
13
+ ```bash
14
+ git clone git@github.com:svelteplot/svelteplot.git
15
+ cd svelteplot
16
+ pnpm install
17
+ ```
18
+
19
+ Run the development server:
20
+
21
+ ```bash
22
+ pnpm dev
23
+ ```
24
+
25
+ Open http://localhost:5173 in your browser.
26
+
27
+ ## Testing
28
+
29
+ Run unit tests:
30
+
31
+ ```bash
32
+ pnpm lint
33
+ pnpm test
34
+ ```
35
+
36
+ You should also run the visual regression tests:
37
+
38
+ ```bash
39
+ pnpm test:visual
40
+ ```
41
+
42
+ This will generate screenshots and compare them with the expected results.
43
+
44
+ ```bash
45
+ pnpm vi:report
46
+ ```
47
+
48
+ To see the differences side by side you can open http://localhost:5173/\_\_vr/report.html
package/dist/Mark.svelte CHANGED
@@ -40,6 +40,7 @@
40
40
  import { getUsedScales, projectXY, projectX, projectY } from './helpers/scales.js';
41
41
  import { testFilter, isValid } from './helpers/index.js';
42
42
  import { resolveChannel, resolveProp } from './helpers/resolve.js';
43
+ import { RENAME } from './transforms/rename.js';
43
44
 
44
45
  let {
45
46
  data = [],
@@ -258,11 +259,13 @@
258
259
  if (options?.[channel] != null && out[channel] === undefined) {
259
260
  // resolve value
260
261
  const value = row[channel];
262
+ // if this channel was renamed, use the original channel for scaling
263
+ const origChannel = options?.[RENAME]?.[channel] || channel;
261
264
  const scaled = usedScales[channel]
262
265
  ? scale === 'x'
263
- ? projectX(channel as 'x' | 'x1' | 'x2', plot.scales, value)
266
+ ? projectX(origChannel as 'x' | 'x1' | 'x2', plot.scales, value)
264
267
  : scale === 'y'
265
- ? projectY(channel as 'y' | 'y1' | 'y1', plot.scales, value)
268
+ ? projectY(origChannel as 'y' | 'y1' | 'y2', plot.scales, value)
266
269
  : scale === 'color' && !isValid(value)
267
270
  ? plot.options.color.unknown
268
271
  : plot.scales[scale].fn(value)
@@ -236,7 +236,7 @@ export function inferScaleType(name, dataValues, markTypes) {
236
236
  if (name === 'symbol')
237
237
  return 'ordinal';
238
238
  // for positional scales, try to pick a scale that's required by the mark types
239
- if ((name === 'x' || name === 'y') && markTypes.size === 1) {
239
+ if (name === 'x' || name === 'y') {
240
240
  if (name === 'y' &&
241
241
  (markTypes.has('barX') || markTypes.has('tickX') || markTypes.has('cell')))
242
242
  return 'band';
@@ -253,7 +253,7 @@ export function inferScaleType(name, dataValues, markTypes) {
253
253
  if (dataValues.every(isDateOrNull))
254
254
  return 'time';
255
255
  if (dataValues.every(isStringOrNull))
256
- return markTypes.has('arrow') ? 'point' : 'band';
256
+ return 'point';
257
257
  return 'linear';
258
258
  }
259
259
  const scaledChannelNames = [
@@ -42,7 +42,6 @@
42
42
  const plot = $derived(getPlotState());
43
43
 
44
44
  const DEFAULTS = {
45
- fill: 'currentColor',
46
45
  ...getContext<PlotDefaults>('svelteplot/_defaults').bar,
47
46
  ...getContext<PlotDefaults>('svelteplot/_defaults').barY
48
47
  };
@@ -12,12 +12,14 @@
12
12
  import type BoxY from './BoxY.svelte';
13
13
 
14
14
  let markProps: BoxXMarkProps = $props();
15
+
15
16
  const DEFAULTS = {
16
17
  tickMedian: true,
17
18
  tickMinMax: false,
18
19
  ...getContext<PlotDefaults>('svelteplot/_defaults').box,
19
20
  ...getContext<PlotDefaults>('svelteplot/_defaults').boxX
20
21
  };
22
+
21
23
  const {
22
24
  data = [{}],
23
25
  bar,
@@ -27,80 +29,122 @@
27
29
  dot,
28
30
  x,
29
31
  y,
32
+ fx,
33
+ fy,
34
+ fill,
35
+ stroke,
30
36
  class: className = ''
31
37
  }: BoxXMarkProps = $derived({
32
38
  ...DEFAULTS,
33
39
  ...markProps
34
40
  });
35
41
 
36
- const { data: grouped } = $derived(
42
+ const { data: grouped, ...groupChannels } = $derived(
37
43
  groupY(
38
44
  {
39
45
  data: data.filter((d) => resolveChannel('x', d, { x, y }) != null),
40
46
  x,
41
47
  y,
42
48
  x1: x,
43
- x2: x
49
+ x2: x,
50
+ fx,
51
+ fy
44
52
  },
45
53
  { x: 'median', x1: 'p25', x2: 'p75', fill: (rows) => rows }
46
54
  )
47
55
  );
48
56
 
57
+ const X = Symbol('x'),
58
+ Y = Symbol('y'),
59
+ FX = Symbol('fx'),
60
+ FY = Symbol('fy'),
61
+ P25 = Symbol('p25'),
62
+ P75 = Symbol('p75'),
63
+ MEDIAN = Symbol('median'),
64
+ MIN = Symbol('min'),
65
+ MAX = Symbol('max'),
66
+ OUTLIERS = Symbol('outliers');
67
+
68
+ const facets = $derived({
69
+ ...(fx != null && { fx: FX }),
70
+ ...(fy != null && { fy: FY })
71
+ });
72
+
49
73
  const boxData = $derived(
50
74
  grouped
51
75
  .map((row) => {
52
- const iqr = row.__x2 - row.__x1;
76
+ const { x: median, x1: p25, x2: p75, fill: fill, y: ry } = groupChannels;
77
+
78
+ const iqr = row[p75] - row[p25];
53
79
  const whisker = iqr * 1.5;
54
- const lower = row.__x1 - whisker;
55
- const upper = row.__x2 + whisker;
56
- const data = row.__fill.map((d) => ({
80
+ const lower = row[p25] - whisker;
81
+ const upper = row[p75] + whisker;
82
+ const data = row[fill].map((d) => ({
57
83
  ...d,
58
- __x: resolveChannel('x', d, { x, y })
84
+ [X]: resolveChannel('x', d, { x, y })
59
85
  }));
60
- const outliers = data.filter((d) => d.__x < lower || d.__x > upper);
86
+ const outliers = data.filter((d) => d[X] < lower || d[X] > upper);
61
87
  const inside = data
62
- .filter((d) => d.__x >= lower && d.__x <= upper)
63
- .sort((a, b) => a.__x - b.__x);
88
+ .filter((d) => d[X] >= lower && d[X] <= upper)
89
+ .sort((a, b) => a[X] - b[X]);
90
+
64
91
  // if (inside.length === 0) console.log('No data inside boxplot', data, row, lower, upper);
65
92
  return {
66
- [y]: row[y],
67
- __y: row[y],
68
- p25: row.__x1,
69
- p75: row.__x2,
70
- median: row.__x,
71
- min: inside[0].__x,
72
- max: inside.at(-1).__x,
73
- outliers
93
+ ...data[0],
94
+ [Y]: row[ry],
95
+ [P25]: row[p25],
96
+ [MEDIAN]: row[median],
97
+ [P75]: row[p75],
98
+ [MIN]: inside[0][X],
99
+ [MAX]: inside.at(-1)[X],
100
+ [FX]: resolveChannel('fx', data[0], { fx }, null),
101
+ [FY]: resolveChannel('fy', data[0], { fy }, null),
102
+ [OUTLIERS]: outliers
74
103
  };
75
104
  })
76
- .sort((a, b) => b.median - a.median)
105
+ .sort((a, b) => b[MEDIAN] - a[MEDIAN])
77
106
  );
78
107
  </script>
79
108
 
80
109
  <GroupMultiple class="box-x {className || ''}" length={className ? 2 : grouped.length}>
81
- <RuleY data={boxData} y="__y" x1="min" x2="max" {...rule || {}} />
82
- <BarX data={boxData} y="__y" x1="p25" x2="p75" fill="#ddd" {...bar || {}} />
110
+ <RuleY data={boxData} y={Y} x1={MIN} x2={P25} {stroke} {...rule || {}} {...facets} />
111
+ <RuleY data={boxData} y={Y} x1={P75} x2={MAX} {stroke} {...rule || {}} {...facets} />
112
+ <BarX data={boxData} y={Y} x1={P25} x2={P75} {fill} {stroke} {...facets} {...bar || {}} />
83
113
  {#if tickMedian}
84
114
  <TickX
85
115
  data={boxData}
86
- y="__y"
87
- x="median"
116
+ y={Y}
117
+ x={MEDIAN}
118
+ {...facets}
119
+ {stroke}
88
120
  strokeWidth={2}
89
121
  {...typeof tickMedian === 'object' ? tickMedian : {}} />
90
122
  {/if}
91
123
  {#if tickMinMax}
92
124
  <TickX
93
125
  data={boxData}
94
- x="min"
95
- y="__y"
126
+ x={MIN}
127
+ y={Y}
128
+ {stroke}
129
+ {...facets}
96
130
  inset="20%"
97
131
  {...typeof tickMinMax === 'object' ? tickMinMax : {}} />
98
132
  <TickX
99
133
  data={boxData}
100
- x="max"
101
- y="__y"
134
+ x={MAX}
135
+ y={Y}
136
+ {stroke}
137
+ {...facets}
102
138
  inset="20%"
103
139
  {...typeof tickMinMax === 'object' ? tickMinMax : {}} />
104
140
  {/if}
105
- <Dot data={boxData.map((d) => d.outliers).flat()} {x} {y} {...dot || {}} />
141
+ <Dot
142
+ data={boxData.map((d) => d[OUTLIERS]).flat()}
143
+ {x}
144
+ {y}
145
+ {fx}
146
+ {fy}
147
+ {fill}
148
+ {stroke}
149
+ {...dot || {}} />
106
150
  </GroupMultiple>
@@ -2,7 +2,8 @@
2
2
  Creates a vertical box plot for visualizing data distribution with quartiles and outliers
3
3
  -->
4
4
  <script lang="ts" generics="Datum extends DataRecord">
5
- interface BoxYMarkProps extends Pick<BaseMarkProps<Datum>, 'class'> {
5
+ interface BoxYMarkProps
6
+ extends Pick<BaseMarkProps<Datum>, 'class' | 'fill' | 'stroke' | 'fx' | 'fy'> {
6
7
  data: Datum[];
7
8
  x: ChannelAccessor;
8
9
  y: ChannelAccessor;
@@ -28,95 +29,144 @@
28
29
  dot: Record<string, ChannelAccessor<Datum>>;
29
30
  }
30
31
  import GroupMultiple from './helpers/GroupMultiple.svelte';
31
- import type { BaseMarkProps, ChannelAccessor, DataRecord } from '../types/index.js';
32
32
  import { groupX, BarY, TickY, RuleX, Dot } from '../index.js';
33
33
  import { resolveChannel } from '../helpers/resolve.js';
34
- import { getContext } from 'svelte';
34
+ import { getContext, type ComponentProps } from 'svelte';
35
35
  import type { PlotDefaults } from '../types/index.js';
36
36
 
37
37
  let markProps: BoxYMarkProps = $props();
38
+
38
39
  const DEFAULTS = {
39
40
  tickMedian: true,
40
41
  tickMinMax: false,
41
42
  ...getContext<PlotDefaults>('svelteplot/_defaults').box,
42
43
  ...getContext<PlotDefaults>('svelteplot/_defaults').boxY
43
44
  };
45
+
44
46
  const {
45
- data = [{} as Datum],
46
- class: className = '',
47
+ data = [{}],
47
48
  bar,
48
49
  rule,
49
50
  tickMedian,
50
51
  tickMinMax,
51
52
  dot,
52
53
  x,
53
- y
54
+ y,
55
+ fx,
56
+ fy,
57
+ fill,
58
+ stroke,
59
+ class: className = ''
54
60
  }: BoxYMarkProps = $derived({
55
61
  ...DEFAULTS,
56
62
  ...markProps
57
63
  });
58
64
 
59
- let { data: grouped } = $derived(
65
+ const { data: grouped, ...groupChannels } = $derived(
60
66
  groupX(
61
67
  {
62
68
  data: data.filter((d) => resolveChannel('x', d, { x, y }) != null),
63
69
  x,
64
70
  y,
65
71
  y1: y,
66
- y2: y
72
+ y2: y,
73
+ fx,
74
+ fy
67
75
  },
68
76
  { y: 'median', y1: 'p25', y2: 'p75', fill: (rows) => rows }
69
77
  )
70
78
  );
71
79
 
72
- let boxData = $derived(
73
- grouped.map((row) => {
74
- const iqr = row.__y2 - row.__y1;
75
- const whisker = iqr * 1.5;
76
- const lower = row.__y1 - whisker;
77
- const upper = row.__y2 + whisker;
78
- const data = row.__fill.map((d) => ({ ...d, __y: resolveChannel('y', d, { x, y }) }));
79
- const outliers = data.filter((d) => d.__y < lower || d.__y > upper);
80
- const inside = data
81
- .filter((d) => d.__y >= lower && d.__y <= upper)
82
- .sort((a, b) => a.__y - b.__y);
83
- return {
84
- __x: row[x],
85
- p25: row.__y1,
86
- p75: row.__y2,
87
- median: row.__y,
88
- min: inside[0].__y,
89
- max: inside.at(-1).__y,
90
- outliers
91
- };
92
- })
80
+ const X = Symbol('x'),
81
+ Y = Symbol('y'),
82
+ FX = Symbol('fx'),
83
+ FY = Symbol('fy'),
84
+ P25 = Symbol('p25'),
85
+ P75 = Symbol('p75'),
86
+ MEDIAN = Symbol('median'),
87
+ MIN = Symbol('min'),
88
+ MAX = Symbol('max'),
89
+ OUTLIERS = Symbol('outliers');
90
+
91
+ const facets = $derived({
92
+ ...(fx != null && { fx: FX }),
93
+ ...(fy != null && { fy: FY })
94
+ });
95
+
96
+ const boxData = $derived(
97
+ grouped
98
+ .map((row) => {
99
+ const { y: median, y1: p25, y2: p75, fill: fill, x: rx } = groupChannels;
100
+
101
+ const iqr = row[p75] - row[p25];
102
+ const whisker = iqr * 1.5;
103
+ const lower = row[p25] - whisker;
104
+ const upper = row[p75] + whisker;
105
+ const data = row[fill].map((d) => ({
106
+ ...d,
107
+ [Y]: resolveChannel('y', d, { x, y })
108
+ }));
109
+ const outliers = data.filter((d) => d[Y] < lower || d[Y] > upper);
110
+ const inside = data
111
+ .filter((d) => d[Y] >= lower && d[Y] <= upper)
112
+ .sort((a, b) => a[Y] - b[Y]);
113
+
114
+ return {
115
+ ...data[0],
116
+ [X]: row[rx],
117
+ [P25]: row[p25],
118
+ [MEDIAN]: row[median],
119
+ [P75]: row[p75],
120
+ [MIN]: inside[0][Y],
121
+ [MAX]: inside.at(-1)[Y],
122
+ [FX]: resolveChannel('fx', data[0], { fx }, null),
123
+ [FY]: resolveChannel('fy', data[0], { fy }, null),
124
+ [OUTLIERS]: outliers
125
+ };
126
+ })
127
+ .sort((a, b) => b[MEDIAN] - a[MEDIAN])
93
128
  );
94
129
  </script>
95
130
 
96
131
  <GroupMultiple class="box-y {className || ''}" length={className ? 2 : grouped.length}>
97
- <RuleX data={boxData} x="__x" y1="min" y2="max" {...rule || {}} />
98
- <BarY data={boxData} x="__x" y1="p25" y2="p75" fill="#ddd" {...bar || {}} />
132
+ <RuleX data={boxData} x={X} y1={MIN} y2={P25} {stroke} {...rule || {}} {...facets} />
133
+ <RuleX data={boxData} x={X} y1={P75} y2={MAX} {stroke} {...rule || {}} {...facets} />
134
+ <BarY data={boxData} x={X} y1={P25} y2={P75} {fill} {stroke} {...facets} {...bar || {}} />
99
135
  {#if tickMedian}
100
136
  <TickY
101
137
  data={boxData}
102
- x="__x"
103
- y="median"
138
+ x={X}
139
+ y={MEDIAN}
140
+ {...facets}
141
+ {stroke}
104
142
  strokeWidth={2}
105
143
  {...typeof tickMedian === 'object' ? tickMedian : {}} />
106
144
  {/if}
107
145
  {#if tickMinMax}
108
146
  <TickY
109
147
  data={boxData}
110
- x="__x"
111
- y="min"
148
+ x={X}
149
+ y={MIN}
150
+ {stroke}
151
+ {...facets}
112
152
  inset="20%"
113
153
  {...typeof tickMinMax === 'object' ? tickMinMax : {}} />
114
154
  <TickY
115
155
  data={boxData}
116
- x="__x"
117
- y="max"
156
+ x={X}
157
+ y={MAX}
158
+ {stroke}
159
+ {...facets}
118
160
  inset="20%"
119
161
  {...typeof tickMinMax === 'object' ? tickMinMax : {}} />
120
162
  {/if}
121
- <Dot data={boxData.map((d) => d.outliers).flat()} {x} {y} {...dot || {}} />
163
+ <Dot
164
+ data={boxData.map((d) => d[OUTLIERS]).flat()}
165
+ {x}
166
+ {y}
167
+ {fx}
168
+ {fy}
169
+ {fill}
170
+ {stroke}
171
+ {...dot || {}} />
122
172
  </GroupMultiple>
@@ -1,88 +1,28 @@
1
- import type { ChannelAccessor, DataRecord } from '../types/index.js';
2
1
  declare class __sveltets_Render<Datum extends DataRecord> {
3
- props(): Pick<Partial<{
4
- filter?: import("../types/index.js").ConstantAccessor<boolean, Datum>;
5
- facet?: "auto" | "include" | "exclude";
6
- fx: ChannelAccessor<Datum>;
7
- fy: ChannelAccessor<Datum>;
8
- dx: import("../types/index.js").ConstantAccessor<number, Datum>;
9
- dy: import("../types/index.js").ConstantAccessor<number, Datum>;
10
- fill: ChannelAccessor<Datum>;
11
- fillOpacity: import("../types/index.js").ConstantAccessor<number, Datum>;
12
- sort: {
13
- channel: string;
14
- order?: "ascending" | "descending";
15
- } | ((a: import("../types/index.js").RawValue, b: import("../types/index.js").RawValue) => number) | import("../types/index.js").ConstantAccessor<import("../types/index.js").RawValue, Datum>;
16
- stroke: ChannelAccessor<Datum>;
17
- strokeWidth: import("../types/index.js").ConstantAccessor<number, Datum>;
18
- strokeOpacity: import("../types/index.js").ConstantAccessor<number, Datum>;
19
- strokeLinejoin: import("../types/index.js").ConstantAccessor<import("csstype").Property.StrokeLinejoin, Datum>;
20
- strokeLinecap: import("../types/index.js").ConstantAccessor<import("csstype").Property.StrokeLinecap, Datum>;
21
- strokeMiterlimit: import("../types/index.js").ConstantAccessor<number, Datum>;
22
- opacity: ChannelAccessor<Datum>;
23
- strokeDasharray: import("../types/index.js").ConstantAccessor<string, Datum>;
24
- strokeDashoffset: import("../types/index.js").ConstantAccessor<number, Datum>;
25
- mixBlendMode: import("../types/index.js").ConstantAccessor<import("csstype").Property.MixBlendMode, Datum>;
26
- clipPath: string;
27
- imageFilter: import("../types/index.js").ConstantAccessor<string, Datum>;
28
- shapeRendering: import("../types/index.js").ConstantAccessor<import("csstype").Property.ShapeRendering, Datum>;
29
- paintOrder: import("../types/index.js").ConstantAccessor<string, Datum>;
30
- onclick?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
31
- ondblclick?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
32
- onmouseup?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
33
- onmousedown?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
34
- onmouseenter?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
35
- onmousemove?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
36
- onmouseleave?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
37
- onmouseout?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
38
- onmouseover?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
39
- onpointercancel?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
40
- onpointerdown?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
41
- onpointerup?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
42
- onpointerenter?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
43
- onpointerleave?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
44
- onpointermove?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
45
- onpointerover?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
46
- onpointerout?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
47
- ondrag?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
48
- ondrop?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
49
- ondragstart?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
50
- ondragenter?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
51
- ondragleave?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
52
- ondragover?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
53
- ondragend?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
54
- ontouchstart?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
55
- ontouchmove?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
56
- ontouchend?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
57
- ontouchcancel?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
58
- oncontextmenu?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
59
- onwheel?: import("svelte/elements").MouseEventHandler<SVGPathElement>;
60
- class: string | null;
61
- cursor: import("../types/index.js").ConstantAccessor<import("csstype").Property.Cursor, Datum>;
62
- }>, "class"> & {
2
+ props(): Pick<BaseMarkProps<Datum_1>, "fill" | "stroke" | "fx" | "fy" | "class"> & {
63
3
  data: Datum[];
64
4
  x: ChannelAccessor;
65
5
  y: ChannelAccessor;
66
6
  /**
67
7
  * Options for the rule marks that represent the min/max range
68
8
  */
69
- rule: Record<string, ChannelAccessor<Datum>>;
9
+ rule: Record<string, ChannelAccessor<Datum_1>>;
70
10
  /**
71
11
  * Options for the bar marks that represent the IQR range
72
12
  */
73
- bar: Record<string, ChannelAccessor<Datum>>;
13
+ bar: Record<string, ChannelAccessor<Datum_1>>;
74
14
  /**
75
15
  * Options for the tick marks that represent the median
76
16
  */
77
- tickMedian: boolean | Record<string, ChannelAccessor<Datum>>;
17
+ tickMedian: Record<string, ChannelAccessor<Datum_1>> | boolean;
78
18
  /**
79
19
  * Options for the tick marks that represent the min/max range
80
20
  */
81
- tickMinMax: boolean | Record<string, ChannelAccessor<Datum>>;
21
+ tickMinMax: Record<string, ChannelAccessor<Datum_1>> | boolean;
82
22
  /**
83
23
  * Options for the dot marks that represent the outliers
84
24
  */
85
- dot: Record<string, ChannelAccessor<Datum>>;
25
+ dot: Record<string, ChannelAccessor<Datum_1>>;
86
26
  };
87
27
  events(): {};
88
28
  slots(): {};
@@ -91,10 +91,3 @@
91
91
  </GroupMultiple>
92
92
  {/snippet}
93
93
  </Mark>
94
-
95
- <style>
96
- rect {
97
- stroke: none;
98
- /* fill: none; */
99
- }
100
- </style>
@@ -55,18 +55,18 @@
55
55
  : [
56
56
  args.x != null
57
57
  ? d.x
58
- : isLeft
59
- ? plot.options.marginLeft
60
- : isRight
61
- ? plot.options.marginLeft + plot.facetWidth
62
- : plot.options.marginLeft + plot.facetWidth * 0.5,
58
+ : (isLeft
59
+ ? plot.options.marginLeft
60
+ : isRight
61
+ ? plot.options.marginLeft + plot.facetWidth
62
+ : plot.options.marginLeft + plot.facetWidth * 0.5) + (d.dx ?? 0),
63
63
  args.y != null
64
64
  ? d.y
65
- : isTop
66
- ? plot.options.marginTop
67
- : isBottom
68
- ? plot.options.marginTop + plot.facetHeight
69
- : plot.options.marginTop + plot.facetHeight * 0.5
65
+ : (isTop
66
+ ? plot.options.marginTop
67
+ : isBottom
68
+ ? plot.options.marginTop + plot.facetHeight
69
+ : plot.options.marginTop + plot.facetHeight * 0.5) + (d.dy ?? 0)
70
70
  ]
71
71
  );
72
72
 
@@ -34,7 +34,6 @@ Helper component for rendering rectangular marks in SVG
34
34
  import type { BaseMarkProps, BaseRectMarkProps, BorderRadius } from '../../types/mark.js';
35
35
  import type { DataRecord, ScaledDataRecord } from '../../types/data.js';
36
36
  import type { PlotContext, UsedScales } from '../../types/index.js';
37
- import { RAW_VALUE } from '../../transforms/recordize.js';
38
37
 
39
38
  let {
40
39
  datum,
@@ -47,7 +46,7 @@ Helper component for rendering rectangular marks in SVG
47
46
  useInsetAsFallbackVertically = true,
48
47
  useInsetAsFallbackHorizontally = true,
49
48
  usedScales,
50
- fallbackStyle = 'fill'
49
+ fallbackStyle
51
50
  }: RectPathProps = $props();
52
51
 
53
52
  const { getPlotState } = getContext<PlotContext>('svelteplot');
@@ -94,7 +93,13 @@ Helper component for rendering rectangular marks in SVG
94
93
  ) > 0)
95
94
  );
96
95
  const [style, styleClass] = $derived(
97
- resolveStyles(plot, datum, options, fallbackStyle, usedScales)
96
+ resolveStyles(
97
+ plot,
98
+ datum,
99
+ options,
100
+ !fallbackStyle ? (options.stroke && !options.fill ? 'stroke' : 'fill') : fallbackStyle,
101
+ usedScales
102
+ )
98
103
  );
99
104
  </script>
100
105
 
@@ -2,6 +2,7 @@ import type { DataRecord } from '../types/index.js';
2
2
  import type { ScaledChannelName, TransformArg } from '../types/index.js';
3
3
  type RenameChannelsOptions = Partial<Record<ScaledChannelName, ScaledChannelName>>;
4
4
  type ReplaceChannelsOptions = Partial<Record<ScaledChannelName, ScaledChannelName[]>>;
5
+ export declare const RENAME = "__renamed__";
5
6
  /**
6
7
  * renames a channel without modifying the data
7
8
  */
@@ -1,3 +1,5 @@
1
+ // using a symbol doesn't work because channels are spread into components
2
+ export const RENAME = '__renamed__';
1
3
  /**
2
4
  * renames a channel without modifying the data
3
5
  */
@@ -6,6 +8,9 @@ export function renameChannels({ data, ...channels }, options) {
6
8
  for (const [from, to] of Object.entries(options)) {
7
9
  if (newChannels[from] !== undefined) {
8
10
  newChannels[to] = newChannels[from];
11
+ // keep track of the renaming
12
+ newChannels[RENAME] = newChannels[RENAME] || {};
13
+ newChannels[RENAME][to] = from;
9
14
  delete newChannels[from];
10
15
  }
11
16
  }
@@ -25,22 +25,7 @@
25
25
 
26
26
  <path
27
27
  d={pathD}
28
- class="rotating"
29
28
  style:animation-duration="{duration}s"
30
29
  stroke="currentColor"
31
30
  fill="none"
32
31
  {...restProps} />
33
-
34
- <style>
35
- @keyframes rotating {
36
- from {
37
- transform: rotate(0deg);
38
- }
39
- to {
40
- transform: rotate(360deg);
41
- }
42
- }
43
- .rotating {
44
- animation: rotating 2s linear infinite;
45
- }
46
- </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteplot",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "license": "ISC",
5
5
  "author": {
6
6
  "name": "Gregor Aisch",
@@ -90,7 +90,8 @@
90
90
  "typescript": "^5.9.2",
91
91
  "vite": "^6.3.5",
92
92
  "vitest": "^3.2.4",
93
- "vitest-matchmedia-mock": "^2.0.3"
93
+ "vitest-matchmedia-mock": "^2.0.3",
94
+ "yoctocolors": "^2.1.2"
94
95
  },
95
96
  "types": "./dist/index.d.ts",
96
97
  "type": "module",
@@ -118,6 +119,8 @@
118
119
  "build": "vite build",
119
120
  "preview": "vite preview",
120
121
  "test": "npm run test:unit",
122
+ "test:visual": "node scripts/visual-regression.js",
123
+ "vr:report": "node scripts/vr-report.js",
121
124
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
122
125
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
123
126
  "lint": "prettier --check src && eslint src",