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 +41 -0
- package/dist/Mark.svelte +5 -2
- package/dist/helpers/scales.js +2 -2
- package/dist/marks/BarY.svelte +0 -1
- package/dist/marks/BoxX.svelte +72 -28
- package/dist/marks/BoxY.svelte +88 -38
- package/dist/marks/BoxY.svelte.d.ts +6 -66
- package/dist/marks/Rect.svelte +0 -7
- package/dist/marks/helpers/MultilineText.svelte +10 -10
- package/dist/marks/helpers/RectPath.svelte +8 -3
- package/dist/transforms/rename.d.ts +1 -0
- package/dist/transforms/rename.js +5 -0
- package/dist/ui/Spiral.svelte +0 -15
- package/package.json +5 -2
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(
|
|
266
|
+
? projectX(origChannel as 'x' | 'x1' | 'x2', plot.scales, value)
|
|
264
267
|
: scale === 'y'
|
|
265
|
-
? projectY(
|
|
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)
|
package/dist/helpers/scales.js
CHANGED
|
@@ -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 (
|
|
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
|
|
256
|
+
return 'point';
|
|
257
257
|
return 'linear';
|
|
258
258
|
}
|
|
259
259
|
const scaledChannelNames = [
|
package/dist/marks/BarY.svelte
CHANGED
package/dist/marks/BoxX.svelte
CHANGED
|
@@ -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
|
|
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
|
|
55
|
-
const upper = row
|
|
56
|
-
const data = row.
|
|
80
|
+
const lower = row[p25] - whisker;
|
|
81
|
+
const upper = row[p75] + whisker;
|
|
82
|
+
const data = row[fill].map((d) => ({
|
|
57
83
|
...d,
|
|
58
|
-
|
|
84
|
+
[X]: resolveChannel('x', d, { x, y })
|
|
59
85
|
}));
|
|
60
|
-
const outliers = data.filter((d) => d
|
|
86
|
+
const outliers = data.filter((d) => d[X] < lower || d[X] > upper);
|
|
61
87
|
const inside = data
|
|
62
|
-
.filter((d) => d
|
|
63
|
-
.sort((a, b) => a
|
|
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
|
-
[
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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=
|
|
82
|
-
<
|
|
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=
|
|
87
|
-
x=
|
|
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=
|
|
95
|
-
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=
|
|
101
|
-
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
|
|
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>
|
package/dist/marks/BoxY.svelte
CHANGED
|
@@ -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
|
|
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 = [{}
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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=
|
|
98
|
-
<
|
|
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=
|
|
103
|
-
y=
|
|
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=
|
|
111
|
-
y=
|
|
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=
|
|
117
|
-
y=
|
|
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
|
|
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<
|
|
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<
|
|
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<
|
|
13
|
+
bar: Record<string, ChannelAccessor<Datum_1>>;
|
|
74
14
|
/**
|
|
75
15
|
* Options for the tick marks that represent the median
|
|
76
16
|
*/
|
|
77
|
-
tickMedian:
|
|
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:
|
|
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<
|
|
25
|
+
dot: Record<string, ChannelAccessor<Datum_1>>;
|
|
86
26
|
};
|
|
87
27
|
events(): {};
|
|
88
28
|
slots(): {};
|
package/dist/marks/Rect.svelte
CHANGED
|
@@ -55,18 +55,18 @@
|
|
|
55
55
|
: [
|
|
56
56
|
args.x != null
|
|
57
57
|
? d.x
|
|
58
|
-
: isLeft
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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(
|
|
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
|
}
|
package/dist/ui/Spiral.svelte
CHANGED
|
@@ -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
|
+
"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",
|