Skip to content

Commit db7647f

Browse files
authored
Add "auto" option to Coordinates.Cartesian (#176)
Added experimental "auto" option for cartesian coordinates that will automatically change the amount of gridlines based on zoom level. No config options for it yet, it just enables it with some default settings if you pass `"auto"` into `xAxis` or `yAxis`. https://github.com/user-attachments/assets/0c1dcbce-1eb2-4d44-baf4-cf50a0eff2ff
1 parent ba0d596 commit db7647f

File tree

2 files changed

+67
-9
lines changed

2 files changed

+67
-9
lines changed

src/display/Coordinates/Cartesian.tsx

+50-9
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,72 @@
11
import * as React from "react"
2-
import { range, round } from "../../math"
2+
import { pickClosestToValue, range, round, roundToNearestPowerOf10 } from "../../math"
33
import { usePaneContext } from "../../context/PaneContext"
44
import { useTransformContext } from "../../context/TransformContext"
55
import { vec } from "../../vec"
66
import { XLabels, YLabels, AxisOptions, defaultAxisOptions } from "./Axes"
7+
import { useCoordinateContext } from "../../context/CoordinateContext"
8+
import { useSpanContext } from "../../context/SpanContext"
79

810
// This is sort of a hack—every SVG pattern on a page needs a unique ID, otherwise they conflict.
911
let incrementer = 0
1012

1113
type GridAxisOptions = Partial<AxisOptions & { subdivisions: number | false }>
1214

1315
export interface CartesianCoordinatesProps {
14-
xAxis?: GridAxisOptions | false
15-
yAxis?: GridAxisOptions | false
16+
xAxis?: GridAxisOptions | false | "auto"
17+
yAxis?: GridAxisOptions | false | "auto"
1618
subdivisions?: number | false
1719
}
1820

1921
export function Cartesian({
20-
xAxis: xAxisOverrides,
21-
yAxis: yAxisOverrides,
22+
xAxis: xAxisProp,
23+
yAxis: yAxisProp,
2224
subdivisions = false,
2325
}: CartesianCoordinatesProps) {
24-
const xAxisEnabled = xAxisOverrides !== false
25-
const yAxisEnabled = yAxisOverrides !== false
26+
const xAxisEnabled = xAxisProp !== false
27+
const yAxisEnabled = yAxisProp !== false
28+
29+
// Better name? Also `3.5` is a magic number that makes it feel right to me,
30+
// it could have any other value (and perhaps it should be computed).
31+
// It *kind of* represents how many major grid lines can be on screen at once,
32+
// e.g. if it's 4 then there can be at most 4 major gridlines on the canvas.
33+
//
34+
// it seems like its behavior isn't quite that simple but it roughly explains why we divide at all here
35+
36+
const mathWidth = useSpanContext().xSpan / 3.5
37+
38+
const nearestPowerOf10 = roundToNearestPowerOf10(mathWidth)
39+
40+
// This should be a prop or something later, just a constant right now
41+
const multiples = [
42+
{ value: 1, subdivisions: 5 },
43+
{ value: 2, subdivisions: 4 },
44+
{ value: 5, subdivisions: 5 },
45+
]
46+
47+
const autoClosest = pickClosestToValue(
48+
mathWidth,
49+
multiples.map((multiple) => nearestPowerOf10 * multiple.value),
50+
)
2651

27-
const xAxis = { subdivisions, ...defaultAxisOptions, ...xAxisOverrides } as GridAxisOptions
28-
const yAxis = { subdivisions, ...defaultAxisOptions, ...yAxisOverrides } as GridAxisOptions
52+
const autoLines = autoClosest[0];
53+
const autoSubdivisions = multiples[autoClosest[1]].subdivisions
54+
55+
const xAxisOverrides =
56+
xAxisProp === "auto" ? { lines: autoLines, subdivisions: autoSubdivisions } : xAxisProp !== false ? xAxisProp : false
57+
const yAxisOverrides =
58+
yAxisProp === "auto" ? { lines: autoLines, subdivisions: autoSubdivisions } : yAxisProp !== false ? yAxisProp : false
59+
60+
const xAxis = {
61+
subdivisions,
62+
...defaultAxisOptions,
63+
...xAxisOverrides,
64+
} as GridAxisOptions
65+
const yAxis = {
66+
subdivisions,
67+
...defaultAxisOptions,
68+
...yAxisOverrides,
69+
} as GridAxisOptions
2970

3071
const id = React.useMemo(() => `cartesian-${incrementer++}`, [])
3172

src/math.ts

+17
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ export function round(value: number, precision = 0): number {
66
return Math.round(value * multiplier) / multiplier
77
}
88

9+
// TODO: Maybe better name (even though this one perfectly describes its purpose)
10+
export function roundToNearestPowerOf10(value: number): number {
11+
return 10 ** Math.floor(Math.log10(value))
12+
}
13+
14+
export function pickClosestToValue(value: number, options: number[]) {
15+
// [Distance from value to option, index of option]
16+
const distanceMap = options
17+
.map((option, i) => [Math.abs(option - value), i])
18+
.sort((a, b) => a[0] - b[0])
19+
20+
const closest = distanceMap[0]
21+
22+
// [value, index]
23+
return [options[closest[1]], closest[1]]
24+
}
25+
926
export function range(min: number, max: number, step = 1): number[] {
1027
const result = []
1128
for (let i = min; i < max - step / 2; i += step) {

0 commit comments

Comments
 (0)