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.
@@ -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 { format as formatValue, type FormatType } from '@layerstack/utils';
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 tickVals = $derived(resolveTickVals(scale, ticks, placement));
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.toString())}
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: formatValue(tick, format ?? scale.tickFormat?.() ?? ((v) => v)),
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
- '[LayerChart] Target div has zero or negative width. Did you forget to set an explicit width in CSS on the container?'
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
- '[LayerChart] Target div has zero or negative height. Did you forget to set an explicit height in CSS on the container?'
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
  }
@@ -351,8 +351,8 @@
351
351
 
352
352
  $effect(() => {
353
353
  if (!draw) return;
354
- [tweenedState.current];
355
354
  // Anytime the path data changes, redraw
355
+ [pathData, data, ctx.data];
356
356
  key = Symbol();
357
357
  });
358
358
  </script>
@@ -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
- const rawText = $derived(value != null ? value.toString() : '');
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
- wordsWithWidth.reduce((result: { words: string[]; width?: number }[], item) => {
285
- const currentLine = result[result.length - 1];
286
-
287
- if (
288
- currentLine &&
289
- (width == null || scaleToFit || (currentLine.width || 0) + item.width + spaceWidth < width)
290
- ) {
291
- // Word can be added to an existing line
292
- currentLine.words.push(item.word);
293
- currentLine.width = currentLine.width || 0;
294
- currentLine.width += item.width + spaceWidth;
295
- } else {
296
- // Add first word to line or word is too long to scaleToFit on existing line
297
- const newLine = { words: [item.word], width: item.width };
298
- result.push(newLine);
299
- }
300
-
301
- return result;
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 lines = $derived(wordsByLines.length);
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 ((lines - 1) / 2) * -getPixelValue(lineHeight) + getPixelValue(capHeight) / 2;
335
+ return ((lineCount - 1) / 2) * -getPixelValue(lineHeight) + getPixelValue(capHeight) / 2;
333
336
  } else {
334
- return (lines - 1) * -getPixelValue(lineHeight);
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
- lines > 0 &&
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: (value) => {
402
- if (seriesLayout === 'stackExpand') {
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 { AnyScale } from '../../utils/scales.svelte.js';
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 ?? (isVertical ? scaleBand().padding(bandPadding) : scaleLinear())
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 ?? (isVertical ? scaleLinear() : scaleBand().padding(bandPadding))
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: isStackLayout && i !== seriesState.visibleSeries.length - 1 ? 'none' : 'edge',
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: (value) => {
354
- if (isVertical && seriesLayout === 'stackExpand') {
355
- return format(value, 'percentRound');
356
- } else {
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: (value) => {
367
- if (!isVertical && seriesLayout === 'stackExpand') {
368
- return format(value, 'percentRound');
369
- } else {
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>[];
@@ -14,14 +14,14 @@
14
14
  class: className,
15
15
  }: {
16
16
  source: string | null;
17
- language: string;
18
- highlightedSource: string;
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: string;
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: string;
5
- highlightedSource: string;
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: string;
11
+ class?: string;
12
12
  };
13
13
  declare const Code: import("svelte").Component<$$ComponentProps, {}, "">;
14
14
  type Code = ReturnType<typeof Code>;
@@ -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 { value, defaultExpandedPaths = ['$'], class: className } = $props();
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
- declare const Json: import("svelte").Component<{
2
- value: any;
3
- defaultExpandedPaths?: any[];
4
- class: any;
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
- export let doubleScale = devicePixelRatio > 1;
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
- $: mapboxv1 = (style: string) => (x: number, y: number, z: number) => {
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
- $: mapboxv4 = (tileset: string) => (x: number, y: number, z: number) => {
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
- $: services = {
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
- $: defaultServiceUrl = services['mapbox v1']['streets-v11'];
112
- export let serviceUrl = defaultServiceUrl;
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
- $: getServiceUrl = (option: string) => {
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
- $: serviceUrl = getServiceUrl(selected);
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" on:click|stopPropagation role="none">
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
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: Props & {
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
- 'dominant-baseline': 'middle',
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
- 'dominant-baseline': innerDominantBaseline,
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
- 'dominant-baseline': outerDominantBaseline,
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
- 'dominant-baseline': 'middle',
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
@@ -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 getMajorTicks(start: Date, end: Date): TimeInterval | null;
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, placement?: 'radius' | 'top' | 'bottom' | 'left' | 'right' | 'angle'): any[];
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;
@@ -1,183 +1,130 @@
1
- import { timeYear, timeMonth, timeWeek, timeDay, timeHour, timeMinute, timeSecond, timeMillisecond, } from 'd3-time';
2
- import { format } from 'date-fns';
3
- import { formatDate, PeriodType, getDuration, fail } from '@layerstack/utils';
4
- import { isScaleBand } from './scales.svelte.js';
5
- // TODO: Use PeriodType along with Duration to format (and possibly select intervals)
6
- const majorTicks = [
7
- {
8
- predicate: (duration) => duration == null, // Unknown
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
- fail(`Unable to locate major ticks for duration: ${duration}`);
138
- }
139
- export function formatMajorTick(date, rangeStart, rangeEnd) {
140
- const duration = getDuration(rangeStart, rangeEnd);
141
- for (var t of majorTicks) {
142
- if (t.predicate(duration)) {
143
- return t.format(date);
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
- fail(`Unable to format major ticks for duration: ${duration}`);
147
- }
148
- export function getMinorTicks(start, end) {
149
- const duration = getDuration(start, end);
150
- for (var t of minorTicks) {
151
- if (t.predicate(duration)) {
152
- return t.interval;
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
- fail(`Unable to locate minor ticks for duration: ${duration}`);
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, placement) {
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
- if (placement) {
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 isLiteralObject(val) {
182
- return val !== null && typeof val === 'object' && !Array.isArray(val);
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
  }
@@ -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, 'top')).toEqual([1, 2]);
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, 'bottom')).toEqual([1, 2, 3]);
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",
7
+ "version": "2.0.0-next.9",
8
8
  "devDependencies": {
9
9
  "@changesets/cli": "^2.29.4",
10
- "@iconify-json/lucide": "^1.2.42",
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.0",
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.242.2",
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.30.1",
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.3"
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.4",
75
+ "@layerstack/svelte-state": "0.1.0-next.7",
76
76
  "@layerstack/tailwind": "2.0.0-next.4",
77
- "@layerstack/utils": "1.1.0-next.2",
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",