mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-27 01:00:36 +00:00
Every period headline now comes from one aggregation, so switching period no longer mixes snapshots taken minutes apart and Lifetime can no longer read below the six months inside it. Days preserved from expired transcripts count toward the totals, and the usage streak is one machine-wide number taken from the hydrated daily cache. A provider-filtered hero reads the scoped payload instead of the unscoped one. Anchored menus and popovers render through a portal, so the card anatomy no longer clips them after three rows. The Spend by-branch picker groups checkouts by git origin, one entry per repository with the checkout count beside it. On the performance side, serve invalidation is scoped to the day a changed file can hold, -shm writes are ignored, and the optimize detectors keep one range-independent scan record with ordered version events. Memo budgets are set by retained bytes, and every serve response carries the generation and timestamp it was built from. A 30D stay with appends drops from 7253ms to 2387ms and peak RSS from 2904MB to 1819MB. No cache format changed. Payloads stay byte-identical except for periodTotals, streak and liveSessions.
64 lines
2.6 KiB
TypeScript
64 lines
2.6 KiB
TypeScript
/** Value-axis maths for the bar charts, kept out of the components so it is testable. */
|
|
|
|
const STEPS = [1, 2, 2.5, 5, 10]
|
|
|
|
/** Four or five round ticks from 0 up to at or above `max`, on a 1/2/2.5/5 ladder. */
|
|
export function niceTicks(max: number, count = 4): number[] {
|
|
if (!(max > 0)) return [0]
|
|
const rough = max / count
|
|
const magnitude = 10 ** Math.floor(Math.log10(rough))
|
|
const step = (STEPS.find(factor => factor * magnitude >= rough) ?? 10) * magnitude
|
|
const top = Math.ceil(max / step) * step
|
|
const ticks: number[] = []
|
|
for (let value = 0; value <= top + step / 2; value += step) {
|
|
ticks.push(Number(value.toPrecision(12)))
|
|
}
|
|
return ticks
|
|
}
|
|
|
|
/** Axis money, short enough for a gutter: `$0`, `$300`, `$1.2K`, `$0.50`. */
|
|
export function formatAxisMoney(value: number): string {
|
|
const sign = value < 0 ? '-' : ''
|
|
const size = Math.abs(value)
|
|
if (size === 0) return '$0'
|
|
if (size >= 1000) {
|
|
const thousands = size / 1000
|
|
return `${sign}$${thousands % 1 === 0 ? thousands : thousands.toFixed(1)}K`
|
|
}
|
|
if (size >= 1) return `${sign}$${Math.round(size)}`
|
|
return `${sign}$${size.toFixed(2)}`
|
|
}
|
|
|
|
/** Plot height the axis labels are spaced against, in CSS px (`.chart` / `.sbars`). */
|
|
const PLOT_HEIGHT = 150
|
|
|
|
/** Drops any tick whose label would collide with the peak's own label: the exact figure wins. */
|
|
export function ticksClearOfPeak(ticks: number[], peak: number, axisMax: number, minGap = 15): number[] {
|
|
if (!(axisMax > 0) || !(peak > 0)) return ticks
|
|
return ticks.filter(tick => Math.abs(tick - peak) / axisMax * PLOT_HEIGHT >= minGap)
|
|
}
|
|
|
|
// Narrowest plot the app supports (900px window, sidebar and axis gutter
|
|
// removed). A column set that fits this fits every wider layout, so the chart
|
|
// never overflows its card.
|
|
const MIN_PLOT_PX = 520
|
|
// Bar width and gap in descending comfort. The first row whose columns fit
|
|
// MIN_PLOT_PX wins, so 30D keeps its airy bars and 6M tightens instead of
|
|
// spilling past the card.
|
|
const BAR_STEPS: ReadonlyArray<{ minWidth: number; gap: number }> = [
|
|
{ minWidth: 2, gap: 4 },
|
|
{ minWidth: 2, gap: 2 },
|
|
{ minWidth: 1, gap: 1 },
|
|
{ minWidth: 1, gap: 0 },
|
|
]
|
|
|
|
export function barLayout(count: number): { minWidth: number; gap: number } {
|
|
return BAR_STEPS.find(step => count * step.minWidth + Math.max(0, count - 1) * step.gap <= MIN_PLOT_PX)
|
|
?? BAR_STEPS[BAR_STEPS.length - 1]
|
|
}
|
|
|
|
/** Days per column. Past one column per available pixel the chart aggregates to
|
|
* whole weeks (multiples of 7 keep the x-axis reading as weeks). */
|
|
export function barBucketDays(count: number): number {
|
|
return count <= MIN_PLOT_PX ? 1 : 7 * Math.ceil(count / (7 * MIN_PLOT_PX))
|
|
}
|