1import { Controller } from "@hotwired/stimulus" 2 3// Console v2 trend chart: one series, 8 weekly points, rendered as an inline SVG line. 4// One hue for the line; judgment lives in the server-rendered delta chip, never in the 5// line color. Hover shows the actual number and the week's dates (Amir 2026-07-28). 6// points value: [{ label: "Jul 14âJul 20", value: 12 | null }, ...]; baseline value optional. 7export default class extends Controller { 8 static targets = ["svg", "tip"] 9 static values = { points: Array, baseline: Number, unit: String } 10 11 connect() { 12 this.draw() 13 this.svgTarget.addEventListener("mousemove", e => this.hover(e)) 14 this.svgTarget.addEventListener("mouseleave", () => this.tipTarget.classList.add("hidden")) 15 } 16 17 draw() { 18 const pts = this.pointsValue 19 const known = pts.filter(p => p.value !== null).map(p => p.value) 20 if (!known.length) { this.svgTarget.innerHTML = "" ; return } 21 const all = this.hasBaselineValue && this.baselineValue ? known.concat([this.baselineValue]) : known 22 const lo = Math.min(...all), hi = Math.max(...all), span = (hi - lo) || 1 23 this.X = i => 8 + i * (204 / (pts.length - 1)) 24 this.Y = v => 46 - ((v - lo) / span) * 36 25 let s = "" 26 if (this.hasBaselineValue && this.baselineValue) 27 s += `<line x1="8" y1="${this.Y(this.baselineValue)}" x2="212" y2="${this.Y(this.baselineValue)}" 28 stroke="#9ca3af" stroke-dasharray="4 3" stroke-width="1"/>` 29 // gaps (null weeks) break the line rather than faking continuity 30 let run = [] 31 const flush = () => { 32 if (run.length > 1) s += `<polyline fill="none" stroke="#4f46e5" stroke-width="2" stroke-linejoin="round" 33 stroke-linecap="round" points="${run.join(" ")}"/>` 34 run = [] 35 } 36 pts.forEach((p, i) => { p.value === null ? flush() : run.push(`${this.X(i)},${this.Y(p.value)}`) }) 37 flush() 38 pts.forEach((p, i) => { 39 if (p.value === null) return 40 const last = i === pts.length - 1 41 s += `<circle cx="${this.X(i)}" cy="${this.Y(p.value)}" r="${last ? 3.5 : 2}" 42 fill="#4f46e5" ${last ? 'stroke="#fff" stroke-width="1.5"' : 'opacity="0.35"'}/>` 43 }) 44 this.svgTarget.innerHTML = s 45 } 46 47 hover(e) { 48 const pts = this.pointsValue 49 const rect = this.svgTarget.getBoundingClientRect() 50 const x = ((e.clientX - rect.left) / rect.width) * 220 51 let best = null 52 pts.forEach((p, i) => { 53 if (p.value === null) return 54 const d = Math.abs(this.X(i) - x) 55 if (!best || d < best.d) best = { p, i, d } 56 }) 57 if (!best) return 58 this.tipTarget.textContent = `${best.p.label}: ${best.p.value}${this.unitValue || ""}` 59 this.tipTarget.classList.remove("hidden") 60 const px = (this.X(best.i) / 220) * rect.width 61 this.tipTarget.style.left = `${Math.min(Math.max(px, 40), rect.width - 40)}px` 62 } 63}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.