Skip to content

Commit 9309197

Browse files
committed
fix: balance rotation/tickEstimates/tickSkipping
1 parent 3141edf commit 9309197

File tree

6 files changed

+107
-66
lines changed

6 files changed

+107
-66
lines changed

examples/line/src/index.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ export default function App() {
1313
useLagRadar();
1414

1515
const { data, randomizeData } = useDemoConfig({
16-
series: 10
16+
series: 10,
1717
});
1818

1919
const series = React.useMemo(
2020
() => ({
21-
showPoints: false
21+
showPoints: false,
2222
}),
2323
[]
2424
);
@@ -29,10 +29,10 @@ export default function App() {
2929
primary: true,
3030
type: "time",
3131
position: "bottom",
32-
filterTicks: ticks =>
33-
ticks.filter(date => +timeDay.floor(date) === +date)
32+
filterTicks: (ticks) =>
33+
ticks.filter((date) => +timeDay.floor(date) === +date),
3434
},
35-
{ type: "linear", position: "left" }
35+
{ type: "linear", position: "left" },
3636
],
3737
[]
3838
);

src/components/AxisLinear.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '../utils/Constants.js'
1717
import useChartContext from '../hooks/useChartContext'
1818
import useMeasure from './AxisLinear.useMeasure'
19+
import useChartState from '../hooks/useChartState'
1920

2021
const defaultStyles = {
2122
line: {
@@ -49,6 +50,7 @@ export default function AxisLinear(axis) {
4950
tickOffset,
5051
gridOffset,
5152
spacing,
53+
id,
5254
} = axis
5355
const [rotation, setRotation] = React.useState(0)
5456
const { gridWidth, gridHeight, dark } = useChartContext()
@@ -57,6 +59,10 @@ export default function AxisLinear(axis) {
5759

5860
useMeasure({ ...axis, elRef, rotation, gridWidth, gridHeight, setRotation })
5961

62+
const [tickLabelSkipRatio] = useChartState(
63+
state => state.tickLabelSkipRatios[id]
64+
)
65+
6066
// Not ready? Render null
6167
if (!show) {
6268
return null
@@ -184,6 +190,11 @@ export default function AxisLinear(axis) {
184190
vertical ? directionMultiplier * spacing : tickOffset,
185191
vertical ? tickOffset : directionMultiplier * spacing
186192
)} rotate(${-rotation}deg)`,
193+
opacity:
194+
!tickLabelSkipRatio ||
195+
(tickLabelSkipRatio > 0 && i % tickLabelSkipRatio === 0)
196+
? 1
197+
: 0,
187198
}}
188199
dominantBaseline={
189200
rotation

src/components/AxisLinear.useMeasure.js

Lines changed: 79 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import React from 'react'
22
import useChartState from '../hooks/useChartState'
33
import useIsomorphicLayoutEffect from '../hooks/useIsomorphicLayoutEffect'
4-
import usePrevious from '../hooks/usePrevious'
4+
import { axisTypeOrdinal } from '../utils/Constants'
55
import { round } from '../utils/Utils'
66

7-
const radiansToDegrees = r => r * (180 / Math.PI)
8-
const degreesToRadians = d => d * (Math.PI / 180)
9-
107
const getElBox = el => {
118
var rect = el.getBoundingClientRect()
129
return {
@@ -30,8 +27,6 @@ export default function useMeasure({
3027
position,
3128
tickSizeInner,
3229
tickSizeOuter,
33-
transform,
34-
barSize,
3530
labelRotation,
3631
tickPadding,
3732
tickCount,
@@ -40,19 +35,20 @@ export default function useMeasure({
4035
vertical,
4136
gridWidth,
4237
gridHeight,
43-
ticks,
44-
scale,
4538
}) {
46-
const disallowListRef = React.useRef([])
47-
const axisDimensionsHistoryRef = React.useRef([])
48-
49-
const [axisDimensions, setChartState] = useChartState(
50-
state => state.axisDimensions
39+
const [estimatedTickCount, setChartState] = useChartState(
40+
state => state.estimatedTickCounts[id]
41+
)
42+
const [tickLabelSkipRatio] = useChartState(
43+
state => state.tickLabelSkipRatios[id]
44+
)
45+
const [axisDimension] = useChartState(
46+
state => state.axisDimensions?.[position]?.[id]
5147
)
5248

5349
const measureDimensions = React.useCallback(() => {
5450
if (!elRef.current) {
55-
if (axisDimensions[position]?.[id]) {
51+
if (axisDimension) {
5652
// If the entire axis is hidden, then we need to remove the axis dimensions
5753
setChartState(state => {
5854
const newAxes = state.axisDimensions[position] || {}
@@ -70,30 +66,13 @@ export default function useMeasure({
7066
}
7167

7268
let gridSize = !vertical ? gridWidth : gridHeight
73-
let calculatedTickCount = tickCount
7469
let width = 0
7570
let height = 0
7671
let top = 0
7772
let bottom = 0
7873
let left = 0
7974
let right = 0
8075

81-
// Measure the distances between ticks
82-
let smallestTickGap = gridSize // This is just a ridiculously large tick spacing that would never happen (hopefully)
83-
84-
Array(...elRef.current.querySelectorAll('.tick'))
85-
.map(el => getElBox(el))
86-
.reduce((prev, current) => {
87-
if (prev) {
88-
smallestTickGap = Math.min(
89-
smallestTickGap,
90-
vertical ? current.top - prev.top : current.left - prev.left
91-
)
92-
}
93-
94-
return current
95-
}, false)
96-
9776
const domainDims = getElBox(elRef.current.querySelector('.domain'))
9877

9978
const measureLabelDims = Array(
@@ -134,26 +113,70 @@ export default function useMeasure({
134113
[]
135114
)
136115

137-
// Auto-fit ticks in "auto" tick mode
138-
if (tickCount === 'auto' && type !== 'ordinal') {
139-
const largestMeasureLabelSize = !vertical
140-
? widestMeasureLabel?.width || 0
141-
: tallestMeasureLabel?.height || 0
116+
let smallestTickGap = gridSize
117+
118+
if (measureLabelDims.length > 1) {
119+
measureLabelDims.reduce((prev, current) => {
120+
if (prev) {
121+
smallestTickGap = Math.min(
122+
smallestTickGap,
123+
vertical ? current.top - prev.top : current.left - prev.left
124+
)
125+
}
126+
127+
return current
128+
}, false)
129+
}
142130

131+
const largestMeasureLabelSize = !vertical
132+
? widestMeasureLabel?.width || 0
133+
: tallestMeasureLabel?.height || 0
134+
135+
// Auto-fit ticks in "auto" tick mode for non-ordinal scales
136+
if (tickCount === 'auto' && type !== 'ordinal') {
143137
// if it's on, determine how many ticks we could display if they were all flat
144138
// How many ticks can we fit in the available axis space?
145-
const estimatedTickCount = Math.floor(
146-
(gridSize + largestMeasureLabelSize + tickPadding) /
147-
(largestMeasureLabelSize + tickPadding)
139+
let calculatedTickCount = Math.floor(
140+
(gridSize + largestMeasureLabelSize + tickPadding * 2) /
141+
(largestMeasureLabelSize + tickPadding * 2)
148142
)
149143

150144
calculatedTickCount = Math.max(
151-
Math.min(estimatedTickCount, maxTickCount),
145+
Math.min(calculatedTickCount, maxTickCount),
152146
minTickCount
153147
)
148+
149+
if (calculatedTickCount !== estimatedTickCount) {
150+
setChartState(old => ({
151+
...old,
152+
estimatedTickCounts: {
153+
...old.estimatedTickCounts,
154+
[id]: calculatedTickCount,
155+
},
156+
}))
157+
}
154158
}
155159

156-
if (!vertical) {
160+
// Visual Skipping of axis labels if they overlap (rotation not included)
161+
const newTickLabelSkipRatio = !rotation
162+
? Math.max(
163+
1,
164+
Math.ceil((largestMeasureLabelSize + tickPadding) / smallestTickGap)
165+
)
166+
: 1
167+
168+
if (newTickLabelSkipRatio !== tickLabelSkipRatio) {
169+
setChartState(old => ({
170+
...old,
171+
tickLabelSkipRatios: {
172+
...old.tickLabelSkipRatios,
173+
[id]: newTickLabelSkipRatio,
174+
},
175+
}))
176+
}
177+
178+
// Rotate ticks for non-time horizontal axes
179+
if (!vertical && type === axisTypeOrdinal) {
157180
const newRotation =
158181
(widestMeasureLabel?.width || 0) + tickPadding > smallestTickGap
159182
? labelRotation
@@ -166,12 +189,16 @@ export default function useMeasure({
166189

167190
// Axis overflow measurements
168191
if (!vertical) {
169-
const leftMostLabelDim = realLabelDims.reduce((d, labelDim) =>
170-
labelDim.left < d.left ? labelDim : d
171-
)
172-
const rightMostLabelDim = realLabelDims.reduce((d, labelDim) =>
173-
labelDim.right > d.right ? labelDim : d
174-
)
192+
const leftMostLabelDim = realLabelDims.length
193+
? realLabelDims.reduce((d, labelDim) =>
194+
labelDim.left < d.left ? labelDim : d
195+
)
196+
: null
197+
const rightMostLabelDim = realLabelDims.length
198+
? realLabelDims.reduce((d, labelDim) =>
199+
labelDim.right > d.right ? labelDim : d
200+
)
201+
: null
175202

176203
left = round(
177204
Math.max(0, domainDims.left - leftMostLabelDim?.left),
@@ -229,14 +256,13 @@ export default function useMeasure({
229256
bottom,
230257
left,
231258
right,
232-
tickCount: calculatedTickCount,
233259
}
234260

235261
// Only update the axisDimensions if something has changed
236262
if (
237-
!axisDimensions?.[position]?.[id] ||
263+
!axisDimension ||
238264
Object.keys(newDimensions).some(key => {
239-
return newDimensions[key] !== axisDimensions[position][id][key]
265+
return newDimensions[key] !== axisDimension[key]
240266
})
241267
) {
242268
setChartState(state => ({
@@ -251,8 +277,9 @@ export default function useMeasure({
251277
}))
252278
}
253279
}, [
254-
axisDimensions,
280+
axisDimension,
255281
elRef,
282+
estimatedTickCount,
256283
gridHeight,
257284
gridWidth,
258285
id,
@@ -264,6 +291,7 @@ export default function useMeasure({
264291
setChartState,
265292
setRotation,
266293
tickCount,
294+
tickLabelSkipRatio,
267295
tickPadding,
268296
tickSizeInner,
269297
tickSizeOuter,

src/components/Chart.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ export default function ChartState(options) {
9999
hovered: null,
100100
element: null,
101101
axisDimensions: {},
102+
estimatedTickCounts: {},
103+
tickLabelSkipRatios: {},
102104
offset: {},
103105
pointer: {},
104106
setOffset,
@@ -136,11 +138,14 @@ export function Chart(options) {
136138
...rest
137139
} = applyDefaults(options)
138140

139-
const [{ hovered, element, axisDimensions, pointer }] = useChartState(
141+
const [
142+
{ hovered, element, axisDimensions, estimatedTickCounts, pointer },
143+
] = useChartState(
140144
d => ({
141145
hovered: d.hovered,
142146
element: d.element,
143147
axisDimensions: d.axisDimensions,
148+
estimatedTickCounts: d.estimatedTickCounts,
144149
pointer: d.pointer,
145150
}),
146151
'shallow'
@@ -177,7 +182,7 @@ export function Chart(options) {
177182
materializedData,
178183
gridHeight,
179184
gridWidth,
180-
axisDimensions,
185+
estimatedTickCounts,
181186
})
182187

183188
const stackData = useStackData({

src/components/pipeline/useAxes.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default ({
77
materializedData,
88
gridHeight,
99
gridWidth,
10-
axisDimensions,
10+
estimatedTickCounts,
1111
}) => {
1212
// Detect axes changes and build axes
1313
let prePrimaryAxes = axes.filter(d => d.primary)
@@ -25,7 +25,7 @@ export default ({
2525
materializedData,
2626
gridWidth,
2727
gridHeight,
28-
axisDimensions,
28+
estimatedTickCounts,
2929
})
3030
})
3131
},
@@ -42,7 +42,7 @@ export default ({
4242
materializedData,
4343
gridWidth,
4444
gridHeight,
45-
axisDimensions,
45+
estimatedTickCounts,
4646
})
4747
})
4848
},

src/utils/buildAxis.linear.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export default function buildAxisLinear({
6060
materializedData,
6161
gridHeight,
6262
gridWidth,
63-
axisDimensions,
63+
estimatedTickCounts,
6464
}) {
6565
if (!position) {
6666
throw new Error(`Chart axes must have a valid 'position' property`)
@@ -84,9 +84,6 @@ export default function buildAxisLinear({
8484
let positiveTotalByKey = {}
8585
let domain
8686

87-
const axisDimension =
88-
axisDimensions && axisDimensions[position] && axisDimensions[position][id]
89-
9087
// Loop through each series
9188
for (
9289
let seriesIndex = 0;
@@ -262,7 +259,7 @@ export default function buildAxisLinear({
262259
let resolvedTickCount = tickCount
263260

264261
if (tickCount === 'auto') {
265-
resolvedTickCount = axisDimension?.tickCount || 10
262+
resolvedTickCount = estimatedTickCounts[id] ?? 10
266263
}
267264

268265
const ticks = filterTicks(

0 commit comments

Comments
 (0)