layerchart 2.0.0-next.7 → 2.0.0-next.9
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/components/Axis.svelte +43 -5
- package/dist/components/Axis.svelte.d.ts +10 -0
- package/dist/components/Chart.svelte +2 -2
- package/dist/components/Spline.svelte +1 -1
- package/dist/components/Text.svelte +56 -51
- package/dist/components/Text.svelte.d.ts +6 -0
- package/dist/components/charts/AreaChart.svelte +2 -8
- package/dist/components/charts/BarChart.svelte +61 -52
- package/dist/components/charts/LineChart.svelte +0 -3
- package/dist/components/charts/ScatterChart.svelte +0 -2
- package/dist/components/charts/utils.svelte.d.ts +2 -2
- package/dist/docs/Code.svelte +4 -4
- package/dist/docs/Code.svelte.d.ts +4 -4
- package/dist/docs/Json.svelte +10 -1
- package/dist/docs/Json.svelte.d.ts +8 -5
- package/dist/docs/TilesetField.svelte +20 -19
- package/dist/docs/TilesetField.svelte.d.ts +5 -22
- package/dist/utils/arcText.svelte.js +4 -4
- package/dist/utils/scales.svelte.d.ts +3 -2
- package/dist/utils/scales.svelte.js +7 -3
- package/dist/utils/ticks.d.ts +4 -4
- package/dist/utils/ticks.js +106 -159
- package/dist/utils/ticks.test.js +6 -16
- package/package.json +8 -8
|
@@ -43,6 +43,18 @@
|
|
|
43
43
|
*/
|
|
44
44
|
ticks?: TicksConfig;
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Width or height of each tick in pxiels (responsive reduce)
|
|
48
|
+
*/
|
|
49
|
+
tickSpacing?: number;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether to render tick labels on multiple lines for additional context
|
|
53
|
+
*
|
|
54
|
+
* @default false
|
|
55
|
+
*/
|
|
56
|
+
tickMultiline?: boolean;
|
|
57
|
+
|
|
46
58
|
/**
|
|
47
59
|
* Length of the tick line
|
|
48
60
|
* @default 4
|
|
@@ -114,7 +126,7 @@
|
|
|
114
126
|
import { extent } from 'd3-array';
|
|
115
127
|
import { pointRadial } from 'd3-shape';
|
|
116
128
|
|
|
117
|
-
import {
|
|
129
|
+
import { type FormatType } from '@layerstack/utils';
|
|
118
130
|
import { cls } from '@layerstack/tailwind';
|
|
119
131
|
|
|
120
132
|
import Group, { type GroupProps } from './Group.svelte';
|
|
@@ -126,7 +138,7 @@
|
|
|
126
138
|
import { getChartContext } from './Chart.svelte';
|
|
127
139
|
import { extractLayerProps, layerClass } from '../utils/attributes.js';
|
|
128
140
|
import { type MotionProp } from '../utils/motion.svelte.js';
|
|
129
|
-
import { resolveTickVals, type TicksConfig } from '../utils/ticks.js';
|
|
141
|
+
import { resolveTickFormat, resolveTickVals, type TicksConfig } from '../utils/ticks.js';
|
|
130
142
|
|
|
131
143
|
let {
|
|
132
144
|
placement,
|
|
@@ -136,6 +148,12 @@
|
|
|
136
148
|
rule = false,
|
|
137
149
|
grid = false,
|
|
138
150
|
ticks,
|
|
151
|
+
tickSpacing = ['top', 'bottom', 'angle'].includes(placement)
|
|
152
|
+
? 80
|
|
153
|
+
: ['left', 'right', 'radius'].includes(placement)
|
|
154
|
+
? 50
|
|
155
|
+
: undefined,
|
|
156
|
+
tickMultiline = false,
|
|
139
157
|
tickLength = 4,
|
|
140
158
|
tickMarks = true,
|
|
141
159
|
format,
|
|
@@ -169,7 +187,27 @@
|
|
|
169
187
|
const xRangeMinMax = $derived(extent<number>(ctx.xRange)) as [number, number];
|
|
170
188
|
const yRangeMinMax = $derived(extent<number>(ctx.yRange)) as [number, number];
|
|
171
189
|
|
|
172
|
-
const
|
|
190
|
+
const ctxSize = $derived(
|
|
191
|
+
orientation === 'vertical'
|
|
192
|
+
? ctx.height
|
|
193
|
+
: orientation === 'horizontal'
|
|
194
|
+
? ctx.width
|
|
195
|
+
: orientation === 'radius'
|
|
196
|
+
? ctx.height / 2
|
|
197
|
+
: orientation === 'angle'
|
|
198
|
+
? ctx.width
|
|
199
|
+
: null
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const tickCount = $derived(
|
|
203
|
+
typeof ticks === 'number'
|
|
204
|
+
? ticks
|
|
205
|
+
: tickSpacing && ctxSize
|
|
206
|
+
? Math.round(ctxSize / tickSpacing)
|
|
207
|
+
: undefined
|
|
208
|
+
);
|
|
209
|
+
const tickVals = $derived(resolveTickVals(scale, ticks, tickCount));
|
|
210
|
+
const tickFormat = $derived(resolveTickFormat(scale, ticks, tickCount, format, tickMultiline));
|
|
173
211
|
|
|
174
212
|
function getCoords(tick: any) {
|
|
175
213
|
switch (placement) {
|
|
@@ -356,7 +394,7 @@
|
|
|
356
394
|
<Text {...resolvedLabelProps} />
|
|
357
395
|
{/if}
|
|
358
396
|
|
|
359
|
-
{#each tickVals as tick, index (tick
|
|
397
|
+
{#each tickVals as tick, index (tick)}
|
|
360
398
|
{@const tickCoords = getCoords(tick)}
|
|
361
399
|
{@const [radialTickCoordsX, radialTickCoordsY] = pointRadial(tickCoords.x, tickCoords.y)}
|
|
362
400
|
{@const [radialTickMarkCoordsX, radialTickMarkCoordsY] = pointRadial(
|
|
@@ -366,7 +404,7 @@
|
|
|
366
404
|
{@const resolvedTickLabelProps = {
|
|
367
405
|
x: orientation === 'angle' ? radialTickCoordsX : tickCoords.x,
|
|
368
406
|
y: orientation === 'angle' ? radialTickCoordsY : tickCoords.y,
|
|
369
|
-
value:
|
|
407
|
+
value: tickFormat(tick, index),
|
|
370
408
|
...getDefaultTickLabelProps(tick),
|
|
371
409
|
motion,
|
|
372
410
|
...tickLabelProps,
|
|
@@ -36,6 +36,16 @@ export type AxisPropsWithoutHTML<In extends Transition = Transition> = {
|
|
|
36
36
|
* Control the number of ticks
|
|
37
37
|
*/
|
|
38
38
|
ticks?: TicksConfig;
|
|
39
|
+
/**
|
|
40
|
+
* Width or height of each tick in pxiels (responsive reduce)
|
|
41
|
+
*/
|
|
42
|
+
tickSpacing?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Whether to render tick labels on multiple lines for additional context
|
|
45
|
+
*
|
|
46
|
+
* @default false
|
|
47
|
+
*/
|
|
48
|
+
tickMultiline?: boolean;
|
|
39
49
|
/**
|
|
40
50
|
* Length of the tick line
|
|
41
51
|
* @default 4
|
|
@@ -844,12 +844,12 @@
|
|
|
844
844
|
if (verbose === true) {
|
|
845
845
|
if (width <= 0 && isMounted === true) {
|
|
846
846
|
console.warn(
|
|
847
|
-
|
|
847
|
+
`[LayerChart] Target div has zero or negative width (${width}). Did you forget to set an explicit width in CSS on the container?`
|
|
848
848
|
);
|
|
849
849
|
}
|
|
850
850
|
if (height <= 0 && isMounted === true) {
|
|
851
851
|
console.warn(
|
|
852
|
-
|
|
852
|
+
`[LayerChart] Target div has zero or negative height (${height}). Did you forget to set an explicit height in CSS on the container?`
|
|
853
853
|
);
|
|
854
854
|
}
|
|
855
855
|
}
|
|
@@ -97,6 +97,20 @@
|
|
|
97
97
|
*/
|
|
98
98
|
verticalAnchor?: 'start' | 'middle' | 'end' | 'inherit';
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The dominant baseline of the text. Useful for aligning text to the baseline of the axis.
|
|
102
|
+
*
|
|
103
|
+
* @default 'auto'
|
|
104
|
+
*/
|
|
105
|
+
dominantBaseline?:
|
|
106
|
+
| 'auto'
|
|
107
|
+
| 'text-before-edge'
|
|
108
|
+
| 'text-after-edge'
|
|
109
|
+
| 'middle'
|
|
110
|
+
| 'hanging'
|
|
111
|
+
| 'ideographic'
|
|
112
|
+
| 'mathematical';
|
|
113
|
+
|
|
100
114
|
/**
|
|
101
115
|
* Rotational angle of the text
|
|
102
116
|
*/
|
|
@@ -178,20 +192,6 @@
|
|
|
178
192
|
import { degreesToRadians } from '../utils/math.js';
|
|
179
193
|
import { createId } from '../utils/createId.js';
|
|
180
194
|
|
|
181
|
-
/*
|
|
182
|
-
TODO:
|
|
183
|
-
- [ ] Handle styled text (use <slot /> to measure?)
|
|
184
|
-
- [ ] Simplify by using `alignment-baseline` / `dominant-baseline`, rework multiline or drop support, etc
|
|
185
|
-
- https://svelte.dev/repl/f12d3003313a43ba8a0be53e5786f1c7?version=3.44.3
|
|
186
|
-
- https://observablehq.com/@neocartocnrs/cheat-sheet-on-texts-in-svg
|
|
187
|
-
|
|
188
|
-
Reference:
|
|
189
|
-
- https://bl.ocks.org/mbostock/7555321
|
|
190
|
-
- https://github.com/airbnb/visx/blob/master/packages/visx-text/src/Text.tsx
|
|
191
|
-
- https://airbnb.io/visx/text
|
|
192
|
-
- https://github.com/airbnb/visx/blob/master/packages/visx-demo/src/pages/text.tsx
|
|
193
|
-
*/
|
|
194
|
-
|
|
195
195
|
const uid = $props.id();
|
|
196
196
|
|
|
197
197
|
let {
|
|
@@ -208,6 +208,7 @@
|
|
|
208
208
|
scaleToFit = false,
|
|
209
209
|
textAnchor = 'start',
|
|
210
210
|
verticalAnchor = 'end',
|
|
211
|
+
dominantBaseline = 'auto',
|
|
211
212
|
rotate,
|
|
212
213
|
opacity = 1,
|
|
213
214
|
strokeWidth = 0,
|
|
@@ -227,6 +228,8 @@
|
|
|
227
228
|
...restProps
|
|
228
229
|
}: TextProps = $props();
|
|
229
230
|
|
|
231
|
+
const renderCtx = getRenderContext();
|
|
232
|
+
|
|
230
233
|
let ref = $state<SVGTextElement>();
|
|
231
234
|
let svgRef = $state<SVGElement>();
|
|
232
235
|
let pathRef = $state<SVGPathElement>();
|
|
@@ -260,49 +263,49 @@
|
|
|
260
263
|
};
|
|
261
264
|
});
|
|
262
265
|
|
|
263
|
-
|
|
266
|
+
// Handle null and convert `\n` strings back to newline characters
|
|
267
|
+
const rawText = $derived(value != null ? value.toString().replace(/\\n/g, '\n') : '');
|
|
264
268
|
|
|
265
269
|
const textValue = $derived.by(() => {
|
|
266
270
|
if (!truncateConfig) return rawText;
|
|
267
271
|
return truncateText(rawText, truncateConfig);
|
|
268
272
|
});
|
|
269
273
|
|
|
270
|
-
const renderCtx = getRenderContext();
|
|
271
|
-
|
|
272
|
-
const words = $derived(textValue ? textValue.split(/(?:(?!\u00A0+)\s+)/) : []);
|
|
273
|
-
|
|
274
|
-
const wordsWithWidth = $derived(
|
|
275
|
-
words.map((word) => ({
|
|
276
|
-
word,
|
|
277
|
-
width: getStringWidth(word, style) || 0,
|
|
278
|
-
}))
|
|
279
|
-
);
|
|
280
|
-
|
|
281
274
|
const spaceWidth = $derived(getStringWidth('\u00A0', style) || 0);
|
|
282
275
|
|
|
283
|
-
const wordsByLines = $derived(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
currentLine
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
276
|
+
const wordsByLines = $derived.by(() => {
|
|
277
|
+
// Split by newlines to preserve explicit line breaks
|
|
278
|
+
const lines = textValue.split('\n');
|
|
279
|
+
|
|
280
|
+
return lines.flatMap((line) => {
|
|
281
|
+
// Split each line into words
|
|
282
|
+
const words = line.split(/(?:(?!\u00A0+)\s+)/);
|
|
283
|
+
|
|
284
|
+
// Handle word wrapping within each line
|
|
285
|
+
return words.reduce((result: { words: string[]; width?: number }[], item) => {
|
|
286
|
+
const currentLine = result[result.length - 1];
|
|
287
|
+
const itemWidth = getStringWidth(item, style) || 0;
|
|
288
|
+
|
|
289
|
+
if (
|
|
290
|
+
currentLine &&
|
|
291
|
+
(width == null || scaleToFit || (currentLine.width || 0) + itemWidth + spaceWidth < width)
|
|
292
|
+
) {
|
|
293
|
+
// Word can be added to an existing line
|
|
294
|
+
currentLine.words.push(item);
|
|
295
|
+
currentLine.width = currentLine.width || 0;
|
|
296
|
+
currentLine.width += itemWidth + spaceWidth;
|
|
297
|
+
} else {
|
|
298
|
+
// Add first word to line or word is too long to scaleToFit on existing line
|
|
299
|
+
const newLine = { words: [item], width: itemWidth };
|
|
300
|
+
result.push(newLine);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return result;
|
|
304
|
+
}, []);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
304
307
|
|
|
305
|
-
const
|
|
308
|
+
const lineCount = $derived(wordsByLines.length);
|
|
306
309
|
|
|
307
310
|
/**
|
|
308
311
|
* Convert css value to pixel value (ex. 0.71em => 11.36)
|
|
@@ -329,9 +332,9 @@
|
|
|
329
332
|
if (verticalAnchor === 'start') {
|
|
330
333
|
return getPixelValue(capHeight);
|
|
331
334
|
} else if (verticalAnchor === 'middle') {
|
|
332
|
-
return ((
|
|
335
|
+
return ((lineCount - 1) / 2) * -getPixelValue(lineHeight) + getPixelValue(capHeight) / 2;
|
|
333
336
|
} else {
|
|
334
|
-
return (
|
|
337
|
+
return (lineCount - 1) * -getPixelValue(lineHeight);
|
|
335
338
|
}
|
|
336
339
|
});
|
|
337
340
|
|
|
@@ -348,7 +351,7 @@
|
|
|
348
351
|
const scaleTransform = $derived.by(() => {
|
|
349
352
|
if (
|
|
350
353
|
scaleToFit &&
|
|
351
|
-
|
|
354
|
+
lineCount > 0 &&
|
|
352
355
|
typeof x == 'number' &&
|
|
353
356
|
typeof y == 'number' &&
|
|
354
357
|
typeof width == 'number'
|
|
@@ -500,6 +503,7 @@
|
|
|
500
503
|
>
|
|
501
504
|
<textPath
|
|
502
505
|
style="text-anchor: {textAnchor};"
|
|
506
|
+
dominant-baseline={dominantBaseline}
|
|
503
507
|
href="#{pathId}"
|
|
504
508
|
{startOffset}
|
|
505
509
|
class={cls(layerClass('text-path'))}
|
|
@@ -514,6 +518,7 @@
|
|
|
514
518
|
y={motionY.current}
|
|
515
519
|
{transform}
|
|
516
520
|
text-anchor={textAnchor}
|
|
521
|
+
dominant-baseline={dominantBaseline}
|
|
517
522
|
{...restProps}
|
|
518
523
|
{fill}
|
|
519
524
|
fill-opacity={fillOpacity}
|
|
@@ -80,6 +80,12 @@ export type TextPropsWithoutHTML = {
|
|
|
80
80
|
* @default 'end'
|
|
81
81
|
*/
|
|
82
82
|
verticalAnchor?: 'start' | 'middle' | 'end' | 'inherit';
|
|
83
|
+
/**
|
|
84
|
+
* The dominant baseline of the text. Useful for aligning text to the baseline of the axis.
|
|
85
|
+
*
|
|
86
|
+
* @default 'auto'
|
|
87
|
+
*/
|
|
88
|
+
dominantBaseline?: 'auto' | 'text-before-edge' | 'text-after-edge' | 'middle' | 'hanging' | 'ideographic' | 'mathematical';
|
|
83
89
|
/**
|
|
84
90
|
* Rotational angle of the text
|
|
85
91
|
*/
|
|
@@ -398,13 +398,8 @@
|
|
|
398
398
|
if (axisDirection === 'y') {
|
|
399
399
|
return {
|
|
400
400
|
placement: radial ? 'radius' : 'left',
|
|
401
|
-
format:
|
|
402
|
-
|
|
403
|
-
return format(value, 'percentRound');
|
|
404
|
-
} else {
|
|
405
|
-
return format(value, undefined, { variant: 'short' });
|
|
406
|
-
}
|
|
407
|
-
},
|
|
401
|
+
format:
|
|
402
|
+
seriesLayout === 'stackExpand' ? (value) => format(value, 'percentRound') : undefined,
|
|
408
403
|
...(typeof axis === 'object' ? axis : null),
|
|
409
404
|
...props.yAxis,
|
|
410
405
|
};
|
|
@@ -412,7 +407,6 @@
|
|
|
412
407
|
|
|
413
408
|
return {
|
|
414
409
|
placement: radial ? 'angle' : 'bottom',
|
|
415
|
-
format: (value) => format(value, undefined, { variant: 'short' }),
|
|
416
410
|
...(typeof axis === 'object' ? axis : null),
|
|
417
411
|
...props.xAxis,
|
|
418
412
|
};
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
|
|
85
85
|
<script lang="ts" generics="TData">
|
|
86
86
|
import { onMount, type ComponentProps } from 'svelte';
|
|
87
|
-
import { scaleBand, scaleLinear } from 'd3-scale';
|
|
87
|
+
import { scaleBand, scaleLinear, scaleTime } from 'd3-scale';
|
|
88
88
|
import { stack, stackOffsetDiverging, stackOffsetExpand, stackOffsetNone } from 'd3-shape';
|
|
89
89
|
import { format } from '@layerstack/utils';
|
|
90
90
|
import { cls } from '@layerstack/tailwind';
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
import { asAny } from '../../utils/types.js';
|
|
110
110
|
import type { Insets } from '../../utils/rect.svelte.js';
|
|
111
111
|
import type { SeriesData, SimplifiedChartProps, SimplifiedChartPropsObject } from './types.js';
|
|
112
|
-
import type
|
|
112
|
+
import { isScaleTime, type AnyScale } from '../../utils/scales.svelte.js';
|
|
113
113
|
import { createLegendProps, SeriesState } from './utils.svelte.js';
|
|
114
114
|
import { setTooltipMetaContext } from '../tooltip/tooltipMetaContext.js';
|
|
115
115
|
import DefaultTooltip from './DefaultTooltip.svelte';
|
|
@@ -171,15 +171,56 @@
|
|
|
171
171
|
const isStackSeries = $derived(seriesLayout.startsWith('stack'));
|
|
172
172
|
const isGroupSeries = $derived(seriesLayout === 'group');
|
|
173
173
|
|
|
174
|
+
const chartData: Array<TData & { stackData?: any }> = $derived.by(() => {
|
|
175
|
+
let _chartData = (
|
|
176
|
+
seriesState.allSeriesData.length ? seriesState.allSeriesData : chartDataArray(data)
|
|
177
|
+
) as Array<TData & { stackData?: any }>;
|
|
178
|
+
if (isStackSeries) {
|
|
179
|
+
const seriesKeys = seriesState.visibleSeries.map((s) => s.key);
|
|
180
|
+
|
|
181
|
+
const offset =
|
|
182
|
+
seriesLayout === 'stackExpand'
|
|
183
|
+
? stackOffsetExpand
|
|
184
|
+
: seriesLayout === 'stackDiverging'
|
|
185
|
+
? stackOffsetDiverging
|
|
186
|
+
: stackOffsetNone;
|
|
187
|
+
const stackData = stack()
|
|
188
|
+
.keys(seriesKeys)
|
|
189
|
+
.value((d, key) => {
|
|
190
|
+
const s = series.find((d) => d.key === key)!;
|
|
191
|
+
return accessor(s.value ?? s.key)(d as any);
|
|
192
|
+
})
|
|
193
|
+
.offset(offset)(chartDataArray(data)) as any[];
|
|
194
|
+
|
|
195
|
+
_chartData = _chartData.map((d, i) => {
|
|
196
|
+
return {
|
|
197
|
+
...d,
|
|
198
|
+
stackData: stackData.map((sd) => sd[i]),
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return _chartData;
|
|
203
|
+
});
|
|
204
|
+
|
|
174
205
|
const xScale = $derived(
|
|
175
|
-
xScaleProp ??
|
|
206
|
+
xScaleProp ??
|
|
207
|
+
(isVertical
|
|
208
|
+
? scaleBand().padding(bandPadding)
|
|
209
|
+
: accessor(xProp)(chartData[0]) instanceof Date // TODO: also check for Array<Date> instances (ex. x={['start', 'end']})
|
|
210
|
+
? scaleTime()
|
|
211
|
+
: scaleLinear())
|
|
176
212
|
);
|
|
177
|
-
const xBaseline = $derived(isVertical ? undefined : 0);
|
|
213
|
+
const xBaseline = $derived(isVertical || isScaleTime(xScale) ? undefined : 0);
|
|
178
214
|
|
|
179
215
|
const yScale = $derived(
|
|
180
|
-
yScaleProp ??
|
|
216
|
+
yScaleProp ??
|
|
217
|
+
(isVertical
|
|
218
|
+
? accessor(yProp)(chartData[0]) instanceof Date // TODO: also check for Array<Date> instances (ex. y={['start', 'end']})
|
|
219
|
+
? scaleTime()
|
|
220
|
+
: scaleLinear()
|
|
221
|
+
: scaleBand().padding(bandPadding))
|
|
181
222
|
);
|
|
182
|
-
const yBaseline = $derived(isVertical ? 0 : undefined);
|
|
223
|
+
const yBaseline = $derived(isVertical || isScaleTime(yScale) ? 0 : undefined);
|
|
183
224
|
|
|
184
225
|
const x1Scale = $derived(
|
|
185
226
|
isGroupSeries && isVertical ? scaleBand().padding(groupPadding) : undefined
|
|
@@ -214,37 +255,6 @@
|
|
|
214
255
|
return d && typeof d === 'object' && 'stackData' in d;
|
|
215
256
|
}
|
|
216
257
|
|
|
217
|
-
const chartData: Array<TData & { stackData?: any }> = $derived.by(() => {
|
|
218
|
-
let _chartData = (
|
|
219
|
-
seriesState.allSeriesData.length ? seriesState.allSeriesData : chartDataArray(data)
|
|
220
|
-
) as Array<TData & { stackData?: any }>;
|
|
221
|
-
if (isStackSeries) {
|
|
222
|
-
const seriesKeys = seriesState.visibleSeries.map((s) => s.key);
|
|
223
|
-
|
|
224
|
-
const offset =
|
|
225
|
-
seriesLayout === 'stackExpand'
|
|
226
|
-
? stackOffsetExpand
|
|
227
|
-
: seriesLayout === 'stackDiverging'
|
|
228
|
-
? stackOffsetDiverging
|
|
229
|
-
: stackOffsetNone;
|
|
230
|
-
const stackData = stack()
|
|
231
|
-
.keys(seriesKeys)
|
|
232
|
-
.value((d, key) => {
|
|
233
|
-
const s = series.find((d) => d.key === key)!;
|
|
234
|
-
return accessor(s.value ?? s.key)(d as any);
|
|
235
|
-
})
|
|
236
|
-
.offset(offset)(chartDataArray(data)) as any[];
|
|
237
|
-
|
|
238
|
-
_chartData = _chartData.map((d, i) => {
|
|
239
|
-
return {
|
|
240
|
-
...d,
|
|
241
|
-
stackData: stackData.map((sd) => sd[i]),
|
|
242
|
-
};
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
return _chartData;
|
|
246
|
-
});
|
|
247
|
-
|
|
248
258
|
function getBarsProps(s: SeriesData<TData, typeof Bars>, i: number): ComponentProps<typeof Bars> {
|
|
249
259
|
const isFirst = i == 0;
|
|
250
260
|
const isLast = i == seriesState.visibleSeries.length - 1;
|
|
@@ -278,7 +288,12 @@
|
|
|
278
288
|
y: isVertical ? valueAccessor : undefined,
|
|
279
289
|
x1: isVertical && isGroupSeries ? (d) => s.value ?? s.key : undefined,
|
|
280
290
|
y1: !isVertical && isGroupSeries ? (d) => s.value ?? s.key : undefined,
|
|
281
|
-
rounded:
|
|
291
|
+
rounded:
|
|
292
|
+
isStackLayout && i !== seriesState.visibleSeries.length - 1
|
|
293
|
+
? 'none'
|
|
294
|
+
: Array.isArray(xProp) || Array.isArray(yProp)
|
|
295
|
+
? 'all'
|
|
296
|
+
: 'edge',
|
|
282
297
|
radius: 4,
|
|
283
298
|
strokeWidth: 1,
|
|
284
299
|
insets: stackInsets,
|
|
@@ -350,26 +365,20 @@
|
|
|
350
365
|
return {
|
|
351
366
|
placement: radial ? 'radius' : 'left',
|
|
352
367
|
|
|
353
|
-
format:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return format(value, undefined, { variant: 'short' });
|
|
358
|
-
}
|
|
359
|
-
},
|
|
368
|
+
format:
|
|
369
|
+
isVertical && seriesLayout === 'stackExpand'
|
|
370
|
+
? (value) => format(value, 'percentRound')
|
|
371
|
+
: undefined,
|
|
360
372
|
...(typeof axis === 'object' ? axis : null),
|
|
361
373
|
...props.yAxis,
|
|
362
374
|
};
|
|
363
375
|
}
|
|
364
376
|
return {
|
|
365
377
|
placement: radial ? 'angle' : 'bottom',
|
|
366
|
-
format:
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return format(value, undefined, { variant: 'short' });
|
|
371
|
-
}
|
|
372
|
-
},
|
|
378
|
+
format:
|
|
379
|
+
!isVertical && seriesLayout === 'stackExpand'
|
|
380
|
+
? (value) => format(value, 'percentRound')
|
|
381
|
+
: undefined,
|
|
373
382
|
...(typeof axis === 'object' ? axis : null),
|
|
374
383
|
...props.xAxis,
|
|
375
384
|
};
|
|
@@ -70,7 +70,6 @@
|
|
|
70
70
|
<script lang="ts" generics="TData">
|
|
71
71
|
import { onMount, type ComponentProps } from 'svelte';
|
|
72
72
|
import { scaleLinear, scaleTime } from 'd3-scale';
|
|
73
|
-
import { format } from '@layerstack/utils';
|
|
74
73
|
import { cls } from '@layerstack/tailwind';
|
|
75
74
|
|
|
76
75
|
import Axis from '../Axis.svelte';
|
|
@@ -284,14 +283,12 @@
|
|
|
284
283
|
if (axisDirection === 'y') {
|
|
285
284
|
return {
|
|
286
285
|
placement: radial ? 'radius' : 'left',
|
|
287
|
-
format: (value) => format(value, undefined, { variant: 'short' }),
|
|
288
286
|
...(typeof axis === 'object' ? axis : null),
|
|
289
287
|
...props.yAxis,
|
|
290
288
|
};
|
|
291
289
|
}
|
|
292
290
|
return {
|
|
293
291
|
placement: radial ? 'angle' : 'bottom',
|
|
294
|
-
format: (value) => format(value, undefined, { variant: 'short' }),
|
|
295
292
|
...(typeof axis === 'object' ? axis : null),
|
|
296
293
|
...props.xAxis,
|
|
297
294
|
};
|
|
@@ -205,14 +205,12 @@
|
|
|
205
205
|
if (axisDirection === 'y') {
|
|
206
206
|
return {
|
|
207
207
|
placement: 'left',
|
|
208
|
-
format: (value) => format(value, undefined, { variant: 'short' }),
|
|
209
208
|
...(typeof axis === 'object' ? axis : null),
|
|
210
209
|
...props.yAxis,
|
|
211
210
|
};
|
|
212
211
|
}
|
|
213
212
|
return {
|
|
214
213
|
placement: 'bottom',
|
|
215
|
-
format: (value) => format(value, undefined, { variant: 'short' }),
|
|
216
214
|
...(typeof axis === 'object' ? axis : null),
|
|
217
215
|
...props.xAxis,
|
|
218
216
|
};
|
|
@@ -8,8 +8,8 @@ export declare class HighlightKey<TData, SeriesComponent extends Component> {
|
|
|
8
8
|
}
|
|
9
9
|
export declare class SeriesState<TData, TComponent extends Component> {
|
|
10
10
|
#private;
|
|
11
|
-
selectedSeries: SelectionState<unknown>;
|
|
12
|
-
selectedKeys: SelectionState<unknown>;
|
|
11
|
+
selectedSeries: SelectionState<unknown, false>;
|
|
12
|
+
selectedKeys: SelectionState<unknown, false>;
|
|
13
13
|
highlightKey: HighlightKey<TData, TComponent>;
|
|
14
14
|
constructor(getSeries: () => SeriesData<TData, TComponent>[]);
|
|
15
15
|
get series(): SeriesData<TData, TComponent>[];
|
package/dist/docs/Code.svelte
CHANGED
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
class: className,
|
|
15
15
|
}: {
|
|
16
16
|
source: string | null;
|
|
17
|
-
language
|
|
18
|
-
highlightedSource
|
|
19
|
-
classes
|
|
17
|
+
language?: string;
|
|
18
|
+
highlightedSource?: string;
|
|
19
|
+
classes?: {
|
|
20
20
|
root?: string;
|
|
21
21
|
pre?: string;
|
|
22
22
|
code?: string;
|
|
23
23
|
};
|
|
24
|
-
class
|
|
24
|
+
class?: string;
|
|
25
25
|
} = $props();
|
|
26
26
|
</script>
|
|
27
27
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import 'prism-svelte';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
source: string | null;
|
|
4
|
-
language
|
|
5
|
-
highlightedSource
|
|
6
|
-
classes
|
|
4
|
+
language?: string;
|
|
5
|
+
highlightedSource?: string;
|
|
6
|
+
classes?: {
|
|
7
7
|
root?: string;
|
|
8
8
|
pre?: string;
|
|
9
9
|
code?: string;
|
|
10
10
|
};
|
|
11
|
-
class
|
|
11
|
+
class?: string;
|
|
12
12
|
};
|
|
13
13
|
declare const Code: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
14
14
|
type Code = ReturnType<typeof Code>;
|
package/dist/docs/Json.svelte
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { type ComponentProps } from 'svelte';
|
|
2
3
|
import JsonTree from 'svelte-json-tree';
|
|
3
4
|
import { cls } from '@layerstack/tailwind';
|
|
4
5
|
|
|
5
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
value,
|
|
8
|
+
defaultExpandedPaths = ['$'],
|
|
9
|
+
class: className,
|
|
10
|
+
}: {
|
|
11
|
+
value: ComponentProps<JsonTree>['value'];
|
|
12
|
+
defaultExpandedPaths?: ComponentProps<JsonTree>['defaultExpandedPaths'];
|
|
13
|
+
class?: string;
|
|
14
|
+
} = $props();
|
|
6
15
|
</script>
|
|
7
16
|
|
|
8
17
|
<div class={cls('overflow-auto px-4 py-2 bg-[#1e1e1e]', className)}>
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { type ComponentProps } from 'svelte';
|
|
2
|
+
import JsonTree from 'svelte-json-tree';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
value: ComponentProps<JsonTree>['value'];
|
|
5
|
+
defaultExpandedPaths?: ComponentProps<JsonTree>['defaultExpandedPaths'];
|
|
6
|
+
class?: string;
|
|
7
|
+
};
|
|
8
|
+
declare const Json: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
9
|
type Json = ReturnType<typeof Json>;
|
|
7
10
|
export default Json;
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { SelectField, Switch } from 'svelte-ux';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
let { doubleScale = $bindable(devicePixelRatio > 1), serviceUrl = $bindable() } = $props();
|
|
5
5
|
|
|
6
6
|
// TODO: Access via context, or possibly global state
|
|
7
7
|
const ACCESS_TOKEN =
|
|
8
8
|
'pk.eyJ1IjoidGVjaG5pcTM1IiwiYSI6ImNsZTR5cDd0ZjAyNm8zdnFvczhzdnFpcXkifQ.-LAr8sl5BZ3y-H0pDyD1qA';
|
|
9
9
|
|
|
10
10
|
// https://docs.mapbox.com/api/maps/styles/
|
|
11
|
-
|
|
11
|
+
const mapboxv1 = $derived((style: string) => (x: number, y: number, z: number) => {
|
|
12
12
|
return `https://api.mapbox.com/styles/v1/mapbox/${style}/tiles/${z}/${x}/${y}${
|
|
13
13
|
doubleScale ? '@2x' : ''
|
|
14
14
|
}?access_token=${ACCESS_TOKEN}`;
|
|
15
|
-
};
|
|
15
|
+
});
|
|
16
16
|
|
|
17
17
|
// https://docs.mapbox.com/api/maps/raster-tiles/
|
|
18
18
|
// https://docs.mapbox.com/data/tilesets/reference/mapbox-streets-v8/
|
|
19
|
-
|
|
19
|
+
const mapboxv4 = $derived((tileset: string) => (x: number, y: number, z: number) => {
|
|
20
20
|
return `https://${'abc'[Math.abs(x + y) % 3]}.tiles.mapbox.com/v4/${tileset}/${z}/${x}/${y}${
|
|
21
21
|
doubleScale ? '@2x' : ''
|
|
22
22
|
}.png?access_token=${ACCESS_TOKEN}`;
|
|
23
|
-
};
|
|
23
|
+
});
|
|
24
24
|
|
|
25
25
|
// https://apps.nationalmap.gov/services/
|
|
26
26
|
const nationalmap = (tileset: string) => (x: number, y: number, z: number) => {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
return `https://${s}.tile.opentopomap.org/${z}/${x}/${y}.png`;
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
const services = $derived<Record<string, Record<string, Function>>>({
|
|
58
58
|
'mapbox v1': {
|
|
59
59
|
'streets-v11': mapboxv1('streets-v11'),
|
|
60
60
|
'light-v10': mapboxv1('light-v10'),
|
|
@@ -100,24 +100,25 @@
|
|
|
100
100
|
// 'ArcGIS Vector': {
|
|
101
101
|
// 'Community Map', url: arcgisVector('World_Basemap_v2'),
|
|
102
102
|
// }
|
|
103
|
-
} as Record<string, Record<string, Function>>;
|
|
104
|
-
|
|
105
|
-
$: serviceOptions = Object.entries(services).flatMap(([group, service]) => {
|
|
106
|
-
return Object.entries(service).map(([label, value]) => {
|
|
107
|
-
return { label, value: `${group}:${label}`, group, serviceUrl: value };
|
|
108
|
-
});
|
|
109
103
|
});
|
|
110
104
|
|
|
111
|
-
|
|
112
|
-
|
|
105
|
+
const serviceOptions = $derived(
|
|
106
|
+
Object.entries(services).flatMap(([group, service]) => {
|
|
107
|
+
return Object.entries(service).map(([label, value]) => {
|
|
108
|
+
return { label, value: `${group}:${label}`, group, serviceUrl: value };
|
|
109
|
+
});
|
|
110
|
+
})
|
|
111
|
+
);
|
|
113
112
|
|
|
114
|
-
|
|
113
|
+
const getServiceUrl = $derived((option: string) => {
|
|
115
114
|
const [selectedService, selectedTileset] = selected.split(':');
|
|
116
115
|
return services[selectedService][selectedTileset];
|
|
117
|
-
};
|
|
116
|
+
});
|
|
118
117
|
|
|
119
|
-
let selected = 'mapbox v1:streets-v11';
|
|
120
|
-
|
|
118
|
+
let selected = $state('mapbox v1:streets-v11');
|
|
119
|
+
$effect(() => {
|
|
120
|
+
serviceUrl = getServiceUrl(selected);
|
|
121
|
+
});
|
|
121
122
|
</script>
|
|
122
123
|
|
|
123
124
|
<SelectField
|
|
@@ -128,7 +129,7 @@
|
|
|
128
129
|
toggleIcon={null}
|
|
129
130
|
stepper
|
|
130
131
|
>
|
|
131
|
-
<div slot="append"
|
|
132
|
+
<div slot="append" onclick={(e) => e.stopPropagation()} role="none">
|
|
132
133
|
<div class="text-[10px] text-surface-content/50 text-center">2x</div>
|
|
133
134
|
<Switch bind:checked={doubleScale} size="md" />
|
|
134
135
|
</div>
|
|
@@ -1,23 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
$$events?: Events;
|
|
7
|
-
$$slots?: Slots;
|
|
8
|
-
}): Exports & {
|
|
9
|
-
$set?: any;
|
|
10
|
-
$on?: any;
|
|
11
|
-
};
|
|
12
|
-
z_$$bindings?: Bindings;
|
|
13
|
-
}
|
|
14
|
-
declare const TilesetField: $$__sveltets_2_IsomorphicComponent<{
|
|
15
|
-
doubleScale?: boolean;
|
|
16
|
-
serviceUrl?: Function;
|
|
17
|
-
}, {
|
|
18
|
-
click: MouseEvent;
|
|
19
|
-
} & {
|
|
20
|
-
[evt: string]: CustomEvent<any>;
|
|
21
|
-
}, {}, {}, string>;
|
|
22
|
-
type TilesetField = InstanceType<typeof TilesetField>;
|
|
1
|
+
declare const TilesetField: import("svelte").Component<{
|
|
2
|
+
doubleScale?: any;
|
|
3
|
+
serviceUrl?: any;
|
|
4
|
+
}, {}, "doubleScale" | "serviceUrl">;
|
|
5
|
+
type TilesetField = ReturnType<typeof TilesetField>;
|
|
23
6
|
export default TilesetField;
|
|
@@ -216,7 +216,7 @@ export function createArcTextProps(props, opts = {}, position) {
|
|
|
216
216
|
x: x,
|
|
217
217
|
y: y,
|
|
218
218
|
textAnchor,
|
|
219
|
-
|
|
219
|
+
dominantBaseline: 'middle',
|
|
220
220
|
};
|
|
221
221
|
});
|
|
222
222
|
const current = $derived.by(() => {
|
|
@@ -224,21 +224,21 @@ export function createArcTextProps(props, opts = {}, position) {
|
|
|
224
224
|
return {
|
|
225
225
|
path: innerPath.current,
|
|
226
226
|
...sharedProps,
|
|
227
|
-
|
|
227
|
+
dominantBaseline: innerDominantBaseline,
|
|
228
228
|
};
|
|
229
229
|
}
|
|
230
230
|
else if (position === 'outer') {
|
|
231
231
|
return {
|
|
232
232
|
path: outerPath.current,
|
|
233
233
|
...sharedProps,
|
|
234
|
-
|
|
234
|
+
dominantBaseline: outerDominantBaseline,
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
237
|
else if (position === 'middle') {
|
|
238
238
|
return {
|
|
239
239
|
path: middlePath.current,
|
|
240
240
|
...sharedProps,
|
|
241
|
-
|
|
241
|
+
dominantBaseline: 'middle',
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
else if (position === 'centroid') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ScaleBand } from 'd3-scale';
|
|
1
|
+
import { type ScaleBand, type ScaleTime } from 'd3-scale';
|
|
2
2
|
import { type MotionProp, type MotionOptions, type SpringOptions, type TweenOptions } from './motion.svelte.js';
|
|
3
3
|
import type { Accessor } from './common.js';
|
|
4
4
|
import type { OnlyObjects } from './types.js';
|
|
@@ -23,6 +23,8 @@ export type AnyScale<TInput extends SingleDomainType = any, TOutput extends Sing
|
|
|
23
23
|
thresholds?: () => TInput[];
|
|
24
24
|
quantiles?: () => TInput[];
|
|
25
25
|
};
|
|
26
|
+
export declare function isScaleBand(scale: AnyScale<any, any>): scale is ScaleBand<any>;
|
|
27
|
+
export declare function isScaleTime(scale: AnyScale<any, any>): scale is ScaleTime<any, any>;
|
|
26
28
|
export declare function getRange(scale: any): any[];
|
|
27
29
|
export type SingleDomainType = number | string | Date | null | undefined;
|
|
28
30
|
export type DomainType = (number | string | Date | null | undefined)[] | null | undefined;
|
|
@@ -45,7 +47,6 @@ export declare function createMotionScale<Domain, Range>(scale: AnyScale, motion
|
|
|
45
47
|
* https://gist.github.com/LuisSevillano/d53a1dc529eef518780c6df99613e2fd
|
|
46
48
|
*/
|
|
47
49
|
export declare function scaleBandInvert(scale: ScaleBand<any>): (value: number) => any;
|
|
48
|
-
export declare function isScaleBand(scale: AnyScale<any, any>): scale is ScaleBand<any>;
|
|
49
50
|
/**
|
|
50
51
|
* Generic way to invert a scale value, handling scaleBand and continuous scales (linear, time, etc).
|
|
51
52
|
* Useful to map mouse event location (x,y) to domain value
|
|
@@ -5,6 +5,13 @@ import { Spring, Tween } from 'svelte/motion';
|
|
|
5
5
|
function isAnyScale(scale) {
|
|
6
6
|
return typeof scale === 'function' && typeof scale.range === 'function';
|
|
7
7
|
}
|
|
8
|
+
export function isScaleBand(scale) {
|
|
9
|
+
return typeof scale.bandwidth === 'function';
|
|
10
|
+
}
|
|
11
|
+
export function isScaleTime(scale) {
|
|
12
|
+
const domain = scale.domain();
|
|
13
|
+
return domain[0] instanceof Date || domain[1] instanceof Date;
|
|
14
|
+
}
|
|
8
15
|
export function getRange(scale) {
|
|
9
16
|
if (isAnyScale(scale)) {
|
|
10
17
|
return scale.range();
|
|
@@ -54,9 +61,6 @@ export function scaleBandInvert(scale) {
|
|
|
54
61
|
return domain[Math.max(0, Math.min(index, domain.length - 1))];
|
|
55
62
|
};
|
|
56
63
|
}
|
|
57
|
-
export function isScaleBand(scale) {
|
|
58
|
-
return typeof scale.bandwidth === 'function';
|
|
59
|
-
}
|
|
60
64
|
/**
|
|
61
65
|
* Generic way to invert a scale value, handling scaleBand and continuous scales (linear, time, etc).
|
|
62
66
|
* Useful to map mouse event location (x,y) to domain value
|
package/dist/utils/ticks.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type TimeInterval } from 'd3-time';
|
|
2
|
+
import { Duration, type FormatType } from '@layerstack/utils';
|
|
2
3
|
import { type AnyScale } from './scales.svelte.js';
|
|
3
|
-
export declare function
|
|
4
|
-
export declare function formatMajorTick(date: Date, rangeStart: Date, rangeEnd: Date): string;
|
|
5
|
-
export declare function getMinorTicks(start: Date, end: Date): TimeInterval | null;
|
|
4
|
+
export declare function getDurationFormat(duration: Duration, multiline?: boolean): (date: Date, i: number) => string;
|
|
6
5
|
export type TicksConfig = number | any[] | ((scale: AnyScale) => any[] | undefined) | {
|
|
7
6
|
interval: TimeInterval | null;
|
|
8
7
|
} | null;
|
|
9
|
-
export declare function resolveTickVals(scale: AnyScale, ticks?: TicksConfig,
|
|
8
|
+
export declare function resolveTickVals(scale: AnyScale, ticks?: TicksConfig, count?: number): any[];
|
|
9
|
+
export declare function resolveTickFormat(scale: AnyScale, ticks?: TicksConfig, count?: number, formatType?: FormatType, multiline?: boolean): (date: Date, i: number) => string;
|
package/dist/utils/ticks.js
CHANGED
|
@@ -1,183 +1,130 @@
|
|
|
1
|
-
import { timeYear, timeMonth,
|
|
2
|
-
import { format } from '
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
interval: timeYear.every(1), // Better than rendering a lot of items
|
|
10
|
-
format: (date) => date.toString(),
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
predicate: (duration) => duration.years > 1,
|
|
14
|
-
interval: timeYear.every(1),
|
|
15
|
-
format: (date) => formatDate(date, PeriodType.CalendarYear, { variant: 'short' }),
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
predicate: (duration) => duration.years,
|
|
19
|
-
interval: timeMonth.every(1),
|
|
20
|
-
format: (date) => formatDate(date, PeriodType.Month, { variant: 'short' }),
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
predicate: (duration) => duration.days > 30,
|
|
24
|
-
interval: timeMonth.every(1),
|
|
25
|
-
format: (date) => formatDate(date, PeriodType.Month, { variant: 'short' }),
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
predicate: (duration) => duration.days,
|
|
29
|
-
interval: timeDay.every(1),
|
|
30
|
-
format: (date) => formatDate(date, PeriodType.Day, { variant: 'short' }),
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
predicate: (duration) => duration.hours,
|
|
34
|
-
interval: timeHour.every(1),
|
|
35
|
-
format: (date) => format(date, 'h:mm a'),
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
predicate: (duration) => duration.minutes > 10,
|
|
39
|
-
interval: timeMinute.every(10),
|
|
40
|
-
format: (date) => format(date, 'h:mm a'),
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
predicate: (duration) => duration.minutes,
|
|
44
|
-
interval: timeMinute.every(1),
|
|
45
|
-
format: (date) => format(date, 'h:mm a'),
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
predicate: (duration) => duration.seconds > 10,
|
|
49
|
-
interval: timeSecond.every(10),
|
|
50
|
-
format: (date) => format(date, 'h:mm:ss'),
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
predicate: (duration) => duration.seconds,
|
|
54
|
-
interval: timeSecond.every(1),
|
|
55
|
-
format: (date) => format(date, 'h:mm:ss'),
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
predicate: (duration) => true, // 0 or more milliseconds
|
|
59
|
-
interval: timeMillisecond.every(100),
|
|
60
|
-
format: (date) => format(date, 'h:mm:ss.SSS'),
|
|
61
|
-
},
|
|
62
|
-
];
|
|
63
|
-
const minorTicks = [
|
|
64
|
-
{
|
|
65
|
-
predicate: (duration) => duration == null, // Unknown
|
|
66
|
-
interval: timeYear.every(1), // Better than rendering a lot of items
|
|
67
|
-
format: (date) => date.toString(),
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
predicate: (duration) => duration.years,
|
|
71
|
-
interval: timeMonth.every(1),
|
|
72
|
-
format: (date) => formatDate(date, PeriodType.Month, { variant: 'short' }),
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
predicate: (duration) => duration.days > 90,
|
|
76
|
-
interval: timeMonth.every(1),
|
|
77
|
-
format: (date) => formatDate(date, PeriodType.Month, { variant: 'short' }),
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
predicate: (duration) => duration.days > 30,
|
|
81
|
-
interval: timeWeek.every(1),
|
|
82
|
-
format: (date) => formatDate(date, PeriodType.WeekSun, { variant: 'short' }),
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
predicate: (duration) => duration.days > 7,
|
|
86
|
-
interval: timeDay.every(1),
|
|
87
|
-
format: (date) => formatDate(date, PeriodType.Day, { variant: 'short' }),
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
predicate: (duration) => duration.days > 3,
|
|
91
|
-
interval: timeHour.every(8),
|
|
92
|
-
format: (date) => format(date, 'h:mm a'),
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
predicate: (duration) => duration.days,
|
|
96
|
-
interval: timeHour.every(1),
|
|
97
|
-
format: (date) => format(date, 'h:mm a'),
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
predicate: (duration) => duration.hours,
|
|
101
|
-
interval: timeMinute.every(15),
|
|
102
|
-
format: (date) => format(date, 'h:mm a'),
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
predicate: (duration) => duration.minutes > 10,
|
|
106
|
-
interval: timeMinute.every(10),
|
|
107
|
-
format: (date) => format(date, 'h:mm a'),
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
predicate: (duration) => duration.minutes > 2,
|
|
111
|
-
interval: timeMinute.every(1),
|
|
112
|
-
format: (date) => format(date, 'h:mm a'),
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
predicate: (duration) => duration.minutes,
|
|
116
|
-
interval: timeSecond.every(10),
|
|
117
|
-
format: (date) => format(date, 'h:mm:ss'),
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
predicate: (duration) => duration.seconds,
|
|
121
|
-
interval: timeSecond.every(1),
|
|
122
|
-
format: (date) => format(date, 'h:mm:ss'),
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
predicate: (duration) => true, // 0 or more milliseconds
|
|
126
|
-
interval: timeMillisecond.every(10),
|
|
127
|
-
format: (date) => format(date, 'h:mm:ss.SSS'),
|
|
128
|
-
},
|
|
129
|
-
];
|
|
130
|
-
export function getMajorTicks(start, end) {
|
|
131
|
-
const duration = getDuration(start, end);
|
|
132
|
-
for (var t of majorTicks) {
|
|
133
|
-
if (t.predicate(duration)) {
|
|
134
|
-
return t.interval;
|
|
1
|
+
import { timeYear, timeMonth, timeDay, timeTicks } from 'd3-time';
|
|
2
|
+
import { format, PeriodType, Duration, isLiteralObject, DateToken, } from '@layerstack/utils';
|
|
3
|
+
import { isScaleBand, isScaleTime } from './scales.svelte.js';
|
|
4
|
+
export function getDurationFormat(duration, multiline = false) {
|
|
5
|
+
return function (date, i) {
|
|
6
|
+
if (+duration >= +new Duration({ duration: { years: 1 } })) {
|
|
7
|
+
// Year
|
|
8
|
+
return format(date, PeriodType.CalendarYear);
|
|
135
9
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
10
|
+
else if (+duration >= +new Duration({ duration: { days: 28 } })) {
|
|
11
|
+
// Month
|
|
12
|
+
const isFirst = i === 0 || +timeYear.floor(date) === +date;
|
|
13
|
+
if (multiline) {
|
|
14
|
+
return (format(date, PeriodType.Month, { variant: 'short' }) +
|
|
15
|
+
(isFirst ? `\n${format(date, PeriodType.CalendarYear)}` : ''));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
return (format(date, PeriodType.Month, { variant: 'short' }) +
|
|
19
|
+
(isFirst ? ` '${format(date, PeriodType.CalendarYear, { variant: 'short' })}` : ''));
|
|
20
|
+
}
|
|
144
21
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
22
|
+
else if (+duration >= +new Duration({ duration: { days: 1 } })) {
|
|
23
|
+
// Day
|
|
24
|
+
const isFirst = i === 0 || +timeMonth.floor(date) === +date;
|
|
25
|
+
if (multiline) {
|
|
26
|
+
return (format(date, PeriodType.Custom, { custom: DateToken.DayOfMonth_numeric }) +
|
|
27
|
+
(isFirst ? `\n${format(date, PeriodType.Month, { variant: 'short' })}` : ''));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
return format(date, PeriodType.Day, { variant: 'short' });
|
|
31
|
+
}
|
|
153
32
|
}
|
|
154
|
-
|
|
155
|
-
|
|
33
|
+
else if (+duration >= +new Duration({ duration: { hours: 1 } })) {
|
|
34
|
+
// Hours
|
|
35
|
+
const isFirst = i === 0 || +timeDay.floor(date) === +date;
|
|
36
|
+
if (multiline) {
|
|
37
|
+
return (format(date, PeriodType.Custom, { custom: DateToken.Hour_numeric }) +
|
|
38
|
+
(isFirst ? `\n${format(date, PeriodType.Day, { variant: 'short' })}` : ''));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
return isFirst
|
|
42
|
+
? format(date, PeriodType.Day, { variant: 'short' })
|
|
43
|
+
: format(date, PeriodType.Custom, { custom: DateToken.Hour_numeric });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (+duration >= +new Duration({ duration: { minutes: 1 } })) {
|
|
47
|
+
// Minutes
|
|
48
|
+
const isFirst = i === 0 || +timeDay.floor(date) === +date;
|
|
49
|
+
if (multiline) {
|
|
50
|
+
return (format(date, PeriodType.TimeOnly, { variant: 'short' }) +
|
|
51
|
+
(isFirst ? `\n${format(date, PeriodType.Day, { variant: 'short' })}` : ''));
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
return format(date, PeriodType.TimeOnly, { variant: 'short' });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (+duration >= +new Duration({ duration: { seconds: 1 } })) {
|
|
58
|
+
// Seconds
|
|
59
|
+
const isFirst = i === 0 || +timeDay.floor(date) === +date;
|
|
60
|
+
return (format(date, PeriodType.TimeOnly) +
|
|
61
|
+
(multiline && isFirst ? `\n${format(date, PeriodType.Day, { variant: 'short' })}` : ''));
|
|
62
|
+
}
|
|
63
|
+
else if (+duration >= +new Duration({ duration: { milliseconds: 1 } })) {
|
|
64
|
+
// Milliseconds
|
|
65
|
+
const isFirst = i === 0 || +timeDay.floor(date) === +date;
|
|
66
|
+
return (format(date, PeriodType.Custom, {
|
|
67
|
+
custom: [
|
|
68
|
+
DateToken.Hour_2Digit,
|
|
69
|
+
DateToken.Minute_2Digit,
|
|
70
|
+
DateToken.Second_2Digit,
|
|
71
|
+
DateToken.MiliSecond_3,
|
|
72
|
+
DateToken.Hour_woAMPM,
|
|
73
|
+
],
|
|
74
|
+
}) + (multiline && isFirst ? `\n${format(date, PeriodType.Day, { variant: 'short' })}` : ''));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
return date.toString();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
156
80
|
}
|
|
157
|
-
export function resolveTickVals(scale, ticks,
|
|
81
|
+
export function resolveTickVals(scale, ticks, count) {
|
|
82
|
+
// Explicit ticks
|
|
158
83
|
if (Array.isArray(ticks))
|
|
159
84
|
return ticks;
|
|
85
|
+
// Function
|
|
160
86
|
if (typeof ticks === 'function')
|
|
161
87
|
return ticks(scale) ?? [];
|
|
88
|
+
// Interval
|
|
162
89
|
if (isLiteralObject(ticks) && 'interval' in ticks) {
|
|
163
90
|
if (ticks.interval === null || !('ticks' in scale) || typeof scale.ticks !== 'function') {
|
|
164
91
|
return []; // Explicitly return empty array for null interval or invalid scale
|
|
165
92
|
}
|
|
166
93
|
return scale.ticks(ticks.interval);
|
|
167
94
|
}
|
|
95
|
+
// Band (use domain)
|
|
168
96
|
if (isScaleBand(scale)) {
|
|
169
97
|
return ticks && typeof ticks === 'number'
|
|
170
98
|
? scale.domain().filter((_, i) => i % ticks === 0)
|
|
171
99
|
: scale.domain();
|
|
172
100
|
}
|
|
101
|
+
// Ticks from scale
|
|
173
102
|
if (scale.ticks && typeof scale.ticks === 'function') {
|
|
174
|
-
|
|
175
|
-
return scale.ticks(ticks ?? (placement === 'left' || placement === 'right' ? 4 : undefined));
|
|
176
|
-
}
|
|
177
|
-
return scale.ticks(ticks);
|
|
103
|
+
return scale.ticks(count ?? (typeof ticks === 'number' ? ticks : undefined));
|
|
178
104
|
}
|
|
179
105
|
return [];
|
|
180
106
|
}
|
|
181
|
-
function
|
|
182
|
-
|
|
107
|
+
export function resolveTickFormat(scale, ticks, count, formatType, multiline = false) {
|
|
108
|
+
// Explicit format
|
|
109
|
+
if (formatType) {
|
|
110
|
+
return (tick) => format(tick, formatType);
|
|
111
|
+
}
|
|
112
|
+
// Time scale
|
|
113
|
+
if (isScaleTime(scale) && count) {
|
|
114
|
+
if (isLiteralObject(ticks) && 'interval' in ticks && ticks.interval != null) {
|
|
115
|
+
const start = ticks.interval.floor(new Date());
|
|
116
|
+
const end = ticks.interval.ceil(new Date());
|
|
117
|
+
return getDurationFormat(new Duration({ start, end }), multiline);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Compare first 2 ticks to determine duration between ticks for formatting
|
|
121
|
+
const [start, end] = timeTicks(scale.domain()[0], scale.domain()[1], count);
|
|
122
|
+
return getDurationFormat(new Duration({ start, end }), multiline);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Format from scale
|
|
126
|
+
if (scale.tickFormat) {
|
|
127
|
+
return scale.tickFormat(count);
|
|
128
|
+
}
|
|
129
|
+
return (tick) => `${tick}`;
|
|
183
130
|
}
|
package/dist/utils/ticks.test.js
CHANGED
|
@@ -35,33 +35,23 @@ describe('resolveTickVals', () => {
|
|
|
35
35
|
const scale = { domain: mockDomain, bandwidth: vi.fn() };
|
|
36
36
|
expect(resolveTickVals(scale)).toEqual(['a', 'b', 'c', 'd', 'e']);
|
|
37
37
|
});
|
|
38
|
-
it('uses default 4 ticks for left placement', () => {
|
|
39
|
-
const scale = { ticks: vi.fn(() => [1, 2, 3, 4]) };
|
|
40
|
-
expect(resolveTickVals(scale, undefined, 'left')).toEqual([1, 2, 3, 4]);
|
|
41
|
-
expect(scale.ticks).toHaveBeenCalledWith(4);
|
|
42
|
-
});
|
|
43
|
-
it('uses default 4 ticks for right placement', () => {
|
|
44
|
-
const scale = { ticks: vi.fn(() => [1, 2, 3, 4]) };
|
|
45
|
-
expect(resolveTickVals(scale, undefined, 'right')).toEqual([1, 2, 3, 4]);
|
|
46
|
-
expect(scale.ticks).toHaveBeenCalledWith(4);
|
|
47
|
-
});
|
|
48
38
|
it('uses undefined for non-left/right placement', () => {
|
|
49
|
-
const scale = { ticks: vi.fn(() => [1, 2]) };
|
|
50
|
-
expect(resolveTickVals(scale, undefined,
|
|
39
|
+
const scale = { domain: mockDomain, ticks: vi.fn(() => [1, 2]) };
|
|
40
|
+
expect(resolveTickVals(scale, undefined, undefined)).toEqual([1, 2]);
|
|
51
41
|
expect(scale.ticks).toHaveBeenCalledWith(undefined);
|
|
52
42
|
});
|
|
53
43
|
it('passes number ticks to scale.ticks', () => {
|
|
54
|
-
const scale = { ticks: vi.fn(() => [10, 20]) };
|
|
44
|
+
const scale = { domain: mockDomain, ticks: vi.fn(() => [10, 20]) };
|
|
55
45
|
expect(resolveTickVals(scale, 5)).toEqual([10, 20]);
|
|
56
46
|
expect(scale.ticks).toHaveBeenCalledWith(5);
|
|
57
47
|
});
|
|
58
48
|
it('returns empty array for scale without ticks', () => {
|
|
59
|
-
const scale = {};
|
|
49
|
+
const scale = { domain: mockDomain };
|
|
60
50
|
expect(resolveTickVals(scale, 5)).toEqual([]);
|
|
61
51
|
});
|
|
62
52
|
it('handles null ticks with placement', () => {
|
|
63
|
-
const scale = { ticks: vi.fn(() => [1, 2, 3]) };
|
|
64
|
-
expect(resolveTickVals(scale, null,
|
|
53
|
+
const scale = { domain: mockDomain, ticks: vi.fn(() => [1, 2, 3]) };
|
|
54
|
+
expect(resolveTickVals(scale, null, undefined)).toEqual([1, 2, 3]);
|
|
65
55
|
expect(scale.ticks).toHaveBeenCalledWith(undefined);
|
|
66
56
|
});
|
|
67
57
|
});
|
package/package.json
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
"author": "Sean Lynch <techniq35@gmail.com>",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "techniq/layerchart",
|
|
7
|
-
"version": "2.0.0-next.
|
|
7
|
+
"version": "2.0.0-next.9",
|
|
8
8
|
"devDependencies": {
|
|
9
9
|
"@changesets/cli": "^2.29.4",
|
|
10
|
-
"@iconify-json/lucide": "^1.2.
|
|
10
|
+
"@iconify-json/lucide": "^1.2.44",
|
|
11
11
|
"@mdi/js": "^7.4.47",
|
|
12
12
|
"@rollup/plugin-dsv": "^3.0.5",
|
|
13
13
|
"@sveltejs/adapter-cloudflare": "^7.0.3",
|
|
14
|
-
"@sveltejs/kit": "^2.21.
|
|
14
|
+
"@sveltejs/kit": "^2.21.1",
|
|
15
15
|
"@sveltejs/package": "^2.3.11",
|
|
16
16
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
|
17
17
|
"@svitejs/changesets-changelog-github-compact": "^1.2.0",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@types/topojson-specification": "^1.0.5",
|
|
44
44
|
"marked": "^15.0.11",
|
|
45
45
|
"mdsvex": "^0.12.3",
|
|
46
|
-
"posthog-js": "^1.
|
|
46
|
+
"posthog-js": "^1.245.1",
|
|
47
47
|
"prettier": "^3.5.3",
|
|
48
48
|
"prettier-plugin-svelte": "^3.4.0",
|
|
49
49
|
"prism-svelte": "^0.5.0",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"rehype-slug": "^6.0.0",
|
|
53
53
|
"shapefile": "^0.6.6",
|
|
54
54
|
"solar-calculator": "^0.3.0",
|
|
55
|
-
"svelte": "5.
|
|
55
|
+
"svelte": "5.32.1",
|
|
56
56
|
"svelte-check": "^4.2.1",
|
|
57
57
|
"svelte-json-tree": "^2.2.0",
|
|
58
58
|
"svelte-ux": "2.0.0-next.2",
|
|
@@ -66,15 +66,15 @@
|
|
|
66
66
|
"unplugin-icons": "^22.1.0",
|
|
67
67
|
"us-atlas": "^3.0.1",
|
|
68
68
|
"vite": "^6.3.5",
|
|
69
|
-
"vitest": "^3.1.
|
|
69
|
+
"vitest": "^3.1.4"
|
|
70
70
|
},
|
|
71
71
|
"type": "module",
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@dagrejs/dagre": "^1.1.4",
|
|
74
74
|
"@layerstack/svelte-actions": "1.0.1-next.2",
|
|
75
|
-
"@layerstack/svelte-state": "0.1.0-next.
|
|
75
|
+
"@layerstack/svelte-state": "0.1.0-next.7",
|
|
76
76
|
"@layerstack/tailwind": "2.0.0-next.4",
|
|
77
|
-
"@layerstack/utils": "
|
|
77
|
+
"@layerstack/utils": "2.0.0-next.3",
|
|
78
78
|
"d3-array": "^3.2.4",
|
|
79
79
|
"d3-color": "^3.1.0",
|
|
80
80
|
"d3-delaunay": "^6.0.4",
|