|
592 | 592 | const gap = 2; |
593 | 593 | const bw = Math.max(3, Math.min(46, (VW - 2 * padX) / n - gap)); |
594 | 594 | const step = bw + gap; |
| 595 | + // A value on top of every bar when they're wide enough to fit the label without |
| 596 | + // colliding; when too narrow (many months) fall back to labelling just the tallest. |
| 597 | + const valueEach = step >= 34; |
595 | 598 | const x0 = (VW - (n * step - gap)) / 2; |
596 | 599 | const plotH = VH - padT - padB; |
597 | 600 | const svg = s('svg', { viewBox: '0 0 ' + VW + ' ' + VH, class: 'chart', role: 'img', |
598 | 601 | 'aria-label': 'spend by month' }); |
599 | 602 | for (const f of [0.5, 1]) { |
600 | 603 | const y = padT + (1 - f) * plotH; |
601 | 604 | svg.appendChild(s('line', { x1: x0, y1: y, x2: VW - x0, y2: y, stroke: thc('line'), 'stroke-width': 1 })); |
602 | | - // the peak bar carries its own direct label, so only the midline gets an axis label |
603 | | - if (f !== 1) svg.appendChild(s('text', { x: VW - x0, y: y - 4, 'text-anchor': 'end', 'font-size': 10, fill: thc('mut'), text: moneyLabel(peak * f) })); |
| 605 | + // The midline gets an axis label only when the bars aren't individually labelled; |
| 606 | + // with per-bar values it's redundant and collides with the rightmost bar's label. |
| 607 | + if (f !== 1 && !valueEach) svg.appendChild(s('text', { x: VW - x0, y: y - 4, 'text-anchor': 'end', 'font-size': 10, fill: thc('mut'), text: moneyLabel(peak * f) })); |
604 | 608 | } |
605 | 609 | svg.appendChild(s('line', { x1: x0, y1: VH - padB, x2: VW - x0, y2: VH - padB, stroke: thc('axis'), 'stroke-width': 1 })); |
606 | 610 | const peakIdx = rows.findIndex(r => r.cost === Math.max(...rows.map(q => q.cost))); |
|
613 | 617 | onclick: () => { go('m', r.month); } }); |
614 | 618 | g.appendChild(s('rect', { class: 'hit', x, y: padT, width: step, height: VH - padT - padB })); |
615 | 619 | if (hgt > 0) g.appendChild(s('path', { d: roundTop(x, y, bw, hgt, 3), fill: thc('accent') })); |
616 | | - if (i === peakIdx && r.cost > 0) |
| 620 | + if (r.cost > 0 && (valueEach || i === peakIdx)) |
617 | 621 | g.appendChild(s('text', { x: x + bw / 2, y: y - 5, 'text-anchor': 'middle', 'font-size': 10, fill: thc('ink2'), text: moneyLabel(r.cost) })); |
618 | 622 | if (i % labelEvery === 0) |
619 | 623 | g.appendChild(s('text', { x: x + bw / 2, y: VH - 7, 'text-anchor': 'middle', 'font-size': 9.5, fill: thc('mut'), text: monthLabel(r.month) })); |
|
1136 | 1140 | const gap = n > 40 ? 1.5 : 3; |
1137 | 1141 | const bw = Math.max(2, Math.min(52, (VW - 2 * padX) / n - gap)); |
1138 | 1142 | const step = bw + gap, x0 = (VW - (n * step - gap)) / 2, plotH = VH - padT - padB; |
| 1143 | + // Label every bar with its own value when the bars are wide enough to fit the text |
| 1144 | + // without colliding; when too narrow (a fully packed month, like Weekly over many |
| 1145 | + // weeks) fall back to labelling just the tallest. Daily keeps bars wide by charting |
| 1146 | + // only its active days (trendDaily), so the common case labels every bar. |
| 1147 | + const valueEach = step >= 38; |
1139 | 1148 | const svg = s('svg', { viewBox: '0 0 ' + VW + ' ' + VH, class: 'tr-chart', role: 'img', 'aria-label': opts.aria || 'trend chart' }); |
1140 | 1149 | for (const f of [0.5, 1]) { |
1141 | 1150 | const y = padT + (1 - f) * plotH; |
1142 | 1151 | svg.appendChild(s('line', { x1: x0, y1: y, x2: VW - x0, y2: y, stroke: thc('line'), 'stroke-width': 1 })); |
1143 | | - if (f !== 1) svg.appendChild(s('text', { x: VW - x0, y: y - 4, 'text-anchor': 'end', 'font-size': 11, fill: thc('mut'), text: moneyLabel(peak * f) })); |
| 1152 | + // The midline gets an axis label only when the bars aren't individually labelled; |
| 1153 | + // with per-bar values it's redundant and collides with the rightmost bar's label. |
| 1154 | + if (f !== 1 && !valueEach) svg.appendChild(s('text', { x: VW - x0, y: y - 4, 'text-anchor': 'end', 'font-size': 11, fill: thc('mut'), text: moneyLabel(peak * f) })); |
1144 | 1155 | } |
1145 | 1156 | svg.appendChild(s('line', { x1: x0, y1: VH - padB, x2: VW - x0, y2: VH - padB, stroke: thc('axis'), 'stroke-width': 1 })); |
1146 | 1157 | const peakVal = Math.max(...vals), peakIdx = vals.indexOf(peakVal), tickEvery = Math.max(1, Math.ceil(n / 18)); |
|
1150 | 1161 | const g = s('g', { class: 'bg', tip: () => (p.tip || (p.label + '\n' + money(p.value))), onclick: p.nav || null }); |
1151 | 1162 | g.appendChild(s('rect', { class: 'hit', x, y: padT, width: step, height: VH - padT - padB })); |
1152 | 1163 | if (hgt > 0) g.appendChild(s('path', { d: roundTop(x, y, bw, hgt, Math.min(3, bw / 2)), fill: thc('accent') })); |
1153 | | - if (i === peakIdx && p.value > 0) |
| 1164 | + if (p.value > 0 && (valueEach || i === peakIdx)) |
1154 | 1165 | g.appendChild(s('text', { x: x + bw / 2, y: y - 5, 'text-anchor': 'middle', 'font-size': 11, fill: thc('ink2'), text: moneyLabel(p.value) })); |
1155 | 1166 | if (i % tickEvery === 0 || i === n - 1) |
1156 | 1167 | g.appendChild(s('text', { x: x + bw / 2, y: VH - 8, 'text-anchor': 'middle', 'font-size': 10, fill: thc('mut'), text: p.label })); |
|
1195 | 1206 | const idx = Math.max(0, Math.min(TRENDS.monthIdx, months.length - 1)), month = months[idx]; |
1196 | 1207 | const byDay = new Map(); |
1197 | 1208 | W.filter(w => w.date.startsWith(month)).forEach(w => { const d = +w.date.slice(8, 10); byDay.set(d, (byDay.get(d) || 0) + cost(w)); }); |
| 1209 | + // Chart only up to the last day that has spend, not the whole calendar month: an |
| 1210 | + // in-progress month (e.g. the current one) shouldn't reserve its empty trailing days, |
| 1211 | + // which squeeze the bars narrow. Trimming keeps them as wide as Weekly/Monthly so each |
| 1212 | + // bar carries its own label instead of colliding. |
| 1213 | + let last = 0; |
| 1214 | + for (let d = 1; d <= daysInMonth(month); d++) if ((byDay.get(d) || 0) > 0) last = d; |
1198 | 1215 | const pairs = []; |
1199 | | - for (let d = 1; d <= daysInMonth(month); d++) { |
| 1216 | + for (let d = 1; d <= last; d++) { |
1200 | 1217 | const date = month + '-' + String(d).padStart(2, '0'), v = byDay.get(d) || 0; |
1201 | 1218 | pairs.push({ label: String(d), value: v, tip: date + '\n' + money(v), nav: v > 0 ? (() => { closeTrends(); go('d', date); }) : null }); |
1202 | 1219 | } |
|
0 commit comments