Skip to content

Commit 40ebca1

Browse files
authored
feat: AutoScale (#624)
* feat(Chart): Automatically determine scale based on data and domain values (instead of defaulting to scaleLinear) * Remove `cScale={scaleOrdinal()}` usage (already default) * Cleanup yNice={#} (since axis ticks now support `tickSpacing`) * Setup autoScale for x1Scale and y1Scale * docs: Improve bar padding examples
1 parent e8d235e commit 40ebca1

20 files changed

Lines changed: 168 additions & 365 deletions

File tree

.changeset/shaky-dots-go.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): Automatically determine scale based on data and domain values (instead of defaulting to scaleLinear)

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

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<script lang="ts" module>
2-
import { scaleLinear, scaleOrdinal, scaleSqrt } from 'd3-scale';
2+
import { scaleOrdinal, scaleSqrt } from 'd3-scale';
33
import { type Accessor, accessor, chartDataArray } from '$lib/utils/common.js';
44
import { printDebug } from '$lib/utils/debug.js';
55
import { filterObject } from '$lib/utils/filterObject.js';
66
import {
7+
autoScale,
78
createScale,
89
getRange,
910
isScaleBand,
@@ -421,21 +422,21 @@
421422
/**
422423
* The D3 scale that should be used for the x-dimension. Pass in an instantiated D3 scale if
423424
* you want to override the default or you want to extra options.
424-
* @default scaleLinear
425+
* @default autoScale
425426
*/
426427
xScale?: XScale;
427428
428429
/**
429430
* The D3 scale that should be used for the x-dimension. Pass in an instantiated D3 scale if
430431
* you want to override the default or you want to extra options.
431-
* @default scaleLinear
432+
* @default autoScale
432433
*/
433434
yScale?: YScale;
434435
435436
/**
436437
* The D3 scale that should be used for the x-dimension. Pass in an instantiated D3 scale if
437438
* you want to override the default or you want to extra options.
438-
* @default scaleLinear
439+
* @default autoScale
439440
*/
440441
zScale?: AnyScale;
441442
@@ -449,14 +450,14 @@
449450
/**
450451
* The D3 scale that should be used for the x1-dimension. Pass in an instantiated D3 scale if
451452
* you want to override the default or you want to extra options.
452-
* @default scaleLinear
453+
* @default autoScale
453454
*/
454455
x1Scale?: AnyScale;
455456
456457
/**
457458
* The D3 scale that should be used for the y1-dimension. Pass in an instantiated D3 scale if
458459
* you want to override the default or you want to extra options.
459-
* @default scaleLinear
460+
* @default autoScale
460461
*/
461462
y1Scale?: AnyScale;
462463
@@ -717,6 +718,7 @@
717718
z: zProp,
718719
r: rProp,
719720
data = [],
721+
flatData: flatDataProp,
720722
xDomain: xDomainProp,
721723
yDomain: yDomainProp,
722724
zDomain: zDomainProp,
@@ -730,12 +732,12 @@
730732
zPadding,
731733
rPadding,
732734
// @ts-expect-error shh
733-
xScale: xScaleProp = scaleLinear(),
735+
xScale: xScaleProp = autoScale(xDomainProp, flatDataProp ?? data, xProp),
736+
// @ts-expect-error shh
737+
yScale: yScaleProp = autoScale(yDomainProp, flatDataProp ?? data, yProp),
734738
// @ts-expect-error shh
735-
yScale: yScaleProp = scaleLinear(),
736-
zScale: zScaleProp = scaleLinear(),
739+
zScale: zScaleProp = autoScale(zDomainProp, flatDataProp ?? data, zProp),
737740
rScale: rScaleProp = scaleSqrt(),
738-
flatData: flatDataProp,
739741
padding: paddingProp = {},
740742
verbose = true,
741743
debug = false,
@@ -1009,24 +1011,36 @@
10091011
const rGet = $derived(createGetter(r, rScale));
10101012
10111013
const x1Scale = $derived(
1012-
x1ScaleProp && x1RangeProp
1013-
? createScale(x1ScaleProp, x1Domain, x1RangeProp, {
1014-
xScale: xScale,
1015-
width,
1016-
height,
1017-
})
1014+
x1RangeProp
1015+
? createScale(
1016+
// @ts-expect-error shh
1017+
x1ScaleProp ?? autoScale(x1DomainProp, flatDataProp ?? data, x1Prop),
1018+
x1Domain,
1019+
x1RangeProp,
1020+
{
1021+
xScale,
1022+
width,
1023+
height,
1024+
}
1025+
)
10181026
: null
10191027
);
10201028
10211029
const x1Get = $derived(createGetter(x1, x1Scale));
10221030
10231031
const y1Scale = $derived(
1024-
y1ScaleProp && y1RangeProp
1025-
? createScale(y1ScaleProp, y1Domain, y1RangeProp, {
1026-
yScale: yScale,
1027-
width,
1028-
height,
1029-
})
1032+
y1RangeProp
1033+
? createScale(
1034+
// @ts-expect-error shh
1035+
y1ScaleProp ?? autoScale(y1DomainProp, flatDataProp ?? data, y1Prop),
1036+
y1Domain,
1037+
y1RangeProp,
1038+
{
1039+
yScale,
1040+
width,
1041+
height,
1042+
}
1043+
)
10301044
: null
10311045
);
10321046

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@
122122
renderContext = 'svg',
123123
profile = false,
124124
debug = false,
125-
xScale: xScaleProp,
126125
children: childrenProp,
127126
aboveContext,
128127
belowContext,
@@ -196,11 +195,6 @@
196195
return _chartData;
197196
});
198197
199-
// Default xScale based on first data's `x` value
200-
const xScale = $derived(
201-
xScaleProp ?? (accessor(x)(chartData[0]) instanceof Date ? scaleTime() : scaleLinear())
202-
);
203-
204198
function isStackData(d: TData): d is TData & { stackData: any[] } {
205199
return d && typeof d === 'object' && 'stackData' in d;
206200
}
@@ -433,7 +427,6 @@
433427
data={chartData}
434428
{x}
435429
{xDomain}
436-
{xScale}
437430
y={resolveAccessor(y)}
438431
yBaseline={0}
439432
yNice

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@
6969

7070
<script lang="ts" generics="TData">
7171
import { onMount, type ComponentProps } from 'svelte';
72-
import { scaleLinear, scaleTime } from 'd3-scale';
7372
import { cls } from '@layerstack/tailwind';
7473
7574
import Axis from '../Axis.svelte';
@@ -124,7 +123,6 @@
124123
renderContext = 'svg',
125124
profile = false,
126125
debug = false,
127-
xScale: xScaleProp,
128126
tooltip = true,
129127
children: childrenProp,
130128
aboveContext,
@@ -159,11 +157,6 @@
159157
: chartDataArray(data)) as Array<TData>
160158
);
161159
162-
// Default xScale based on first data's `x` value
163-
const xScale = $derived(
164-
xScaleProp ?? (accessor(xProp)(chartData[0]) instanceof Date ? scaleTime() : scaleLinear())
165-
);
166-
167160
function getSplineProps(s: SeriesData<TData, typeof Spline>, i: number) {
168161
const splineProps: ComponentProps<typeof Spline> = {
169162
data: s.data,
@@ -333,7 +326,6 @@
333326
data={chartData}
334327
x={xProp}
335328
{xDomain}
336-
{xScale}
337329
y={yProp ?? series.map((s) => s.value ?? s.key)}
338330
yBaseline={0}
339331
yNice

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

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
</script>
4545

4646
<script lang="ts" generics="TData">
47-
import { scaleLinear, scaleTime } from 'd3-scale';
4847
import { format } from '@layerstack/utils';
4948
import { cls } from '@layerstack/tailwind';
5049
@@ -88,8 +87,6 @@
8887
renderContext = 'svg',
8988
profile = false,
9089
debug = false,
91-
xScale: xScaleProp,
92-
yScale: yScaleProp,
9390
children: childrenProp,
9491
aboveContext,
9592
belowContext,
@@ -107,18 +104,6 @@
107104
108105
const seriesState = new SeriesState(() => series);
109106
110-
// Default xScale based on first data's `x` value
111-
const xScale = $derived(
112-
xScaleProp ??
113-
(accessor(xProp)(chartDataArray(data)[0]) instanceof Date ? scaleTime() : scaleLinear())
114-
);
115-
116-
// Default yScale based on first data's `y` value
117-
const yScale = $derived(
118-
yScaleProp ??
119-
(accessor(yProp)(chartDataArray(data)[0]) instanceof Date ? scaleTime() : scaleLinear())
120-
);
121-
122107
const chartData = $derived(
123108
seriesState.visibleSeries
124109
.flatMap((s) => s.data?.map((d) => ({ seriesKey: s.key, ...d })))
@@ -241,10 +226,8 @@
241226
data={chartData}
242227
x={xProp}
243228
{xDomain}
244-
{xScale}
245229
y={yProp}
246230
{yDomain}
247-
{yScale}
248231
yNice
249232
c={yProp}
250233
cRange={['var(--color-primary)']}

packages/layerchart/src/lib/utils/scales.svelte.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { unique } from '@layerstack/utils';
2-
import { scaleBand, type ScaleBand, type ScaleTime } from 'd3-scale';
2+
import { scaleBand, scaleLinear, scaleTime, type ScaleBand, type ScaleTime } from 'd3-scale';
33
import {
44
createControlledMotion,
55
type MotionProp,
@@ -8,7 +8,7 @@ import {
88
type TweenOptions,
99
} from '$lib/utils/motion.svelte.js';
1010
import { Spring, Tween } from 'svelte/motion';
11-
import type { Accessor } from './common.js';
11+
import { accessor, type Accessor } from './common.js';
1212
import type { OnlyObjects } from './types.js';
1313
import type { TimeInterval } from 'd3-time';
1414

@@ -162,6 +162,44 @@ export function createScale(
162162
return scaleCopy;
163163
}
164164

165+
/**
166+
* Auto-detect scale type based on domain values or data values
167+
*/
168+
export function autoScale(
169+
domain?: DomainType,
170+
data?: any[],
171+
propAccessor?: Accessor<any>
172+
): AnyScale {
173+
let values = null;
174+
if (domain && domain.length > 0) {
175+
// Determine based on domain values
176+
values = domain;
177+
} else if (data && data.length > 0 && propAccessor) {
178+
// Determine based on data values
179+
const value = accessor(propAccessor)(data[0]);
180+
181+
// If accessor defined with an array (ex. `x={['start', 'end']}`) use both values
182+
if (Array.isArray(value)) {
183+
values = value;
184+
} else {
185+
values = [value];
186+
}
187+
}
188+
189+
if (values) {
190+
if (values.some((v) => v instanceof Date)) {
191+
return scaleTime();
192+
} else if (values.some((v) => typeof v === 'number')) {
193+
return scaleLinear();
194+
} else if (values.some((v) => typeof v === 'string')) {
195+
return scaleBand();
196+
}
197+
}
198+
199+
// fallback to linear scale
200+
return scaleLinear();
201+
}
202+
165203
/**
166204
* Create a `scaleBand()` within another scaleBand()'s bandwidth
167205
* (typically a x1 of an x0 scale, used for grouping)

0 commit comments

Comments
 (0)