Skip to content

Commit e5ccdae

Browse files
authored
Time interval (#562)
* Fix jsdoc typo * feat(Chart): Add `xInterval` / `yInterval` for time scales usage with bar charts * Fix merge * fix(Axis): Offset 1/2 time interval * Add missing data example * Automatically calculate xDomain / yDomain when using xInterval / yInterval * Fix handling of yInterval (horizontal bar chart) * feat(BarChart): Support time scale with `xInterval` / `yInterval` props * docs(BarChart): Fix example order * fix(Bar): Fix edge rounding for vertical bars * docs(BarChart): Add interval inset example * fix(Axis): Remove last tick when xInterval / yInterval is provided (ex. bar chart) * fix(Highlight): Properly handle area highlights with y-axis time scales * feat(TooltipContext): Support `band` mode with time scale (similar to band scale) * Switch back to using band mode for all BarChart (time or band scales) * Fix typo * Improve candlestick example (until Rule is updated) * docs(BarChart): Update brush example to use interval (time scale) which is better supported ATM.
1 parent 816c348 commit e5ccdae

18 files changed

Lines changed: 469 additions & 57 deletions

File tree

.changeset/eleven-corners-float.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'layerchart': patch
3+
---
4+
5+
fix(Highlight): Properly handle area highlights with y-axis time scales

.changeset/funny-otters-kick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'layerchart': patch
3+
---
4+
5+
feat(TooltipContext): Support `band` mode with time scale (similar to band scale)

.changeset/new-turtles-clean.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'layerchart': patch
3+
---
4+
5+
feat(BarChart): Support time scale with `xInterval` / `yInterval` props

.changeset/sad-chairs-stand.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'layerchart': patch
3+
---
4+
5+
feat(Chart): Add `xInterval` / `yInterval` for time scales usage with bar charts

packages/layerchart/src/lib/components/Axis.svelte

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
ticks?: TicksConfig;
4545
4646
/**
47-
* Width or height of each tick in pxiels (responsive reduce)
47+
* Width or height of each tick in pixels (enabling responsive count)
4848
*/
4949
tickSpacing?: number;
5050
@@ -96,7 +96,7 @@
9696
transitionInParams?: TransitionParams<In>;
9797
9898
/**
99-
* Scale for the axis
99+
* Override scale for the axis
100100
*/
101101
scale?: any;
102102
@@ -183,6 +183,9 @@
183183
const scale = $derived(
184184
scaleProp ?? (['horizontal', 'angle'].includes(orientation) ? ctx.xScale : ctx.yScale)
185185
);
186+
const interval = $derived(
187+
['horizontal', 'angle'].includes(orientation) ? ctx.xInterval : ctx.yInterval
188+
);
186189
187190
const xRangeMinMax = $derived(extent<number>(ctx.xRange)) as [number, number];
188191
const yRangeMinMax = $derived(extent<number>(ctx.yRange)) as [number, number];
@@ -206,7 +209,7 @@
206209
? Math.round(ctxSize / tickSpacing)
207210
: undefined
208211
);
209-
const tickVals = $derived(resolveTickVals(scale, ticks, tickCount));
212+
const tickVals = $derived(resolveTickVals(scale, ticks, tickCount, interval));
210213
const tickFormat = $derived(
211214
resolveTickFormat({
212215
scale,
@@ -221,27 +224,29 @@
221224
function getCoords(tick: any) {
222225
switch (placement) {
223226
case 'top':
224-
return {
225-
x: scale(tick) + (isScaleBand(scale) ? scale.bandwidth() / 2 : 0),
226-
y: yRangeMinMax[0],
227-
};
228-
229227
case 'bottom':
230228
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],
233237
};
234238
235239
case 'left':
236-
return {
237-
x: xRangeMinMax[0],
238-
y: scale(tick) + (isScaleBand(scale) ? scale.bandwidth() / 2 : 0),
239-
};
240-
241240
case 'right':
242241
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),
245250
};
246251
247252
case 'angle':

packages/layerchart/src/lib/components/Bar.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
import Rect from './Rect.svelte';
8282
import Spline from './Spline.svelte';
8383
84-
import { isScaleBand } from '../utils/scales.svelte.js';
84+
import { isScaleBand, isScaleTime } from '../utils/scales.svelte.js';
8585
import { accessor, type Accessor } from '../utils/common.js';
8686
import { getChartContext } from './Chart.svelte';
8787
import type { CommonStyleProps, Without } from '$lib/utils/types.js';
@@ -127,7 +127,7 @@
127127
128128
const dimensions = $derived(getDimensions(data) ?? { x: 0, y: 0, width: 0, height: 0 });
129129
130-
const isVertical = $derived(isScaleBand(ctx.xScale));
130+
const isVertical = $derived(isScaleBand(ctx.xScale) || isScaleTime(ctx.xScale));
131131
const valueAccessor = $derived(accessor(isVertical ? y : x));
132132
const value = $derived(valueAccessor(data));
133133
const resolvedValue = $derived(Array.isArray(value) ? greatestAbs(value) : value);

packages/layerchart/src/lib/components/Chart.svelte

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
createScale,
88
getRange,
99
isScaleBand,
10+
isScaleTime,
1011
makeAccessor,
1112
type AnyScale,
1213
type DomainType,
@@ -40,6 +41,7 @@
4041
import TransformContext, { type TransformContextValue } from './TransformContext.svelte';
4142
import BrushContext, { type BrushContextValue } from './BrushContext.svelte';
4243
import { layerClass } from '$lib/utils/attributes.js';
44+
import type { TimeInterval } from 'd3-time';
4345
4446
const defaultPadding = { top: 0, right: 0, bottom: 0, left: 0 };
4547
@@ -149,6 +151,8 @@
149151
cGet: (d: T) => any;
150152
x1Get: (d: T) => any;
151153
y1Get: (d: T) => any;
154+
xInterval: TimeInterval | null;
155+
yInterval: TimeInterval | null;
152156
radial: boolean;
153157
tooltip: TooltipContextValue<T>;
154158
geo: GeoContextValue;
@@ -640,6 +644,16 @@
640644
*/
641645
yBaseline?: number | null;
642646
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+
643657
/* Props passed to ChartContext */
644658
645659
/**
@@ -738,6 +752,8 @@
738752
rRange: rRangeProp,
739753
xBaseline = null,
740754
yBaseline = null,
755+
xInterval = null,
756+
yInterval = null,
741757
meta = {},
742758
children: _children,
743759
radial = false,
@@ -780,6 +796,12 @@
780796
781797
const _xDomain: DomainType | undefined = $derived.by(() => {
782798
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+
783805
if (xBaseline != null && Array.isArray(data)) {
784806
const xValues = data.flatMap(accessor(xProp));
785807
return [min([xBaseline, ...xValues]), max([xBaseline, ...xValues])];
@@ -788,6 +810,12 @@
788810
789811
const _yDomain: DomainType | undefined = $derived.by(() => {
790812
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+
791819
if (yBaseline != null && Array.isArray(data)) {
792820
const yValues = data.flatMap(accessor(yProp));
793821
return [min([yBaseline, ...yValues]), max([yBaseline, ...yValues])];
@@ -798,7 +826,9 @@
798826
_yRangeProp ?? (radial ? ({ height }: { height: number }) => [0, height / 2] : undefined)
799827
);
800828
801-
const yReverse = $derived(yScaleProp ? !isScaleBand(yScaleProp) : true);
829+
const yReverse = $derived(
830+
yScaleProp ? !isScaleBand(yScaleProp) && !isScaleTime(yScaleProp) : true
831+
);
802832
803833
const x = $derived(makeAccessor(xProp));
804834
const y = $derived(makeAccessor(yProp));
@@ -1247,6 +1277,12 @@
12471277
get y1Scale() {
12481278
return y1Scale;
12491279
},
1280+
get xInterval() {
1281+
return xInterval;
1282+
},
1283+
get yInterval() {
1284+
return yInterval;
1285+
},
12501286
get radial() {
12511287
return radial;
12521288
},

packages/layerchart/src/lib/components/Highlight.svelte

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@
116116
import { notNull } from '@layerstack/utils';
117117
import { cls } from '@layerstack/tailwind';
118118
119-
import { isScaleBand } from '$lib/utils/scales.svelte.js';
119+
import { isScaleBand, isScaleTime } from '$lib/utils/scales.svelte.js';
120120
import { asAny } from '$lib/utils/types.js';
121121
import { getChartContext } from './Chart.svelte';
122122
import { getTooltipContext } from './tooltip/TooltipContext.svelte';
@@ -158,7 +158,9 @@
158158
Array.isArray(yValue) ? yValue.map((v) => ctx.yScale(v)) : ctx.yScale(yValue)
159159
);
160160
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+
);
162164
163165
const _lines: { x1: number; y1: number; x2: number; y2: number }[] = $derived.by(() => {
164166
let tmpLines: { x1: number; y1: number; x2: number; y2: number }[] = [];
@@ -249,6 +251,7 @@
249251
height: 0,
250252
};
251253
if (!highlightData) return tmpArea;
254+
252255
if (axis === 'x' || axis === 'both') {
253256
// x area
254257
if (Array.isArray(xCoord)) {
@@ -284,9 +287,9 @@
284287
tmpArea.height = ctx.yScale.step();
285288
} else {
286289
// 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)));
288291
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]);
290293
tmpArea.height = (ctx.yScale(nextDataPoint) ?? 0) - (yCoord ?? 0);
291294
}
292295

packages/layerchart/src/lib/components/charts/BarChart.svelte

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@
141141
bandPadding = radial ? 0 : 0.4,
142142
groupPadding = 0,
143143
stackPadding = 0,
144+
xInterval,
145+
yInterval,
144146
tooltip = true,
145147
children: childrenProp,
146148
aboveContext,
@@ -211,21 +213,25 @@
211213
212214
const xScale = $derived(
213215
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())
219223
);
220224
const xBaseline = $derived(isVertical || isScaleTime(xScale) ? undefined : 0);
221225
222226
const yScale = $derived(
223227
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))
229235
);
230236
const yBaseline = $derived(isVertical || isScaleTime(yScale) ? 0 : undefined);
231237
@@ -436,13 +442,15 @@
436442
{x1Scale}
437443
{x1Domain}
438444
{x1Range}
445+
{xInterval}
439446
y={resolveAccessor(yProp)}
440447
{yScale}
441448
{yBaseline}
442449
yNice={orientation === 'vertical'}
443450
{y1Scale}
444451
{y1Domain}
445452
{y1Range}
453+
{yInterval}
446454
c={isVertical ? yProp : xProp}
447455
cRange={['var(--color-primary)']}
448456
{radial}

0 commit comments

Comments
 (0)