svelteplot 0.9.2 → 0.10.0

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.
@@ -0,0 +1,332 @@
1
+ <script lang="ts" generics="Datum extends DataRecord">
2
+ interface TextCanvasProps<Datum extends DataRecord> {
3
+ data: ScaledDataRecord<Datum>[];
4
+ options: BaseMarkProps<Datum> & {
5
+ x?: ChannelAccessor<Datum>;
6
+ y?: ChannelAccessor<Datum>;
7
+ text: ConstantAccessor<string | null | false | undefined, Datum>;
8
+ title?: ConstantAccessor<string, Datum>;
9
+ fontFamily?: ConstantAccessor<CSS.Property.FontFamily, Datum>;
10
+ fontSize?: ConstantAccessor<CSS.Property.FontSize | number, Datum>;
11
+ fontWeight?: ConstantAccessor<CSS.Property.FontWeight, Datum>;
12
+ fontStyle?: ConstantAccessor<CSS.Property.FontStyle, Datum>;
13
+ fontVariant?: ConstantAccessor<CSS.Property.FontVariant, Datum>;
14
+ letterSpacing?: ConstantAccessor<CSS.Property.LetterSpacing, Datum>;
15
+ wordSpacing?: ConstantAccessor<CSS.Property.WordSpacing, Datum>;
16
+ textTransform?: ConstantAccessor<CSS.Property.TextTransform, Datum>;
17
+ textDecoration?: ConstantAccessor<CSS.Property.TextDecoration, Datum>;
18
+ textAnchor?: ConstantAccessor<CSS.Property.TextAnchor, Datum>;
19
+ lineAnchor?: ConstantAccessor<'bottom' | 'top' | 'middle'>;
20
+ lineHeight?: ConstantAccessor<number, Datum>;
21
+ frameAnchor?: ConstantAccessor<
22
+ | 'bottom'
23
+ | 'top'
24
+ | 'left'
25
+ | 'right'
26
+ | 'top-left'
27
+ | 'bottom-left'
28
+ | 'top-right'
29
+ | 'bottom-right'
30
+ | 'middle',
31
+ Datum
32
+ >;
33
+ rotate?: ConstantAccessor<number, Datum>;
34
+ };
35
+ usedScales: UsedScales;
36
+ }
37
+
38
+ import type * as CSS from 'csstype';
39
+ import type { Attachment } from 'svelte/attachments';
40
+ import { devicePixelRatio } from 'svelte/reactivity/window';
41
+ import type {
42
+ BaseMarkProps,
43
+ ChannelAccessor,
44
+ ConstantAccessor,
45
+ DataRecord,
46
+ ScaledDataRecord,
47
+ UsedScales
48
+ } from '../../types/index.js';
49
+ import { resolveProp, resolveScaledStyleProps } from '../../helpers/resolve.js';
50
+ import { CSS_VAR } from '../../constants';
51
+ import { maybeFromPixel, maybeFromRem } from '../../helpers/getBaseStyles';
52
+ import { usePlot } from '../../hooks/usePlot.svelte.js';
53
+ import CanvasLayer from './CanvasLayer.svelte';
54
+ import { resolveColor } from './canvas.js';
55
+
56
+ const plot = usePlot();
57
+
58
+ const LINE_ANCHOR = {
59
+ bottom: 'alphabetic',
60
+ middle: 'middle',
61
+ top: 'hanging'
62
+ } as const;
63
+
64
+ const DEFAULT_TEXT_OPTIONS = {
65
+ strokeWidth: 1.6
66
+ } as const;
67
+
68
+ let { data, options, usedScales }: TextCanvasProps<Datum> = $props();
69
+
70
+ function maybeOpacity(value: unknown) {
71
+ return value == null ? 1 : +value;
72
+ }
73
+
74
+ function normalizeTextAlign(value: unknown): CanvasTextAlign {
75
+ if (value === 'end' || value === 'right') return 'right';
76
+ if (value === 'middle' || value === 'center') return 'center';
77
+ return 'left';
78
+ }
79
+
80
+ function normalizeLineCap(value: unknown): CanvasLineCap {
81
+ return value === 'round' || value === 'square' || value === 'butt' ? value : 'butt';
82
+ }
83
+
84
+ function normalizeLineJoin(value: unknown): CanvasLineJoin {
85
+ return value === 'round' || value === 'bevel' || value === 'miter' ? value : 'miter';
86
+ }
87
+
88
+ function normalizeLineAnchor(value: unknown): 'bottom' | 'middle' | 'top' {
89
+ return value === 'top' || value === 'bottom' || value === 'middle' ? value : 'middle';
90
+ }
91
+
92
+ function toPixels(value: unknown, canvas: HTMLCanvasElement, fallback = 12): number {
93
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
94
+ if (typeof value !== 'string') return fallback;
95
+
96
+ const raw = value.trim();
97
+ if (!raw) return fallback;
98
+
99
+ const fromVar = CSS_VAR.exec(raw);
100
+ const resolved = fromVar
101
+ ? getComputedStyle(canvas).getPropertyValue(`--${fromVar[1]}`).trim()
102
+ : raw;
103
+
104
+ const rootFontSize = maybeFromPixel(
105
+ getComputedStyle(canvas.ownerDocument.documentElement).fontSize
106
+ );
107
+ const maybeRem = maybeFromRem(resolved, Number(rootFontSize));
108
+ const maybePx = maybeFromPixel(maybeRem);
109
+ const numeric =
110
+ typeof maybePx === 'number'
111
+ ? maybePx
112
+ : Number.isFinite(+maybePx)
113
+ ? +maybePx
114
+ : parseFloat(resolved);
115
+
116
+ return Number.isFinite(numeric) ? numeric : fallback;
117
+ }
118
+
119
+ function toFontSize(value: unknown, canvas: HTMLCanvasElement, fallback = 12) {
120
+ if (typeof value === 'number' && Number.isFinite(value)) {
121
+ return { css: `${value}px`, numeric: value };
122
+ }
123
+ if (typeof value !== 'string') {
124
+ return { css: `${fallback}px`, numeric: fallback };
125
+ }
126
+
127
+ const raw = value.trim();
128
+ if (!raw) return { css: `${fallback}px`, numeric: fallback };
129
+
130
+ const fromVar = CSS_VAR.exec(raw);
131
+ const resolved = fromVar
132
+ ? getComputedStyle(canvas).getPropertyValue(`--${fromVar[1]}`).trim()
133
+ : raw;
134
+ const numeric = toPixels(resolved, canvas, fallback);
135
+ const css = /^\d+(?:\.\d+)?$/.test(resolved) ? `${resolved}px` : resolved;
136
+
137
+ return {
138
+ css: css || `${fallback}px`,
139
+ numeric
140
+ };
141
+ }
142
+
143
+ function textTransform(line: string, transform: unknown) {
144
+ if (transform === 'uppercase') return line.toUpperCase();
145
+ if (transform === 'lowercase') return line.toLowerCase();
146
+ if (transform === 'capitalize') {
147
+ return line.replace(/\b[a-z]/gi, (letter) => letter.toUpperCase());
148
+ }
149
+ return line;
150
+ }
151
+
152
+ const render: Attachment = (canvasEl: Element) => {
153
+ const canvas = canvasEl as HTMLCanvasElement;
154
+ const context = canvas.getContext('2d');
155
+
156
+ $effect(() => {
157
+ if (context) {
158
+ const inheritedFontStyles = getComputedStyle(
159
+ (canvas.parentElement?.parentElement as Element) || plot.body || canvas
160
+ );
161
+ context.resetTransform();
162
+ context.scale(devicePixelRatio.current ?? 1, devicePixelRatio.current ?? 1);
163
+
164
+ for (const datum of data) {
165
+ if (!datum.valid) continue;
166
+
167
+ const frameAnchor = resolveProp(options.frameAnchor, datum.datum, 'middle');
168
+ const isLeft =
169
+ frameAnchor === 'left' ||
170
+ frameAnchor === 'top-left' ||
171
+ frameAnchor === 'bottom-left';
172
+ const isRight =
173
+ frameAnchor === 'right' ||
174
+ frameAnchor === 'top-right' ||
175
+ frameAnchor === 'bottom-right';
176
+ const isTop =
177
+ frameAnchor === 'top' ||
178
+ frameAnchor === 'top-left' ||
179
+ frameAnchor === 'top-right';
180
+ const isBottom =
181
+ frameAnchor === 'bottom' ||
182
+ frameAnchor === 'bottom-left' ||
183
+ frameAnchor === 'bottom-right';
184
+
185
+ const x =
186
+ options.x != null
187
+ ? datum.x
188
+ : (isLeft
189
+ ? plot.options.marginLeft
190
+ : isRight
191
+ ? plot.options.marginLeft + plot.facetWidth
192
+ : plot.options.marginLeft + plot.facetWidth * 0.5) +
193
+ (datum.dx ?? 0);
194
+ const y =
195
+ options.y != null
196
+ ? datum.y
197
+ : (isTop
198
+ ? plot.options.marginTop
199
+ : isBottom
200
+ ? plot.options.marginTop + plot.facetHeight
201
+ : plot.options.marginTop + plot.facetHeight * 0.5) +
202
+ (datum.dy ?? 0);
203
+
204
+ if (x == null || y == null) continue;
205
+
206
+ const lineAnchor = normalizeLineAnchor(
207
+ resolveProp(
208
+ options.lineAnchor,
209
+ datum.datum,
210
+ options.y != null
211
+ ? 'middle'
212
+ : isTop
213
+ ? 'top'
214
+ : isBottom
215
+ ? 'bottom'
216
+ : 'middle'
217
+ )
218
+ );
219
+ const defaultTextAnchor = isLeft ? 'start' : isRight ? 'end' : 'middle';
220
+ const styleProps = resolveScaledStyleProps(
221
+ datum.datum,
222
+ {
223
+ ...DEFAULT_TEXT_OPTIONS,
224
+ textAnchor: defaultTextAnchor,
225
+ ...options
226
+ },
227
+ usedScales,
228
+ plot,
229
+ 'fill'
230
+ );
231
+
232
+ const inheritedFontSize = inheritedFontStyles.fontSize || '12px';
233
+ const { css: fontSize, numeric: fontSizePx } = toFontSize(
234
+ styleProps['font-size'] ?? inheritedFontSize,
235
+ canvas,
236
+ toPixels(inheritedFontSize, canvas, 12)
237
+ );
238
+ const fontStyle = String(
239
+ styleProps['font-style'] || inheritedFontStyles.fontStyle || 'normal'
240
+ );
241
+ const fontVariant = String(
242
+ styleProps['font-variant'] || inheritedFontStyles.fontVariant || 'normal'
243
+ );
244
+ const fontWeight = String(
245
+ styleProps['font-weight'] || inheritedFontStyles.fontWeight || 'normal'
246
+ );
247
+ const fontFamily = String(
248
+ styleProps['font-family'] || inheritedFontStyles.fontFamily || 'sans-serif'
249
+ );
250
+ const textTransformValue =
251
+ styleProps['text-transform'] || inheritedFontStyles.textTransform || 'none';
252
+ const lineHeightAccessor = options.lineHeight;
253
+ const rotateAccessor = options.rotate;
254
+ const lineHeight = Number(
255
+ resolveProp<number, Datum>(lineHeightAccessor, datum.datum, 1.2)
256
+ );
257
+ const rotate =
258
+ (Number(resolveProp<number, Datum>(rotateAccessor, datum.datum, 0)) *
259
+ Math.PI) /
260
+ 180;
261
+
262
+ const textLines = String(resolveProp(options.text, datum.datum, ''))
263
+ .split('\n')
264
+ .map((line) => textTransform(line, textTransformValue));
265
+
266
+ const multilineOffset =
267
+ textLines.length > 1
268
+ ? (lineAnchor === 'bottom'
269
+ ? textLines.length - 1
270
+ : lineAnchor === 'middle'
271
+ ? (textLines.length - 1) * 0.5
272
+ : 0) *
273
+ fontSizePx *
274
+ lineHeight
275
+ : 0;
276
+
277
+ const opacity = maybeOpacity(styleProps['opacity']);
278
+ const fillOpacity = maybeOpacity(styleProps['fill-opacity']);
279
+ const strokeOpacity = maybeOpacity(styleProps['stroke-opacity']);
280
+
281
+ const fillValue = String(styleProps.fill || 'currentColor');
282
+ const strokeValue = String(styleProps.stroke || 'none');
283
+
284
+ const fill = resolveColor(fillValue, canvas);
285
+ const stroke = resolveColor(strokeValue, canvas);
286
+ const strokeWidth = toPixels(styleProps['stroke-width'], canvas, 1.6);
287
+
288
+ context.save();
289
+ context.translate(Math.round(x), Math.round(y - multilineOffset));
290
+ context.rotate(rotate);
291
+
292
+ context.font = `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize} ${fontFamily}`;
293
+ context.textAlign = normalizeTextAlign(styleProps['text-anchor']);
294
+ context.textBaseline = LINE_ANCHOR[lineAnchor];
295
+ context.lineWidth = strokeWidth;
296
+ context.lineCap = normalizeLineCap(styleProps['stroke-linecap']);
297
+ context.lineJoin = normalizeLineJoin(styleProps['stroke-linejoin']);
298
+
299
+ for (let index = 0; index < textLines.length; index += 1) {
300
+ const line = textLines[index];
301
+ const yOffset = index ? fontSizePx * lineHeight * index : 0;
302
+
303
+ if (stroke && stroke !== 'none') {
304
+ context.strokeStyle = stroke;
305
+ context.globalAlpha = opacity * strokeOpacity;
306
+ context.strokeText(line, 0, yOffset);
307
+ }
308
+
309
+ if (fill && fill !== 'none') {
310
+ context.fillStyle = fill;
311
+ context.globalAlpha = opacity * fillOpacity;
312
+ context.fillText(line, 0, yOffset);
313
+ }
314
+ }
315
+
316
+ context.restore();
317
+ }
318
+ }
319
+
320
+ return () => {
321
+ context?.clearRect(
322
+ 0,
323
+ 0,
324
+ plot.width * (devicePixelRatio.current ?? 1),
325
+ plot.height * (devicePixelRatio.current ?? 1)
326
+ );
327
+ };
328
+ });
329
+ };
330
+ </script>
331
+
332
+ <CanvasLayer {@attach render} />
@@ -0,0 +1,50 @@
1
+ import type * as CSS from 'csstype';
2
+ import type { BaseMarkProps, ChannelAccessor, ConstantAccessor, DataRecord, ScaledDataRecord, UsedScales } from '../../types/index.js';
3
+ interface TextCanvasProps<Datum extends DataRecord> {
4
+ data: ScaledDataRecord<Datum>[];
5
+ options: BaseMarkProps<Datum> & {
6
+ x?: ChannelAccessor<Datum>;
7
+ y?: ChannelAccessor<Datum>;
8
+ text: ConstantAccessor<string | null | false | undefined, Datum>;
9
+ title?: ConstantAccessor<string, Datum>;
10
+ fontFamily?: ConstantAccessor<CSS.Property.FontFamily, Datum>;
11
+ fontSize?: ConstantAccessor<CSS.Property.FontSize | number, Datum>;
12
+ fontWeight?: ConstantAccessor<CSS.Property.FontWeight, Datum>;
13
+ fontStyle?: ConstantAccessor<CSS.Property.FontStyle, Datum>;
14
+ fontVariant?: ConstantAccessor<CSS.Property.FontVariant, Datum>;
15
+ letterSpacing?: ConstantAccessor<CSS.Property.LetterSpacing, Datum>;
16
+ wordSpacing?: ConstantAccessor<CSS.Property.WordSpacing, Datum>;
17
+ textTransform?: ConstantAccessor<CSS.Property.TextTransform, Datum>;
18
+ textDecoration?: ConstantAccessor<CSS.Property.TextDecoration, Datum>;
19
+ textAnchor?: ConstantAccessor<CSS.Property.TextAnchor, Datum>;
20
+ lineAnchor?: ConstantAccessor<'bottom' | 'top' | 'middle'>;
21
+ lineHeight?: ConstantAccessor<number, Datum>;
22
+ frameAnchor?: ConstantAccessor<'bottom' | 'top' | 'left' | 'right' | 'top-left' | 'bottom-left' | 'top-right' | 'bottom-right' | 'middle', Datum>;
23
+ rotate?: ConstantAccessor<number, Datum>;
24
+ };
25
+ usedScales: UsedScales;
26
+ }
27
+ declare function $$render<Datum extends DataRecord>(): {
28
+ props: TextCanvasProps<Datum>;
29
+ exports: {};
30
+ bindings: "";
31
+ slots: {};
32
+ events: {};
33
+ };
34
+ declare class __sveltets_Render<Datum extends DataRecord> {
35
+ props(): ReturnType<typeof $$render<Datum>>['props'];
36
+ events(): ReturnType<typeof $$render<Datum>>['events'];
37
+ slots(): ReturnType<typeof $$render<Datum>>['slots'];
38
+ bindings(): "";
39
+ exports(): {};
40
+ }
41
+ interface $$IsomorphicComponent {
42
+ new <Datum extends DataRecord>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Datum>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Datum>['props']>, ReturnType<__sveltets_Render<Datum>['events']>, ReturnType<__sveltets_Render<Datum>['slots']>> & {
43
+ $$bindings?: ReturnType<__sveltets_Render<Datum>['bindings']>;
44
+ } & ReturnType<__sveltets_Render<Datum>['exports']>;
45
+ <Datum extends DataRecord>(internal: unknown, props: ReturnType<__sveltets_Render<Datum>['props']> & {}): ReturnType<__sveltets_Render<Datum>['exports']>;
46
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
47
+ }
48
+ declare const TextCanvas: $$IsomorphicComponent;
49
+ type TextCanvas<Datum extends DataRecord> = InstanceType<typeof TextCanvas<Datum>>;
50
+ export default TextCanvas;
@@ -0,0 +1,214 @@
1
+ <script lang="ts" generics="Datum extends DataRecord">
2
+ interface TickCanvasProps<Datum extends DataRecord> {
3
+ data: ScaledDataRecord<Datum>[];
4
+ options: BaseMarkProps<Datum> & {
5
+ x?: ChannelAccessor<Datum>;
6
+ y?: ChannelAccessor<Datum>;
7
+ inset?: ConstantAccessor<number | string, Datum>;
8
+ tickLength?: ConstantAccessor<number, Datum>;
9
+ };
10
+ usedScales: UsedScales;
11
+ orientation: 'vertical' | 'horizontal';
12
+ }
13
+
14
+ import type {
15
+ BaseMarkProps,
16
+ ChannelAccessor,
17
+ ConstantAccessor,
18
+ DataRecord,
19
+ ScaledDataRecord,
20
+ UsedScales
21
+ } from '../../types/index.js';
22
+ import { resolveProp, resolveScaledStyleProps } from '../../helpers/resolve.js';
23
+ import { parseInset } from '../../helpers/index.js';
24
+ import type { Attachment } from 'svelte/attachments';
25
+ import { devicePixelRatio } from 'svelte/reactivity/window';
26
+ import CanvasLayer from './CanvasLayer.svelte';
27
+ import { resolveColor } from './canvas.js';
28
+ import { usePlot } from '../../hooks/usePlot.svelte.js';
29
+
30
+ const plot = usePlot();
31
+
32
+ let { data, options, usedScales, orientation }: TickCanvasProps<Datum> = $props();
33
+
34
+ function maybeOpacity(value: unknown) {
35
+ return value == null ? 1 : +value;
36
+ }
37
+
38
+ function normalizeLineCap(value: unknown): CanvasLineCap {
39
+ return value === 'round' || value === 'square' || value === 'butt' ? value : 'butt';
40
+ }
41
+
42
+ const render: Attachment = (canvasEl: Element) => {
43
+ const canvas = canvasEl as HTMLCanvasElement;
44
+ const context = canvas.getContext('2d');
45
+
46
+ $effect(() => {
47
+ if (context) {
48
+ const yUsesBand = usedScales.y && plot.scales.y.type === 'band';
49
+ const xUsesBand = usedScales.x && plot.scales.x.type === 'band';
50
+ const yBandwidth = yUsesBand ? plot.scales.y.fn.bandwidth() : 0;
51
+ const xBandwidth = xUsesBand ? plot.scales.x.fn.bandwidth() : 0;
52
+ const hasYChannel = options.y != null;
53
+ const hasXChannel = options.x != null;
54
+ const marginTop = plot.options.marginTop;
55
+ const marginLeft = plot.options.marginLeft;
56
+ const fullY2 = marginTop + plot.plotHeight;
57
+ const fullX2 = marginLeft + plot.facetWidth;
58
+
59
+ context.resetTransform();
60
+ context.scale(devicePixelRatio.current ?? 1, devicePixelRatio.current ?? 1);
61
+ context.beginPath();
62
+
63
+ let currentStyle: {
64
+ key: string;
65
+ stroke: string;
66
+ lineCap: CanvasLineCap;
67
+ lineWidth: number;
68
+ alpha: number;
69
+ } | null = null;
70
+ let hasCurrentSegments = false;
71
+ const resolvedStrokeCache: Record<
72
+ string,
73
+ ReturnType<typeof resolveColor>
74
+ > = Object.create(null);
75
+ let currentTickLength = 10;
76
+ let currentInsetValue: number | string = 0;
77
+
78
+ const flushPath = () => {
79
+ if (!currentStyle || !hasCurrentSegments) return;
80
+
81
+ let resolvedStroke = resolvedStrokeCache[currentStyle.stroke];
82
+ if (!(currentStyle.stroke in resolvedStrokeCache)) {
83
+ resolvedStroke = resolveColor(currentStyle.stroke, canvas);
84
+ resolvedStrokeCache[currentStyle.stroke] = resolvedStroke;
85
+ }
86
+ if (resolvedStroke && resolvedStroke !== 'none') {
87
+ context.lineCap = currentStyle.lineCap;
88
+ context.lineWidth = currentStyle.lineWidth;
89
+ context.strokeStyle = resolvedStroke;
90
+ context.globalAlpha = currentStyle.alpha;
91
+ context.stroke();
92
+ }
93
+
94
+ context.beginPath();
95
+ hasCurrentSegments = false;
96
+ };
97
+
98
+ const prepareStyle = (datum: ScaledDataRecord<Datum>) => {
99
+ let { stroke, ...restStyles } = resolveScaledStyleProps(
100
+ datum.datum,
101
+ options,
102
+ usedScales,
103
+ plot,
104
+ 'stroke'
105
+ );
106
+
107
+ const opacity = maybeOpacity(restStyles['opacity']);
108
+ const strokeOpacity = maybeOpacity(restStyles['stroke-opacity']);
109
+ const lineCap = normalizeLineCap(restStyles['stroke-linecap']);
110
+ const strokeWidth = +(resolveProp(
111
+ options.strokeWidth,
112
+ datum.datum,
113
+ 1
114
+ ) as number);
115
+ const tickLength = +(resolveProp(
116
+ options.tickLength,
117
+ datum.datum,
118
+ 10
119
+ ) as number);
120
+ const insetValue = resolveProp(options.inset, datum.datum, 0) as
121
+ | number
122
+ | string;
123
+
124
+ const strokeValue = String(stroke || 'currentColor');
125
+ const alpha = opacity * strokeOpacity;
126
+ const styleKey = `${strokeValue}|${lineCap}|${strokeWidth}|${alpha}`;
127
+ if (!currentStyle || currentStyle.key !== styleKey) {
128
+ flushPath();
129
+ currentStyle = {
130
+ key: styleKey,
131
+ stroke: strokeValue,
132
+ lineCap,
133
+ lineWidth: strokeWidth,
134
+ alpha
135
+ };
136
+ }
137
+
138
+ currentTickLength = tickLength;
139
+ currentInsetValue = insetValue;
140
+ };
141
+
142
+ if (orientation === 'vertical') {
143
+ for (const datum of data) {
144
+ if (!datum.valid) continue;
145
+ prepareStyle(datum);
146
+
147
+ const x = datum.x;
148
+ if (x == null) continue;
149
+
150
+ let y1: number;
151
+ let y2: number;
152
+
153
+ if (hasYChannel) {
154
+ const y = datum.y;
155
+ if (y == null) continue;
156
+
157
+ y1 = y - yBandwidth * 0.5;
158
+ y2 = y + yBandwidth * 0.5;
159
+ } else {
160
+ y1 = marginTop + (datum.dy ?? 0);
161
+ y2 = fullY2 + (datum.dy ?? 0);
162
+ }
163
+
164
+ const inset = parseInset(currentInsetValue, Math.abs(y2 - y1));
165
+ const singlePoint = y1 === y2;
166
+ context.moveTo(x, y1 + inset + (singlePoint ? currentTickLength * 0.5 : 0));
167
+ context.lineTo(x, y2 - inset - (singlePoint ? currentTickLength * 0.5 : 0));
168
+ hasCurrentSegments = true;
169
+ }
170
+ } else {
171
+ for (const datum of data) {
172
+ if (!datum.valid) continue;
173
+ prepareStyle(datum);
174
+
175
+ const y = datum.y;
176
+ if (y == null) continue;
177
+
178
+ let x1: number;
179
+ let x2: number;
180
+
181
+ if (hasXChannel) {
182
+ const x = datum.x;
183
+ if (x == null) continue;
184
+
185
+ x1 = x - xBandwidth * 0.5;
186
+ x2 = x + xBandwidth * 0.5;
187
+ } else {
188
+ x1 = marginLeft + (datum.dx ?? 0);
189
+ x2 = fullX2 + (datum.dx ?? 0);
190
+ }
191
+
192
+ const inset = parseInset(currentInsetValue, Math.abs(x2 - x1));
193
+ const singlePoint = x1 === x2;
194
+ context.moveTo(x1 + inset + (singlePoint ? currentTickLength * 0.5 : 0), y);
195
+ context.lineTo(x2 - inset - (singlePoint ? currentTickLength * 0.5 : 0), y);
196
+ hasCurrentSegments = true;
197
+ }
198
+ }
199
+ flushPath();
200
+ }
201
+
202
+ return () => {
203
+ context?.clearRect(
204
+ 0,
205
+ 0,
206
+ plot.width * (devicePixelRatio.current ?? 1),
207
+ plot.height * (devicePixelRatio.current ?? 1)
208
+ );
209
+ };
210
+ });
211
+ };
212
+ </script>
213
+
214
+ <CanvasLayer {@attach render} />
@@ -0,0 +1,36 @@
1
+ import type { BaseMarkProps, ChannelAccessor, ConstantAccessor, DataRecord, ScaledDataRecord, UsedScales } from '../../types/index.js';
2
+ interface TickCanvasProps<Datum extends DataRecord> {
3
+ data: ScaledDataRecord<Datum>[];
4
+ options: BaseMarkProps<Datum> & {
5
+ x?: ChannelAccessor<Datum>;
6
+ y?: ChannelAccessor<Datum>;
7
+ inset?: ConstantAccessor<number | string, Datum>;
8
+ tickLength?: ConstantAccessor<number, Datum>;
9
+ };
10
+ usedScales: UsedScales;
11
+ orientation: 'vertical' | 'horizontal';
12
+ }
13
+ declare function $$render<Datum extends DataRecord>(): {
14
+ props: TickCanvasProps<Datum>;
15
+ exports: {};
16
+ bindings: "";
17
+ slots: {};
18
+ events: {};
19
+ };
20
+ declare class __sveltets_Render<Datum extends DataRecord> {
21
+ props(): ReturnType<typeof $$render<Datum>>['props'];
22
+ events(): ReturnType<typeof $$render<Datum>>['events'];
23
+ slots(): ReturnType<typeof $$render<Datum>>['slots'];
24
+ bindings(): "";
25
+ exports(): {};
26
+ }
27
+ interface $$IsomorphicComponent {
28
+ new <Datum extends DataRecord>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Datum>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Datum>['props']>, ReturnType<__sveltets_Render<Datum>['events']>, ReturnType<__sveltets_Render<Datum>['slots']>> & {
29
+ $$bindings?: ReturnType<__sveltets_Render<Datum>['bindings']>;
30
+ } & ReturnType<__sveltets_Render<Datum>['exports']>;
31
+ <Datum extends DataRecord>(internal: unknown, props: ReturnType<__sveltets_Render<Datum>['props']> & {}): ReturnType<__sveltets_Render<Datum>['exports']>;
32
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
33
+ }
34
+ declare const TickCanvas: $$IsomorphicComponent;
35
+ type TickCanvas<Datum extends DataRecord> = InstanceType<typeof TickCanvas<Datum>>;
36
+ export default TickCanvas;
@@ -23,6 +23,10 @@ export type MarkerOptions = {
23
23
  * shorthand for setting the marker on all points
24
24
  */
25
25
  marker?: boolean | MarkerShape | Snippet;
26
+ /**
27
+ * scale factor for marker size, relative to the line stroke width
28
+ */
29
+ markerScale?: ConstantAccessor<number>;
26
30
  };
27
31
  export type ConstantAccessor<K, T = Record<string | symbol, RawValue>> = K | ((d: T, index: number) => K) | null | undefined;
28
32
  export type TransformArg<T> = Channels<T> & BaseMarkProps<T> & {