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