svelteplot 0.2.1 → 0.2.3

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.
@@ -14,6 +14,7 @@
14
14
  import CanvasLayer from './CanvasLayer.svelte';
15
15
  import { getContext } from 'svelte';
16
16
  import { devicePixelRatio } from 'svelte/reactivity/window';
17
+ import { resolveColor } from './canvas';
17
18
 
18
19
  const { getPlotState } = getContext<PlotContext>('svelteplot');
19
20
  const plot = $derived(getPlotState());
@@ -46,19 +47,8 @@
46
47
  if (datum.valid) {
47
48
  let { fill, stroke } = datum;
48
49
 
49
- if (`${fill}`.toLowerCase() === 'currentcolor')
50
- fill = getComputedStyle(
51
- canvas?.parentElement?.parentElement
52
- ).getPropertyValue('color');
53
- if (`${stroke}`.toLowerCase() === 'currentcolor')
54
- stroke = getComputedStyle(
55
- canvas?.parentElement?.parentElement
56
- ).getPropertyValue('color');
57
-
58
- if (CSS_VAR.test(fill))
59
- fill = getComputedStyle(canvas).getPropertyValue(fill.slice(4, -1));
60
- if (CSS_VAR.test(stroke))
61
- stroke = getComputedStyle(canvas).getPropertyValue(stroke.slice(4, -1));
50
+ fill = resolveColor(fill, canvas);
51
+ stroke = resolveColor(stroke, canvas);
62
52
 
63
53
  if (stroke && stroke !== 'none') {
64
54
  const strokeWidth = resolveProp(
@@ -0,0 +1,116 @@
1
+ <script lang="ts">
2
+ import type {
3
+ Mark,
4
+ BaseMarkProps,
5
+ PlotContext,
6
+ ScaledDataRecord,
7
+ UsedScales
8
+ } from '../../types.js';
9
+ import { resolveProp, resolveScaledStyleProps } from '../../helpers/resolve.js';
10
+ import { getContext } from 'svelte';
11
+ import { type Line } from 'd3-shape';
12
+ import CanvasLayer from './CanvasLayer.svelte';
13
+ import type { Attachment } from 'svelte/attachments';
14
+ import { devicePixelRatio } from 'svelte/reactivity/window';
15
+ import { resolveColor } from './canvas';
16
+
17
+ let {
18
+ mark,
19
+ groupedLineData,
20
+ usedScales,
21
+ linePath
22
+ }: {
23
+ mark: Mark<BaseMarkProps>;
24
+ groupedLineData: ScaledDataRecord[][];
25
+ usedScales: UsedScales;
26
+ linePath: Line<ScaledDataRecord>;
27
+ groupByKey?: unknown;
28
+ } = $props();
29
+
30
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
31
+ const plot = $derived(getPlotState());
32
+
33
+ function maybeOpacity(value: unknown) {
34
+ return value == null ? 1 : +value;
35
+ }
36
+
37
+ const render = ((canvas: HTMLCanvasElement) => {
38
+ const context = canvas.getContext('2d');
39
+
40
+ $effect(() => {
41
+ if (context) {
42
+ linePath.context(context);
43
+ context.resetTransform();
44
+ context.scale(devicePixelRatio.current ?? 1, devicePixelRatio.current ?? 1);
45
+ context.lineJoin = 'round';
46
+ context.lineCap = 'round';
47
+
48
+ for (const group of groupedLineData) {
49
+ if (group.length < 2) continue;
50
+
51
+ // Get the first point to determine line styles
52
+ const firstPoint = group[0];
53
+ if (!firstPoint || !firstPoint.valid) continue;
54
+
55
+ let { stroke, ...restStyles } = resolveScaledStyleProps(
56
+ firstPoint.datum,
57
+ mark.options,
58
+ usedScales,
59
+ plot,
60
+ 'stroke'
61
+ );
62
+
63
+ const opacity = maybeOpacity(restStyles['opacity']);
64
+ const strokeOpacity = maybeOpacity(restStyles['stroke-opacity']);
65
+
66
+ const strokeWidth = resolveProp(
67
+ mark.options.strokeWidth,
68
+ firstPoint.datum,
69
+ 1.4
70
+ ) as number;
71
+
72
+ if (mark.options.outlineStroke) {
73
+ // draw stroke outline first
74
+ const outlineStroke = resolveColor(mark.options.outlineStroke, canvas);
75
+ const outlineStrokeWidth =
76
+ mark.options.outlineStrokeWidth ?? strokeWidth + 2;
77
+ const outlineStrokeOpacity = mark.options.outlineStrokeOpacity ?? 1;
78
+
79
+ context.lineWidth = outlineStrokeWidth;
80
+ context.strokeStyle = outlineStroke;
81
+ context.globalAlpha = opacity * outlineStrokeOpacity;
82
+ context.beginPath();
83
+ linePath(group);
84
+ context.stroke();
85
+ }
86
+
87
+ stroke = resolveColor(stroke, canvas);
88
+
89
+ if (stroke && stroke !== 'none') {
90
+ context.lineWidth = strokeWidth ?? 1.4;
91
+ }
92
+
93
+ context.strokeStyle = stroke ? stroke : 'currentColor';
94
+ context.globalAlpha = opacity * strokeOpacity;
95
+
96
+ // Start drawing the line
97
+ context.beginPath();
98
+ linePath(group);
99
+ context.stroke();
100
+ }
101
+ linePath.context(null);
102
+ }
103
+
104
+ return () => {
105
+ context?.clearRect(
106
+ 0,
107
+ 0,
108
+ plot.width * (devicePixelRatio.current ?? 1),
109
+ plot.height * (devicePixelRatio.current ?? 1)
110
+ );
111
+ };
112
+ });
113
+ }) as Attachment;
114
+ </script>
115
+
116
+ <CanvasLayer {@attach render} />
@@ -0,0 +1,12 @@
1
+ import type { Mark, BaseMarkProps, ScaledDataRecord, UsedScales } from '../../types.js';
2
+ import { type Line } from 'd3-shape';
3
+ type $$ComponentProps = {
4
+ mark: Mark<BaseMarkProps>;
5
+ groupedLineData: ScaledDataRecord[][];
6
+ usedScales: UsedScales;
7
+ linePath: Line<ScaledDataRecord>;
8
+ groupByKey?: unknown;
9
+ };
10
+ declare const LineCanvas: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type LineCanvas = ReturnType<typeof LineCanvas>;
12
+ export default LineCanvas;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte';
3
+ import type { PlotContext, RawValue } from '../../types';
4
+
5
+ let {
6
+ id,
7
+ stops
8
+ }: {
9
+ id: string;
10
+ stops: { x: RawValue; color: string }[];
11
+ } = $props();
12
+
13
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
14
+ const plot = $derived(getPlotState());
15
+
16
+ const projectedStops = $derived(
17
+ stops
18
+ .map((d) => ({ ...d, px: plot.scales.x.fn(d.x) / plot.width }))
19
+ .sort((a, b) => a.px - b.px)
20
+ );
21
+ </script>
22
+
23
+ <linearGradient {id} gradientUnits="userSpaceOnUse" x1={0} y2={0} y1={0} x2={plot.width}>
24
+ {#each projectedStops as { px, color }}
25
+ <stop stop-color={color} offset={px} />
26
+ {/each}
27
+ </linearGradient>
@@ -0,0 +1,11 @@
1
+ import type { RawValue } from '../../types';
2
+ type $$ComponentProps = {
3
+ id: string;
4
+ stops: {
5
+ x: RawValue;
6
+ color: string;
7
+ }[];
8
+ };
9
+ declare const LinearGradientX: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type LinearGradientX = ReturnType<typeof LinearGradientX>;
11
+ export default LinearGradientX;
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ import { getContext } from 'svelte';
3
+ import type { PlotContext, RawValue } from '../../types';
4
+
5
+ let {
6
+ id,
7
+ stops
8
+ }: {
9
+ id: string;
10
+ stops: { y: RawValue; color: string }[];
11
+ } = $props();
12
+
13
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
14
+ const plot = $derived(getPlotState());
15
+
16
+ const projectedStops = $derived(
17
+ stops
18
+ .map((d) => ({ ...d, py: plot.scales.y.fn(d.y) / plot.height }))
19
+ .sort((a, b) => a.py - b.py)
20
+ );
21
+ </script>
22
+
23
+ <linearGradient {id} gradientUnits="userSpaceOnUse" x1={0} x2={0} y1={0} y2={plot.height}>
24
+ {#each projectedStops as { py, color }}
25
+ <stop stop-color={color} offset={py} />
26
+ {/each}
27
+ </linearGradient>
@@ -0,0 +1,11 @@
1
+ import type { RawValue } from '../../types';
2
+ type $$ComponentProps = {
3
+ id: string;
4
+ stops: {
5
+ y: RawValue;
6
+ color: string;
7
+ }[];
8
+ };
9
+ declare const LinearGradientY: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type LinearGradientY = ReturnType<typeof LinearGradientY>;
11
+ export default LinearGradientY;
@@ -0,0 +1,129 @@
1
+ <!-- @component
2
+ Helper component for rendering rectangular marks in SVG
3
+ -->
4
+ <script lang="ts">
5
+ import { resolveProp, resolveStyles } from '../../helpers/resolve';
6
+ import { roundedRect } from '../../helpers/roundedRect';
7
+ import type {
8
+ BaseMarkProps,
9
+ BaseRectMarkProps,
10
+ BorderRadius,
11
+ ScaledDataRecord,
12
+ UsedScales,
13
+ PlotContext
14
+ } from '../../types';
15
+ import { addEventHandlers } from './events';
16
+ import { getContext } from 'svelte';
17
+
18
+ let {
19
+ datum,
20
+ options,
21
+ class: className = null,
22
+ x,
23
+ y,
24
+ width,
25
+ height,
26
+ useInsetAsFallbackVertically = true,
27
+ useInsetAsFallbackHorizontally = true,
28
+ usedScales
29
+ }: {
30
+ datum: ScaledDataRecord;
31
+ class: string | null;
32
+ x: number;
33
+ y: number;
34
+ width: number;
35
+ height: number;
36
+ options: BaseRectMarkProps & BaseMarkProps;
37
+ /**
38
+ * By default, the `inset` property is applied to all four insets. Mark components
39
+ * can tweak this behavior for insetTop and insetBottom by setting the
40
+ * useInsetAsFallbackVertically prop to false.
41
+ */
42
+ useInsetAsFallbackVertically?: boolean;
43
+ /**
44
+ * By default, the `inset` property is applied to all four insets. Mark components
45
+ * can tweak this behavior for insetLeft and insetRight by setting the
46
+ * useInsetAsFallbackHorizontally prop to false.
47
+ */
48
+ useInsetAsFallbackHorizontally?: boolean;
49
+ usedScales: UsedScales;
50
+ } = $props();
51
+
52
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
53
+ const plot = $derived(getPlotState());
54
+
55
+ const dx = $derived(+(resolveProp(options.dx, datum.datum, 0) as number));
56
+ const dy = $derived(+(resolveProp(options.dy, datum.datum, 0) as number));
57
+ const inset = $derived(+(resolveProp(options.inset, datum.datum, 0) as number));
58
+ const insetLeft = $derived(
59
+ +(resolveProp(
60
+ options.insetLeft,
61
+ datum.datum,
62
+ useInsetAsFallbackHorizontally ? inset : 0
63
+ ) as number)
64
+ );
65
+ const insetRight = $derived(
66
+ +(resolveProp(
67
+ options.insetRight,
68
+ datum.datum,
69
+ useInsetAsFallbackHorizontally ? inset : 0
70
+ ) as number)
71
+ );
72
+ const insetTop = $derived(
73
+ +(resolveProp(
74
+ options.insetTop,
75
+ datum.datum,
76
+ useInsetAsFallbackVertically ? inset : 0
77
+ ) as number)
78
+ );
79
+ const insetBottom = $derived(
80
+ +(resolveProp(
81
+ options.insetBottom,
82
+ datum.datum,
83
+ useInsetAsFallbackVertically ? inset : 0
84
+ ) as number)
85
+ );
86
+ const borderRadius = $derived((options.borderRadius ?? 0) as BorderRadius);
87
+ const hasBorderRadius = $derived(
88
+ (typeof borderRadius === 'number' && borderRadius > 0) ||
89
+ (typeof borderRadius === 'object' &&
90
+ Math.max(
91
+ borderRadius.topRight ?? 0,
92
+ borderRadius.bottomRight ?? 0,
93
+ borderRadius.topLeft ?? 0,
94
+ borderRadius.bottomLeft ?? 0
95
+ ) > 0)
96
+ );
97
+ const [style, styleClass] = $derived(resolveStyles(plot, datum, options, 'fill', usedScales));
98
+ </script>
99
+
100
+ {#if hasBorderRadius}
101
+ <path
102
+ transform="translate({[x + dx + insetLeft, y + insetBottom + dy]})"
103
+ d={roundedRect(
104
+ 0,
105
+ 0,
106
+ width - insetLeft - insetRight,
107
+ height - insetTop - insetBottom,
108
+ borderRadius
109
+ )}
110
+ class={[styleClass, className]}
111
+ {style}
112
+ use:addEventHandlers={{
113
+ getPlotState,
114
+ options,
115
+ datum: datum.datum
116
+ }} />
117
+ {:else}
118
+ <rect
119
+ transform="translate({[x + dx + insetLeft, y + insetBottom + dy]})"
120
+ width={width - insetLeft - insetRight}
121
+ height={height - insetTop - insetBottom}
122
+ class={[styleClass, className]}
123
+ {style}
124
+ use:addEventHandlers={{
125
+ getPlotState,
126
+ options,
127
+ datum: datum.datum
128
+ }} />
129
+ {/if}
@@ -0,0 +1,27 @@
1
+ import type { BaseMarkProps, BaseRectMarkProps, ScaledDataRecord, UsedScales } from '../../types';
2
+ type $$ComponentProps = {
3
+ datum: ScaledDataRecord;
4
+ class: string | null;
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ options: BaseRectMarkProps & BaseMarkProps;
10
+ /**
11
+ * By default, the `inset` property is applied to all four insets. Mark components
12
+ * can tweak this behavior for insetTop and insetBottom by setting the
13
+ * useInsetAsFallbackVertically prop to false.
14
+ */
15
+ useInsetAsFallbackVertically?: boolean;
16
+ /**
17
+ * By default, the `inset` property is applied to all four insets. Mark components
18
+ * can tweak this behavior for insetLeft and insetRight by setting the
19
+ * useInsetAsFallbackHorizontally prop to false.
20
+ */
21
+ useInsetAsFallbackHorizontally?: boolean;
22
+ usedScales: UsedScales;
23
+ };
24
+ /** Helper component for rendering rectangular marks in SVG */
25
+ declare const RectPath: import("svelte").Component<$$ComponentProps, {}, "">;
26
+ type RectPath = ReturnType<typeof RectPath>;
27
+ export default RectPath;
@@ -0,0 +1 @@
1
+ export declare function resolveColor(color: string, canvas: HTMLCanvasElement): string | CanvasGradient;
@@ -0,0 +1,32 @@
1
+ import { CSS_URL, CSS_VAR } from '../../constants';
2
+ export function resolveColor(color, canvas) {
3
+ if (`${color}`.toLowerCase() === 'currentcolor') {
4
+ color = getComputedStyle(canvas?.parentElement?.parentElement).getPropertyValue('color');
5
+ }
6
+ if (CSS_VAR.test(color)) {
7
+ color = getComputedStyle(canvas).getPropertyValue(color.slice(4, -1));
8
+ }
9
+ if (CSS_URL.test(color)) {
10
+ // might be a gradient we can parse!
11
+ const m = color.match(/^url\((#[^\)]+)\)/);
12
+ const gradientId = m[1];
13
+ const gradient = canvas.ownerDocument.querySelector(gradientId);
14
+ if (gradient) {
15
+ // parse gradient
16
+ if (gradient.nodeName.toLowerCase() === 'lineargradient') {
17
+ const x0 = +gradient.getAttribute('x1');
18
+ const x1 = +gradient.getAttribute('x2');
19
+ const y0 = +gradient.getAttribute('y1');
20
+ const y1 = +gradient.getAttribute('y2');
21
+ const ctxGradient = canvas.getContext('2d').createLinearGradient(x0, y0, x1, y1);
22
+ for (const stop of gradient.querySelectorAll('stop')) {
23
+ const offset = +stop.getAttribute('offset');
24
+ const color = resolveColor(stop.getAttribute('stop-color'), canvas);
25
+ ctxGradient.addColorStop(Math.min(1, Math.max(0, offset)), color);
26
+ }
27
+ return ctxGradient;
28
+ }
29
+ }
30
+ }
31
+ return color;
32
+ }
package/dist/types.d.ts CHANGED
@@ -555,14 +555,19 @@ export type BaseMarkProps = Partial<{
555
555
  class: string;
556
556
  cursor: ConstantAccessor<CSS.Property.Cursor>;
557
557
  }>;
558
+ export type BorderRadius = number | {
559
+ topLeft?: number;
560
+ topRight?: number;
561
+ bottomRight?: number;
562
+ bottomLeft?: number;
563
+ };
558
564
  export type BaseRectMarkProps = {
559
- rx?: ConstantAccessor<number>;
560
- ry?: ConstantAccessor<number>;
561
565
  inset?: ConstantAccessor<number>;
562
566
  insetLeft?: ConstantAccessor<number>;
563
567
  insetTop?: ConstantAccessor<number>;
564
568
  insetRight?: ConstantAccessor<number>;
565
569
  insetBottom?: ConstantAccessor<number>;
570
+ borderRadius?: BorderRadius;
566
571
  };
567
572
  export type Channels = Record<string, ChannelAccessor | ConstantAccessor<string | number | boolean | symbol>>;
568
573
  export type TransformArg<K> = Channels & {