Deploy to git-pages / deploy (push) Successful in 12s
The district-name watermark inside the arrests-over-time chart was redundant with the page's own heading and was the direct cause of a label collision (a high point's rate label rendered on top of it). Removes it. Two more collisions, both edge cases the screenshot happened to hit: the first wave's label sat close enough to the plot's top that it could overlap the topmost gridline text, and a point sitting flush on the plot's left edge had its center-anchored label bleed into the y-axis tick labels. Fixes both with more vertical clearance and edge-aware text anchoring (first point anchors right, last point anchors left, matching the same fix already applied to chart 4's wave ticks before that chart was cut). Adds a shared niceTicks() utility (Heckbert's nice-numbers algorithm) so axis ticks read as round numbers (0/100/200) instead of arbitrary fractions of the data max (0/113/226/339) — applied to all three charts for consistency. Also fixes a legend/mark mismatch in the arrests-over-time chart (the modeled series legend showed a diamond; the actual mark is a dot) by adding a proper 'dot' shape to ChartLegend. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
41 lines
1.4 KiB
JavaScript
41 lines
1.4 KiB
JavaScript
/**
|
|
* "Nice numbers" axis tick generator (Heckbert 1990) — produces clean,
|
|
* human-friendly tick values (0 / 50 / 100 / 150 rather than 0 / 37 / 74 /
|
|
* 111, an artifact of slicing frac*max into equal fractions) for any linear
|
|
* axis, from just the data's max value.
|
|
*/
|
|
function niceNumber(range, round) {
|
|
const exponent = Math.floor(Math.log10(range))
|
|
const fraction = range / 10 ** exponent
|
|
let niceFraction
|
|
if (round) {
|
|
if (fraction < 1.5) niceFraction = 1
|
|
else if (fraction < 3) niceFraction = 2
|
|
else if (fraction < 7) niceFraction = 5
|
|
else niceFraction = 10
|
|
} else {
|
|
if (fraction <= 1) niceFraction = 1
|
|
else if (fraction <= 2) niceFraction = 2
|
|
else if (fraction <= 5) niceFraction = 5
|
|
else niceFraction = 10
|
|
}
|
|
return niceFraction * 10 ** exponent
|
|
}
|
|
|
|
/**
|
|
* @param {number} maxValue - the largest value the axis must cover
|
|
* @param {number} tickCount - approximate desired number of ticks
|
|
* @returns {{ ticks: number[], niceMax: number }}
|
|
*/
|
|
export function niceTicks(maxValue, tickCount = 5) {
|
|
if (!(maxValue > 0)) return { ticks: [0], niceMax: 1 }
|
|
const niceRange = niceNumber(maxValue, false)
|
|
const niceStep = niceNumber(niceRange / (tickCount - 1), true)
|
|
const niceMax = Math.ceil(maxValue / niceStep) * niceStep
|
|
const ticks = []
|
|
for (let v = 0; v <= niceMax + niceStep / 2; v += niceStep) {
|
|
ticks.push(Math.round(v * 1e6) / 1e6)
|
|
}
|
|
return { ticks, niceMax }
|
|
}
|