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.
- package/dist/marks/Line.svelte +2 -0
- package/dist/marks/Link.svelte +2 -0
- package/dist/marks/RuleX.svelte +39 -15
- package/dist/marks/RuleX.svelte.d.ts +1 -0
- package/dist/marks/RuleY.svelte +39 -16
- package/dist/marks/RuleY.svelte.d.ts +1 -0
- package/dist/marks/Text.svelte +49 -20
- package/dist/marks/Text.svelte.d.ts +114 -3
- package/dist/marks/TickX.svelte +40 -33
- package/dist/marks/TickX.svelte.d.ts +1 -0
- package/dist/marks/TickY.svelte +40 -33
- package/dist/marks/TickY.svelte.d.ts +1 -0
- package/dist/marks/Vector.svelte +6 -10
- package/dist/marks/helpers/Marker.svelte +5 -4
- package/dist/marks/helpers/Marker.svelte.d.ts +1 -0
- package/dist/marks/helpers/MarkerPath.svelte +8 -1
- package/dist/marks/helpers/MarkerPath.svelte.d.ts +4 -0
- package/dist/marks/helpers/RuleCanvas.svelte +175 -0
- package/dist/marks/helpers/RuleCanvas.svelte.d.ts +41 -0
- package/dist/marks/helpers/TextCanvas.svelte +332 -0
- package/dist/marks/helpers/TextCanvas.svelte.d.ts +50 -0
- package/dist/marks/helpers/TickCanvas.svelte +214 -0
- package/dist/marks/helpers/TickCanvas.svelte.d.ts +36 -0
- package/dist/types/index.d.ts +4 -0
- package/package.json +151 -152
|
@@ -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;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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> & {
|