Skip to content

Commit d7055cb

Browse files
auto tick placement
1 parent c0df810 commit d7055cb

2 files changed

Lines changed: 110 additions & 11 deletions

File tree

client/src/components/sidebar/game/quick-line-chart.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,11 @@ export const QuickLineChart: React.FC<LineChartProps> = ({ data, width, height,
5858
margin,
5959
{
6060
range: { min: 0, max: data.length },
61-
options: {
62-
count: 8
63-
}
61+
options: {}
6462
},
6563
{
6664
range: { min: 0, max: max },
67-
options: {
68-
count: 8
69-
}
65+
options: {}
7066
}
7167
)
7268
}, [data.length, height, margin, width])

client/src/util/graph-util.ts

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { Axis } from 'd3'
33
export function getAxes(
44
width: number,
55
height: number,
6-
margin: { top: number; right: number; bottom: number; left: number },
6+
rawMargin: { top: number; right: number; bottom: number; left: number },
77
size: { x: number; y: number }
88
) {
9-
// +/- 1 is to avoid overlapping axis lines
10-
const xScale = (value: number) => (value * (width - margin.left - margin.right)) / size.x + margin.left + 1
11-
const yScale = (value: number) =>
12-
height - margin.bottom - (value / size.y) * (height - margin.top - margin.bottom) - 1
9+
// +1 is to avoid overlapping axis lines
10+
const margin = { ...rawMargin, left: rawMargin.left + 1, bottom: rawMargin.bottom + 1 }
11+
const xScale = (value: number) => (value * (width - margin.left - margin.right)) / size.x + margin.left
12+
const yScale = (value: number) => height - margin.bottom - (value / size.y) * (height - margin.top - margin.bottom)
1313
const innerWidth = width - margin.left - margin.right
1414
const innerHeight = height - margin.top - margin.bottom
1515
return { xScale, yScale, innerWidth, innerHeight }
@@ -62,6 +62,57 @@ export function drawXAxis(
6262
const textWidth = context.measureText(label).width
6363
context.fillText(label, xPos - textWidth / 2, height)
6464
}
65+
} else {
66+
// auto mode
67+
const MIN_TICKS = 4
68+
const MAX_TICKS = 10
69+
const { min, max } = range
70+
const rangeSize = max - min
71+
const roughInterval = rangeSize / ((MAX_TICKS + MIN_TICKS) / 2)
72+
let interval = Math.pow(10, Math.ceil(Math.log10(roughInterval))) * 10
73+
let refinedInterval = 0
74+
while (!refinedInterval && interval >= 1) {
75+
interval = interval / 10
76+
refinedInterval =
77+
interval *
78+
([1, 2, 5].find(
79+
(multiplier) =>
80+
rangeSize / (interval * multiplier) <= MAX_TICKS &&
81+
rangeSize / (interval * multiplier) >= MIN_TICKS
82+
) || 0)
83+
}
84+
85+
if (!refinedInterval) {
86+
refinedInterval = Math.pow(10, Math.ceil(Math.log10(roughInterval)))
87+
}
88+
89+
// Calculate tick positions
90+
const start = Math.ceil(min / refinedInterval) * refinedInterval
91+
const end = Math.floor(max / refinedInterval) * refinedInterval
92+
const ticks = []
93+
for (let tick = start; tick <= end; tick += refinedInterval) {
94+
ticks.push(tick)
95+
}
96+
97+
// Ensure a tick at 0 if within range
98+
if (min <= 0 && max >= 0 && !ticks.includes(0)) {
99+
ticks.push(0)
100+
ticks.sort((a, b) => a - b)
101+
}
102+
103+
// Render ticks and labels
104+
ticks.forEach((tick) => {
105+
const xPos = margin.left + (tick - min) / valueRangeScale
106+
context.beginPath()
107+
context.moveTo(xPos, height - margin.bottom)
108+
context.lineTo(xPos, height - margin.bottom + 6)
109+
context.stroke()
110+
111+
context.fillStyle = options.textColor ?? 'black'
112+
const label = (tick != Math.round(tick) ? tick.toFixed(1) : tick).toString()
113+
const textWidth = context.measureText(label).width
114+
context.fillText(label, xPos - textWidth / 2, height)
115+
})
65116
}
66117
}
67118
export function drawYAxis(
@@ -104,6 +155,58 @@ export function drawYAxis(
104155
const textWidth = context.measureText(label).width
105156
context.fillText(label, margin.left - textWidth - 10, yPos + 5)
106157
}
158+
} else {
159+
// auto mode for y-axis
160+
const MIN_TICKS = 3
161+
const MAX_TICKS = 10
162+
const { min, max } = range
163+
const rangeSize = max - min
164+
const roughInterval = rangeSize / ((MAX_TICKS + MIN_TICKS) / 2)
165+
let interval = Math.pow(10, Math.ceil(Math.log10(roughInterval))) * 10
166+
let refinedInterval = 0
167+
168+
while (!refinedInterval && interval >= 1) {
169+
interval = interval / 10
170+
refinedInterval =
171+
interval *
172+
([1, 2, 5].find(
173+
(multiplier) =>
174+
rangeSize / (interval * multiplier) <= MAX_TICKS &&
175+
rangeSize / (interval * multiplier) >= MIN_TICKS
176+
) || 0)
177+
}
178+
179+
if (!refinedInterval) {
180+
refinedInterval = Math.pow(10, Math.ceil(Math.log10(roughInterval)))
181+
}
182+
183+
// Calculate tick positions
184+
const start = Math.ceil(min / refinedInterval) * refinedInterval
185+
const end = Math.floor(max / refinedInterval) * refinedInterval
186+
const ticks = []
187+
for (let tick = start; tick <= end; tick += refinedInterval) {
188+
ticks.push(tick)
189+
}
190+
191+
// Ensure a tick at 0 if within range
192+
if (min <= 0 && max >= 0 && !ticks.includes(0)) {
193+
ticks.push(0)
194+
ticks.sort((a, b) => a - b)
195+
}
196+
197+
// Render ticks and labels
198+
ticks.forEach((tick) => {
199+
const yPos = height - margin.bottom - (tick - min) / valueRangeScale
200+
context.beginPath()
201+
context.moveTo(margin.left, yPos)
202+
context.lineTo(margin.left - 6, yPos)
203+
context.stroke()
204+
205+
context.fillStyle = options.textColor ?? 'black'
206+
const label = (tick != Math.round(tick) ? tick.toFixed(1) : tick).toString()
207+
const textWidth = context.measureText(label).width
208+
context.fillText(label, margin.left - textWidth - 10, yPos + 5)
209+
})
107210
}
108211
}
109212

0 commit comments

Comments
 (0)