layerchart 2.0.0-next.30 → 2.0.0-next.31

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.
@@ -44,7 +44,7 @@
44
44
  ticks?: TicksConfig;
45
45
 
46
46
  /**
47
- * Width or height of each tick in pxiels (responsive reduce)
47
+ * Width or height of each tick in pixels (enabling responsive count)
48
48
  */
49
49
  tickSpacing?: number;
50
50
 
@@ -96,7 +96,7 @@
96
96
  transitionInParams?: TransitionParams<In>;
97
97
 
98
98
  /**
99
- * Scale for the axis
99
+ * Override scale for the axis
100
100
  */
101
101
  scale?: any;
102
102
 
@@ -183,6 +183,9 @@
183
183
  const scale = $derived(
184
184
  scaleProp ?? (['horizontal', 'angle'].includes(orientation) ? ctx.xScale : ctx.yScale)
185
185
  );
186
+ const interval = $derived(
187
+ ['horizontal', 'angle'].includes(orientation) ? ctx.xInterval : ctx.yInterval
188
+ );
186
189
 
187
190
  const xRangeMinMax = $derived(extent<number>(ctx.xRange)) as [number, number];
188
191
  const yRangeMinMax = $derived(extent<number>(ctx.yRange)) as [number, number];
@@ -206,7 +209,7 @@
206
209
  ? Math.round(ctxSize / tickSpacing)
207
210
  : undefined
208
211
  );
209
- const tickVals = $derived(resolveTickVals(scale, ticks, tickCount));
212
+ const tickVals = $derived(resolveTickVals(scale, ticks, tickCount, interval));
210
213
  const tickFormat = $derived(
211
214
  resolveTickFormat({
212
215
  scale,
@@ -221,27 +224,29 @@
221
224
  function getCoords(tick: any) {
222
225
  switch (placement) {
223
226
  case 'top':
224
- return {
225
- x: scale(tick) + (isScaleBand(scale) ? scale.bandwidth() / 2 : 0),
226
- y: yRangeMinMax[0],
227
- };
228
-
229
227
  case 'bottom':
230
228
  return {
231
- x: scale(tick) + (isScaleBand(scale) ? scale.bandwidth() / 2 : 0),
232
- y: yRangeMinMax[1],
229
+ x:
230
+ scale(tick) +
231
+ (isScaleBand(scale)
232
+ ? scale.bandwidth() / 2
233
+ : ctx.xInterval
234
+ ? (scale(ctx.xInterval.offset(tick)) - scale(tick)) / 2 // offset 1/2 width of time interval
235
+ : 0),
236
+ y: placement === 'top' ? yRangeMinMax[0] : yRangeMinMax[1],
233
237
  };
234
238
 
235
239
  case 'left':
236
- return {
237
- x: xRangeMinMax[0],
238
- y: scale(tick) + (isScaleBand(scale) ? scale.bandwidth() / 2 : 0),
239
- };
240
-
241
240
  case 'right':
242
241
  return {
243
- x: xRangeMinMax[1],
244
- y: scale(tick) + (isScaleBand(scale) ? scale.bandwidth() / 2 : 0),
242
+ x: placement === 'left' ? xRangeMinMax[0] : xRangeMinMax[1],
243
+ y:
244
+ scale(tick) +
245
+ (isScaleBand(scale)
246
+ ? scale.bandwidth() / 2
247
+ : ctx.yInterval
248
+ ? (scale(ctx.yInterval.offset(tick)) - scale(tick)) / 2 // offset 1/2 height of time interval
249
+ : 0),
245
250
  };
246
251
 
247
252
  case 'angle':
@@ -37,7 +37,7 @@ export type AxisPropsWithoutHTML<In extends Transition = Transition> = {
37
37
  */
38
38
  ticks?: TicksConfig;
39
39
  /**
40
- * Width or height of each tick in pxiels (responsive reduce)
40
+ * Width or height of each tick in pixels (enabling responsive count)
41
41
  */
42
42
  tickSpacing?: number;
43
43
  /**
@@ -83,7 +83,7 @@ export type AxisPropsWithoutHTML<In extends Transition = Transition> = {
83
83
  */
84
84
  transitionInParams?: TransitionParams<In>;
85
85
  /**
86
- * Scale for the axis
86
+ * Override scale for the axis
87
87
  */
88
88
  scale?: any;
89
89
  /**
@@ -81,7 +81,7 @@
81
81
  import Rect from './Rect.svelte';
82
82
  import Spline from './Spline.svelte';
83
83
 
84
- import { isScaleBand } from '../utils/scales.svelte.js';
84
+ import { isScaleBand, isScaleTime } from '../utils/scales.svelte.js';
85
85
  import { accessor, type Accessor } from '../utils/common.js';
86
86
  import { getChartContext } from './Chart.svelte';
87
87
  import type { CommonStyleProps, Without } from '../utils/types.js';
@@ -127,7 +127,7 @@
127
127
 
128
128
  const dimensions = $derived(getDimensions(data) ?? { x: 0, y: 0, width: 0, height: 0 });
129
129
 
130
- const isVertical = $derived(isScaleBand(ctx.xScale));
130
+ const isVertical = $derived(isScaleBand(ctx.xScale) || isScaleTime(ctx.xScale));
131
131
  const valueAccessor = $derived(accessor(isVertical ? y : x));
132
132
  const value = $derived(valueAccessor(data));
133
133
  const resolvedValue = $derived(Array.isArray(value) ? greatestAbs(value) : value);
@@ -151,16 +151,20 @@
151
151
  const bottomRight = $derived(['all', 'bottom', 'right', 'bottom-right'].includes(rounded));
152
152
  const width = $derived(dimensions.width);
153
153
  const height = $derived(dimensions.height);
154
- const diameter = $derived(2 * radius);
154
+
155
+ // Clamp radius to prevent extending beyond bounding box
156
+ const r = $derived(Math.min(radius, width / 2, height / 2));
157
+ const diameter = $derived(2 * r);
158
+
155
159
  const pathData = $derived(
156
- `M${dimensions.x + radius},${dimensions.y} h${width - diameter}
157
- ${topRight ? `a${radius},${radius} 0 0 1 ${radius},${radius}` : `h${radius}v${radius}`}
160
+ `M${dimensions.x + r},${dimensions.y} h${width - diameter}
161
+ ${topRight ? `a${r},${r} 0 0 1 ${r},${r}` : `h${r}v${r}`}
158
162
  v${height - diameter}
159
- ${bottomRight ? `a${radius},${radius} 0 0 1 ${-radius},${radius}` : `v${radius}h${-radius}`}
163
+ ${bottomRight ? `a${r},${r} 0 0 1 ${-r},${r}` : `v${r}h${-r}`}
160
164
  h${diameter - width}
161
- ${bottomLeft ? `a${radius},${radius} 0 0 1 ${-radius},${-radius}` : `h${-radius}v${-radius}`}
165
+ ${bottomLeft ? `a${r},${r} 0 0 1 ${-r},${-r}` : `h${-r}v${-r}`}
162
166
  v${diameter - height}
163
- ${topLeft ? `a${radius},${radius} 0 0 1 ${radius},${-radius}` : `v${-radius}h${radius}`}
167
+ ${topLeft ? `a${r},${r} 0 0 1 ${r},${-r}` : `v${-r}h${r}`}
164
168
  z`
165
169
  .split('\n')
166
170
  .join('')
@@ -7,6 +7,7 @@
7
7
  createScale,
8
8
  getRange,
9
9
  isScaleBand,
10
+ isScaleTime,
10
11
  makeAccessor,
11
12
  type AnyScale,
12
13
  type DomainType,
@@ -40,6 +41,7 @@
40
41
  import TransformContext, { type TransformContextValue } from './TransformContext.svelte';
41
42
  import BrushContext, { type BrushContextValue } from './BrushContext.svelte';
42
43
  import { layerClass } from '../utils/attributes.js';
44
+ import type { TimeInterval } from 'd3-time';
43
45
 
44
46
  const defaultPadding = { top: 0, right: 0, bottom: 0, left: 0 };
45
47
 
@@ -149,6 +151,8 @@
149
151
  cGet: (d: T) => any;
150
152
  x1Get: (d: T) => any;
151
153
  y1Get: (d: T) => any;
154
+ xInterval: TimeInterval | null;
155
+ yInterval: TimeInterval | null;
152
156
  radial: boolean;
153
157
  tooltip: TooltipContextValue<T>;
154
158
  geo: GeoContextValue;
@@ -640,6 +644,16 @@
640
644
  */
641
645
  yBaseline?: number | null;
642
646
 
647
+ /**
648
+ * Time interval to use for the x-axis when using a time scale.
649
+ */
650
+ xInterval?: TimeInterval | null;
651
+
652
+ /**
653
+ * Time interval to use for the y-axis when using a time scale.
654
+ */
655
+ yInterval?: TimeInterval | null;
656
+
643
657
  /* Props passed to ChartContext */
644
658
 
645
659
  /**
@@ -738,6 +752,8 @@
738
752
  rRange: rRangeProp,
739
753
  xBaseline = null,
740
754
  yBaseline = null,
755
+ xInterval = null,
756
+ yInterval = null,
741
757
  meta = {},
742
758
  children: _children,
743
759
  radial = false,
@@ -780,6 +796,12 @@
780
796
 
781
797
  const _xDomain: DomainType | undefined = $derived.by(() => {
782
798
  if (xDomainProp !== undefined) return xDomainProp;
799
+
800
+ if (xInterval != null && Array.isArray(data) && data.length > 0) {
801
+ const lastXValue = accessor(xProp)(data[data.length - 1]);
802
+ return [null, xInterval.offset(lastXValue)];
803
+ }
804
+
783
805
  if (xBaseline != null && Array.isArray(data)) {
784
806
  const xValues = data.flatMap(accessor(xProp));
785
807
  return [min([xBaseline, ...xValues]), max([xBaseline, ...xValues])];
@@ -788,6 +810,12 @@
788
810
 
789
811
  const _yDomain: DomainType | undefined = $derived.by(() => {
790
812
  if (yDomainProp !== undefined) return yDomainProp;
813
+
814
+ if (yInterval != null && Array.isArray(data) && data.length > 0) {
815
+ const lastYValue = accessor(yProp)(data[data.length - 1]);
816
+ return [null, yInterval.offset(lastYValue)];
817
+ }
818
+
791
819
  if (yBaseline != null && Array.isArray(data)) {
792
820
  const yValues = data.flatMap(accessor(yProp));
793
821
  return [min([yBaseline, ...yValues]), max([yBaseline, ...yValues])];
@@ -798,7 +826,9 @@
798
826
  _yRangeProp ?? (radial ? ({ height }: { height: number }) => [0, height / 2] : undefined)
799
827
  );
800
828
 
801
- const yReverse = $derived(yScaleProp ? !isScaleBand(yScaleProp) : true);
829
+ const yReverse = $derived(
830
+ yScaleProp ? !isScaleBand(yScaleProp) && !isScaleTime(yScaleProp) : true
831
+ );
802
832
 
803
833
  const x = $derived(makeAccessor(xProp));
804
834
  const y = $derived(makeAccessor(yProp));
@@ -1247,6 +1277,12 @@
1247
1277
  get y1Scale() {
1248
1278
  return y1Scale;
1249
1279
  },
1280
+ get xInterval() {
1281
+ return xInterval;
1282
+ },
1283
+ get yInterval() {
1284
+ return yInterval;
1285
+ },
1250
1286
  get radial() {
1251
1287
  return radial;
1252
1288
  },
@@ -8,6 +8,7 @@ import type { HierarchyNode } from 'd3-hierarchy';
8
8
  import type { SankeyGraph } from 'd3-sankey';
9
9
  import TransformContext, { type TransformContextValue } from './TransformContext.svelte';
10
10
  import BrushContext, { type BrushContextValue } from './BrushContext.svelte';
11
+ import type { TimeInterval } from 'd3-time';
11
12
  export type ChartResizeDetail = {
12
13
  width: number;
13
14
  height: number;
@@ -81,6 +82,8 @@ export type ChartContextValue<T = any, XScale extends AnyScale = AnyScale, YScal
81
82
  cGet: (d: T) => any;
82
83
  x1Get: (d: T) => any;
83
84
  y1Get: (d: T) => any;
85
+ xInterval: TimeInterval | null;
86
+ yInterval: TimeInterval | null;
84
87
  radial: boolean;
85
88
  tooltip: TooltipContextValue<T>;
86
89
  geo: GeoContextValue;
@@ -484,6 +487,14 @@ export type ChartPropsWithoutHTML<T, XScale extends AnyScale = AnyScale, YScale
484
487
  * @default null
485
488
  */
486
489
  yBaseline?: number | null;
490
+ /**
491
+ * Time interval to use for the x-axis when using a time scale.
492
+ */
493
+ xInterval?: TimeInterval | null;
494
+ /**
495
+ * Time interval to use for the y-axis when using a time scale.
496
+ */
497
+ yInterval?: TimeInterval | null;
487
498
  /**
488
499
  * Use radial instead of cartesian coordinates, mapping `x` to `angle` and `y`` to radial.
489
500
  * Radial lines are positioned relative to the origin, use transform (ex. `<Group center>`)
@@ -31,7 +31,7 @@
31
31
  > = {
32
32
  alpha: number;
33
33
  alphaTarget: number;
34
- simulation: SimulationFor<NodeDatum, LinkDatum>;
34
+ simulation: Simulation<NodeDatum, LinkDatum>;
35
35
  };
36
36
 
37
37
  export type OnTickEvent<
@@ -40,9 +40,9 @@
40
40
  > = {
41
41
  alpha: number;
42
42
  alphaTarget: number;
43
- nodes: NodeDatumFor<NodeDatum>[];
44
- links: LinkDatumFor<NodeDatum, LinkDatum>[];
45
- simulation: SimulationFor<NodeDatum, LinkDatum>;
43
+ nodes: NodeDatum[];
44
+ links: LinkDatum[];
45
+ simulation: Simulation<NodeDatum, LinkDatum>;
46
46
  };
47
47
 
48
48
  export type OnEndEvent<
@@ -51,7 +51,18 @@
51
51
  > = {
52
52
  alpha: number;
53
53
  alphaTarget: number;
54
- simulation: SimulationFor<NodeDatum, LinkDatum>;
54
+ simulation: Simulation<NodeDatum, LinkDatum>;
55
+ };
56
+
57
+ export type OnNodesChangeEvent<
58
+ NodeDatum extends SimulationNodeDatum,
59
+ LinkDatum extends SimulationLinkDatum<NodeDatum> | undefined,
60
+ > = {
61
+ alpha: number;
62
+ alphaTarget: number;
63
+ nodes: NodeDatum[];
64
+ links: LinkDatum[];
65
+ simulation: Simulation<NodeDatum, LinkDatum>;
55
66
  };
56
67
 
57
68
  /**
@@ -81,16 +92,6 @@
81
92
  */
82
93
  export const DEFAULT_VELOCITY_DECAY: number = 0.4;
83
94
 
84
- type NodeDatumFor<NodeDatum> = NodeDatum & SimulationNodeDatum;
85
-
86
- type LinkDatumFor<NodeDatum, LinkDatum> = LinkDatum &
87
- SimulationLinkDatum<NodeDatumFor<NodeDatum>>;
88
-
89
- type SimulationFor<NodeDatum, LinkDatum> = Simulation<
90
- NodeDatumFor<NodeDatum>,
91
- LinkDatumFor<NodeDatum, LinkDatum>
92
- >;
93
-
94
95
  export type ForceSimulationProps<
95
96
  NodeDatum extends SimulationNodeDatum,
96
97
  LinkDatum extends SimulationLinkDatum<NodeDatum> | undefined,
@@ -159,6 +160,11 @@
159
160
  */
160
161
  onStart?: (e: OnStartEvent<NodeDatum, LinkDatum | undefined>) => void;
161
162
 
163
+ /**
164
+ * Callback function triggered right before nodes get passed to the simulation
165
+ */
166
+ onNodesChange?: (e: OnNodesChangeEvent<NodeDatum, LinkDatum | undefined>) => void;
167
+
162
168
  /**
163
169
  * Callback function triggered on each simulation tick
164
170
  */
@@ -172,10 +178,10 @@
172
178
  children?: Snippet<
173
179
  [
174
180
  {
175
- nodes: NodeDatumFor<NodeDatum>[];
176
- links: LinkDatumFor<NodeDatum, LinkDatum>[];
181
+ nodes: NodeDatum[];
182
+ links: LinkDatum[];
177
183
  linkPositions: LinkPosition[];
178
- simulation: SimulationFor<NodeDatum, LinkDatum>;
184
+ simulation: Simulation<NodeDatum, LinkDatum>;
179
185
  },
180
186
  ]
181
187
  >;
@@ -200,6 +206,7 @@
200
206
  stopped = false,
201
207
  static: staticProp,
202
208
  onStart: onStartProp,
209
+ onNodesChange: onNodesChangeProp,
203
210
  onTick: onTickProp,
204
211
  onEnd: onEndProp,
205
212
  children,
@@ -211,15 +218,13 @@
211
218
  // MARK: Private Props
212
219
 
213
220
  let linkPositions: LinkPosition[] = $state([]);
214
- let simulatedNodes: NodeDatumFor<NodeDatum>[] = $state([]);
215
- let simulatedLinks: LinkDatumFor<NodeDatum, LinkDatum>[] = $derived(
216
- (data.links ?? []) as LinkDatumFor<NodeDatum, LinkDatum>[]
217
- );
221
+ let simulatedNodes: NodeDatum[] = $state([]);
222
+ let simulatedLinks: LinkDatum[] = $derived(data.links ?? []);
218
223
 
219
224
  // This casting is unfortunately necessary, due to unfortunate
220
225
  // overloading choices made, over at `@typed/d3-force`:
221
- const simulation: SimulationFor<NodeDatum, LinkDatum> = (
222
- forceSimulation() as SimulationFor<NodeDatum, LinkDatum>
226
+ const simulation: Simulation<NodeDatum, LinkDatum> = (
227
+ forceSimulation<NodeDatum>() as Simulation<NodeDatum, LinkDatum>
223
228
  ).stop();
224
229
 
225
230
  // d3.Simulation does not provide a `.forces()` getter, so we need to
@@ -263,6 +268,7 @@
263
268
  () => {
264
269
  // Any time the `nodes` prop, or the `data` store gets changed
265
270
  // we pass them to the internal d3 simulation object:
271
+ onNodesChange();
266
272
  pushNodesToSimulation(data.nodes);
267
273
  runOrResumeSimulation();
268
274
  }
@@ -498,6 +504,16 @@
498
504
  });
499
505
  }
500
506
 
507
+ function onNodesChange() {
508
+ onNodesChangeProp?.({
509
+ alpha,
510
+ alphaTarget,
511
+ nodes: data.nodes,
512
+ links: data.links ?? [],
513
+ simulation,
514
+ });
515
+ }
516
+
501
517
  $effect(() => {
502
518
  return () => {
503
519
  simulation.stop();
@@ -14,19 +14,26 @@ export type LinkPosition = {
14
14
  export type OnStartEvent<NodeDatum extends SimulationNodeDatum, LinkDatum extends SimulationLinkDatum<NodeDatum> | undefined> = {
15
15
  alpha: number;
16
16
  alphaTarget: number;
17
- simulation: SimulationFor<NodeDatum, LinkDatum>;
17
+ simulation: Simulation<NodeDatum, LinkDatum>;
18
18
  };
19
19
  export type OnTickEvent<NodeDatum extends SimulationNodeDatum, LinkDatum extends SimulationLinkDatum<NodeDatum> | undefined> = {
20
20
  alpha: number;
21
21
  alphaTarget: number;
22
- nodes: NodeDatumFor<NodeDatum>[];
23
- links: LinkDatumFor<NodeDatum, LinkDatum>[];
24
- simulation: SimulationFor<NodeDatum, LinkDatum>;
22
+ nodes: NodeDatum[];
23
+ links: LinkDatum[];
24
+ simulation: Simulation<NodeDatum, LinkDatum>;
25
25
  };
26
26
  export type OnEndEvent<NodeDatum extends SimulationNodeDatum, LinkDatum extends SimulationLinkDatum<NodeDatum> | undefined> = {
27
27
  alpha: number;
28
28
  alphaTarget: number;
29
- simulation: SimulationFor<NodeDatum, LinkDatum>;
29
+ simulation: Simulation<NodeDatum, LinkDatum>;
30
+ };
31
+ export type OnNodesChangeEvent<NodeDatum extends SimulationNodeDatum, LinkDatum extends SimulationLinkDatum<NodeDatum> | undefined> = {
32
+ alpha: number;
33
+ alphaTarget: number;
34
+ nodes: NodeDatum[];
35
+ links: LinkDatum[];
36
+ simulation: Simulation<NodeDatum, LinkDatum>;
30
37
  };
31
38
  /**
32
39
  * Default initial alpha value of the simulation.
@@ -50,9 +57,6 @@ export declare const DEFAULT_ALPHA_MIN: number;
50
57
  * Default velocity decay factor applied to nodes each tick.
51
58
  */
52
59
  export declare const DEFAULT_VELOCITY_DECAY: number;
53
- type NodeDatumFor<NodeDatum> = NodeDatum & SimulationNodeDatum;
54
- type LinkDatumFor<NodeDatum, LinkDatum> = LinkDatum & SimulationLinkDatum<NodeDatumFor<NodeDatum>>;
55
- type SimulationFor<NodeDatum, LinkDatum> = Simulation<NodeDatumFor<NodeDatum>, LinkDatumFor<NodeDatum, LinkDatum>>;
56
60
  export type ForceSimulationProps<NodeDatum extends SimulationNodeDatum, LinkDatum extends SimulationLinkDatum<NodeDatum> | undefined> = {
57
61
  /**
58
62
  * Force simulation parameters
@@ -107,6 +111,10 @@ export type ForceSimulationProps<NodeDatum extends SimulationNodeDatum, LinkDatu
107
111
  * Callback function triggered when simulation starts
108
112
  */
109
113
  onStart?: (e: OnStartEvent<NodeDatum, LinkDatum | undefined>) => void;
114
+ /**
115
+ * Callback function triggered right before nodes get passed to the simulation
116
+ */
117
+ onNodesChange?: (e: OnNodesChangeEvent<NodeDatum, LinkDatum | undefined>) => void;
110
118
  /**
111
119
  * Callback function triggered on each simulation tick
112
120
  */
@@ -117,10 +125,10 @@ export type ForceSimulationProps<NodeDatum extends SimulationNodeDatum, LinkDatu
117
125
  onEnd?: (e: OnEndEvent<NodeDatum, LinkDatum | undefined>) => void;
118
126
  children?: Snippet<[
119
127
  {
120
- nodes: NodeDatumFor<NodeDatum>[];
121
- links: LinkDatumFor<NodeDatum, LinkDatum>[];
128
+ nodes: NodeDatum[];
129
+ links: LinkDatum[];
122
130
  linkPositions: LinkPosition[];
123
- simulation: SimulationFor<NodeDatum, LinkDatum>;
131
+ simulation: Simulation<NodeDatum, LinkDatum>;
124
132
  }
125
133
  ]>;
126
134
  };
@@ -116,7 +116,7 @@
116
116
  import { notNull } from '@layerstack/utils';
117
117
  import { cls } from '@layerstack/tailwind';
118
118
 
119
- import { isScaleBand } from '../utils/scales.svelte.js';
119
+ import { isScaleBand, isScaleTime } from '../utils/scales.svelte.js';
120
120
  import { asAny } from '../utils/types.js';
121
121
  import { getChartContext } from './Chart.svelte';
122
122
  import { getTooltipContext } from './tooltip/TooltipContext.svelte';
@@ -158,7 +158,9 @@
158
158
  Array.isArray(yValue) ? yValue.map((v) => ctx.yScale(v)) : ctx.yScale(yValue)
159
159
  );
160
160
  const yOffset = $derived(isScaleBand(ctx.yScale) && !ctx.radial ? ctx.yScale.bandwidth() / 2 : 0);
161
- const axis = $derived(axisProp == null ? (isScaleBand(ctx.yScale) ? 'y' : 'x') : axisProp);
161
+ const axis = $derived(
162
+ axisProp == null ? (isScaleBand(ctx.yScale) || isScaleTime(ctx.yScale) ? 'y' : 'x') : axisProp
163
+ );
162
164
 
163
165
  const _lines: { x1: number; y1: number; x2: number; y2: number }[] = $derived.by(() => {
164
166
  let tmpLines: { x1: number; y1: number; x2: number; y2: number }[] = [];
@@ -249,6 +251,7 @@
249
251
  height: 0,
250
252
  };
251
253
  if (!highlightData) return tmpArea;
254
+
252
255
  if (axis === 'x' || axis === 'both') {
253
256
  // x area
254
257
  if (Array.isArray(xCoord)) {
@@ -284,9 +287,9 @@
284
287
  tmpArea.height = ctx.yScale.step();
285
288
  } else {
286
289
  // Find width to next data point
287
- const index = ctx.flatData.findIndex((d) => Number(x(d)) === Number(x(highlightData)));
290
+ const index = ctx.flatData.findIndex((d) => Number(y(d)) === Number(y(highlightData)));
288
291
  const isLastPoint = index + 1 === ctx.flatData.length;
289
- const nextDataPoint = isLastPoint ? max(ctx.yDomain) : x(ctx.flatData[index + 1]);
292
+ const nextDataPoint = isLastPoint ? max(ctx.yDomain) : y(ctx.flatData[index + 1]);
290
293
  tmpArea.height = (ctx.yScale(nextDataPoint) ?? 0) - (yCoord ?? 0);
291
294
  }
292
295
 
@@ -141,6 +141,8 @@
141
141
  bandPadding = radial ? 0 : 0.4,
142
142
  groupPadding = 0,
143
143
  stackPadding = 0,
144
+ xInterval,
145
+ yInterval,
144
146
  tooltip = true,
145
147
  children: childrenProp,
146
148
  aboveContext,
@@ -211,21 +213,25 @@
211
213
 
212
214
  const xScale = $derived(
213
215
  xScaleProp ??
214
- (isVertical
215
- ? scaleBand().padding(bandPadding)
216
- : accessor(xProp)(chartData[0]) instanceof Date // TODO: also check for Array<Date> instances (ex. x={['start', 'end']})
217
- ? scaleTime()
218
- : scaleLinear())
216
+ (xInterval
217
+ ? scaleTime()
218
+ : isVertical
219
+ ? scaleBand().padding(bandPadding)
220
+ : accessor(xProp)(chartData[0]) instanceof Date // TODO: also check for Array<Date> instances (ex. x={['start', 'end']})
221
+ ? scaleTime()
222
+ : scaleLinear())
219
223
  );
220
224
  const xBaseline = $derived(isVertical || isScaleTime(xScale) ? undefined : 0);
221
225
 
222
226
  const yScale = $derived(
223
227
  yScaleProp ??
224
- (isVertical
225
- ? accessor(yProp)(chartData[0]) instanceof Date // TODO: also check for Array<Date> instances (ex. y={['start', 'end']})
226
- ? scaleTime()
227
- : scaleLinear()
228
- : scaleBand().padding(bandPadding))
228
+ (yInterval
229
+ ? scaleTime()
230
+ : isVertical
231
+ ? accessor(yProp)(chartData[0]) instanceof Date // TODO: also check for Array<Date> instances (ex. y={['start', 'end']})
232
+ ? scaleTime()
233
+ : scaleLinear()
234
+ : scaleBand().padding(bandPadding))
229
235
  );
230
236
  const yBaseline = $derived(isVertical || isScaleTime(yScale) ? 0 : undefined);
231
237
 
@@ -436,6 +442,7 @@
436
442
  {x1Scale}
437
443
  {x1Domain}
438
444
  {x1Range}
445
+ {xInterval}
439
446
  y={resolveAccessor(yProp)}
440
447
  {yScale}
441
448
  {yBaseline}
@@ -443,6 +450,7 @@
443
450
  {y1Scale}
444
451
  {y1Domain}
445
452
  {y1Range}
453
+ {yInterval}
446
454
  c={isVertical ? yProp : xProp}
447
455
  cRange={['var(--color-primary)']}
448
456
  {radial}
@@ -135,7 +135,7 @@
135
135
  import ChartClipPath from './../ChartClipPath.svelte';
136
136
  import Voronoi from './../Voronoi.svelte';
137
137
 
138
- import { isScaleBand, scaleInvert } from '../../utils/scales.svelte.js';
138
+ import { isScaleBand, isScaleTime, scaleInvert } from '../../utils/scales.svelte.js';
139
139
  import { cartesianToPolar } from '../../utils/math.js';
140
140
  import { quadtreeRects } from '../../utils/quadtree.js';
141
141
  import { raise } from '../../utils/chart.js';
@@ -493,14 +493,63 @@
493
493
  const fullHeight = max(ctx.yRange) - min(ctx.yRange);
494
494
 
495
495
  if (mode === 'band') {
496
- // full band width/height regardless of value
497
- return {
498
- x: isScaleBand(ctx.xScale) ? x - xOffset : min(ctx.xRange),
499
- y: isScaleBand(ctx.yScale) ? y - yOffset : min(ctx.yRange),
500
- width: isScaleBand(ctx.xScale) ? ctx.xScale.step() : fullWidth,
501
- height: isScaleBand(ctx.yScale) ? ctx.yScale.step() : fullHeight,
502
- data: d,
503
- };
496
+ if (isScaleBand(ctx.xScale)) {
497
+ // full band width/height regardless of value
498
+ return {
499
+ x: x - xOffset,
500
+ y: min(ctx.yRange),
501
+ width: ctx.xScale.step(),
502
+ height: fullHeight,
503
+ data: d,
504
+ };
505
+ } else if (isScaleBand(ctx.yScale)) {
506
+ return {
507
+ x: min(ctx.xRange),
508
+ y: y - yOffset,
509
+ width: fullWidth,
510
+ height: ctx.yScale.step(),
511
+ data: d,
512
+ };
513
+ } else if (isScaleTime(ctx.xScale)) {
514
+ // Find width to next data point
515
+ const index = ctx.flatData.findIndex(
516
+ (d2) => Number(ctx.x(d2)) === Number(ctx.x(d))
517
+ );
518
+ const isLastPoint = index + 1 === ctx.flatData.length;
519
+ const nextDataPoint = isLastPoint
520
+ ? max(ctx.xDomain)
521
+ : ctx.x(ctx.flatData[index + 1]);
522
+
523
+ return {
524
+ x: x - xOffset,
525
+ y: min(ctx.yRange),
526
+ width: (ctx.xScale(nextDataPoint) ?? 0) - (xValue ?? 0),
527
+ height: fullHeight,
528
+ data: d,
529
+ };
530
+ } else if (isScaleTime(ctx.yScale)) {
531
+ // Find height to next data point
532
+ const index = ctx.flatData.findIndex(
533
+ (d2) => Number(ctx.y(d2)) === Number(ctx.y(d))
534
+ );
535
+ const isLastPoint = index + 1 === ctx.flatData.length;
536
+ const nextDataPoint = isLastPoint
537
+ ? max(ctx.yDomain)
538
+ : ctx.y(ctx.flatData[index + 1]);
539
+
540
+ return {
541
+ x: min(ctx.xRange),
542
+ y: y - yOffset,
543
+ width: fullWidth,
544
+ height: (ctx.yScale(nextDataPoint) ?? 0) - (yValue ?? 0),
545
+ data: d,
546
+ };
547
+ } else {
548
+ console.warn(
549
+ '[layerchart] TooltipContext band mode requires at least one scale to be band or time.'
550
+ );
551
+ return undefined;
552
+ }
504
553
  } else if (mode === 'bounds') {
505
554
  return {
506
555
  x: isScaleBand(ctx.xScale) || Array.isArray(xValue) ? x - xOffset : min(ctx.xRange),
@@ -36,10 +36,10 @@ export declare function createDimensionGetter<TData>(ctx: ChartContextValue<TDat
36
36
  y: any;
37
37
  width: number;
38
38
  height: number;
39
- };
39
+ } | undefined;
40
40
  /**
41
41
  * If value is an array, returns first item, else returns original value
42
42
  * Useful when x/y getters for band scale are an array (such as for histograms)
43
43
  */
44
- export declare function firstValue(value: number | number[]): number;
44
+ export declare function firstValue(value: number | number[] | undefined): number | undefined;
45
45
  export {};
@@ -59,7 +59,7 @@ export function createDimensionGetter(ctx, getOptions) {
59
59
  const width = Math.max(0, ctx.xScale(right) - ctx.xScale(left) - insets.left - insets.right);
60
60
  return { x, y, width, height };
61
61
  }
62
- else {
62
+ else if (isScaleBand(ctx.xScale)) {
63
63
  // Vertical band or linear
64
64
  const x = firstValue(ctx.xScale(_x(item))) + (ctx.x1Scale ? ctx.x1Scale(_x1(item)) : 0) + insets.left;
65
65
  const width = Math.max(0, ctx.xScale.bandwidth
@@ -94,6 +94,74 @@ export function createDimensionGetter(ctx, getOptions) {
94
94
  const height = ctx.yScale(bottom) - ctx.yScale(top) - insets.bottom - insets.top;
95
95
  return { x, y, width, height };
96
96
  }
97
+ else if (ctx.xInterval) {
98
+ // x-axis time scale with interval
99
+ const xValue = _x(item);
100
+ const start = ctx.xInterval.floor(xValue);
101
+ const end = ctx.xInterval.offset(start);
102
+ const x = ctx.xScale(start) + insets.left;
103
+ const width = ctx.xScale(end) - x - insets.right;
104
+ const yValue = _y(item);
105
+ let top = 0;
106
+ let bottom = 0;
107
+ if (Array.isArray(yValue)) {
108
+ // Array contains both top and bottom values (stack, etc);
109
+ top = max(yValue);
110
+ bottom = min(yValue);
111
+ }
112
+ else if (yValue == null) {
113
+ // null/undefined value
114
+ top = 0;
115
+ bottom = 0;
116
+ }
117
+ else if (yValue > 0) {
118
+ // Positive value
119
+ top = yValue;
120
+ bottom = max([0, yDomainMinMax[0]]);
121
+ }
122
+ else {
123
+ // Negative value
124
+ top = min([0, yDomainMinMax[1]]);
125
+ bottom = yValue;
126
+ }
127
+ const y = ctx.yScale(top) + insets.top;
128
+ const height = ctx.yScale(bottom) - ctx.yScale(top) - insets.bottom - insets.top;
129
+ return { x, y, width, height };
130
+ }
131
+ else if (ctx.yInterval) {
132
+ // y-axis time scale with interval
133
+ const yValue = _y(item);
134
+ const start = ctx.yInterval.floor(yValue);
135
+ const end = ctx.yInterval.offset(start);
136
+ const y = ctx.yScale(start) + insets.top;
137
+ const height = ctx.yScale(end) - y - insets.bottom;
138
+ const xValue = _x(item);
139
+ let left = 0;
140
+ let right = 0;
141
+ if (Array.isArray(xValue)) {
142
+ // Array contains both top and bottom values (stack, etc);
143
+ left = min(xValue);
144
+ right = max(xValue);
145
+ }
146
+ else if (xValue == null) {
147
+ // null/undefined value
148
+ left = 0;
149
+ right = 0;
150
+ }
151
+ else if (xValue > 0) {
152
+ // Positive value
153
+ left = max([0, xDomainMinMax[0]]);
154
+ right = xValue;
155
+ }
156
+ else {
157
+ // Negative value
158
+ left = xValue;
159
+ right = min([0, xDomainMinMax[1]]);
160
+ }
161
+ const x = ctx.xScale(left) + insets.left;
162
+ const width = ctx.xScale(right) - x - insets.right;
163
+ return { x, y, width, height };
164
+ }
97
165
  };
98
166
  }
99
167
  /**
@@ -9,7 +9,7 @@ export declare function getDurationFormat(duration: Duration, options?: {
9
9
  export type TicksConfig = number | any[] | ((scale: AnyScale) => any[] | undefined) | {
10
10
  interval: TimeInterval | null;
11
11
  } | null;
12
- export declare function resolveTickVals(scale: AnyScale, ticks?: TicksConfig, count?: number): any[];
12
+ export declare function resolveTickVals(scale: AnyScale, ticks?: TicksConfig, count?: number, interval?: TimeInterval | null): any[];
13
13
  export declare function resolveTickFormat(options: {
14
14
  scale: AnyScale;
15
15
  ticks?: TicksConfig;
@@ -110,7 +110,7 @@ export function getDurationFormat(duration, options = {
110
110
  }
111
111
  };
112
112
  }
113
- export function resolveTickVals(scale, ticks, count) {
113
+ export function resolveTickVals(scale, ticks, count, interval) {
114
114
  // Explicit ticks
115
115
  if (Array.isArray(ticks))
116
116
  return ticks;
@@ -132,7 +132,12 @@ export function resolveTickVals(scale, ticks, count) {
132
132
  }
133
133
  // Ticks from scale
134
134
  if (scale.ticks && typeof scale.ticks === 'function') {
135
- return scale.ticks(count ?? (typeof ticks === 'number' ? ticks : undefined));
135
+ const tickVals = scale.ticks(count ?? (typeof ticks === 'number' ? ticks : undefined));
136
+ if (interval) {
137
+ // Remove last tick when interval is provided (such as for bar charts with center aligned (offset) ticks)
138
+ tickVals.pop();
139
+ }
140
+ return tickVals;
136
141
  }
137
142
  return [];
138
143
  }
@@ -54,4 +54,9 @@ describe('resolveTickVals', () => {
54
54
  expect(resolveTickVals(scale, null, undefined)).toEqual([1, 2, 3]);
55
55
  expect(scale.ticks).toHaveBeenCalledWith(undefined);
56
56
  });
57
+ it('removes last tick when interval is provided', () => {
58
+ const interval = { every: vi.fn() };
59
+ const scale = { ticks: vi.fn(() => [1, 2, 3, 4]) };
60
+ expect(resolveTickVals(scale, undefined, undefined, interval)).toEqual([1, 2, 3]);
61
+ });
57
62
  });
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "author": "Sean Lynch <techniq35@gmail.com>",
5
5
  "license": "MIT",
6
6
  "repository": "techniq/layerchart",
7
- "version": "2.0.0-next.30",
7
+ "version": "2.0.0-next.31",
8
8
  "devDependencies": {
9
9
  "@changesets/cli": "^2.29.4",
10
10
  "@iconify-json/lucide": "^1.2.48",