PageSourceSearch

https://www.picklebeastpickleball.com/assets/controllers/chart_lin…25de29a7bfe48205dbccdfc7649c4fbe8466f3cc.js

js picklebeastpickleball.com collected 2026-09-24 14:17:35 UTC 9,121 bytes, 274 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3// Shared, CVD-validated, theme-aware --chart-* palette (null if unresolved, so the
4// caller falls back to the legacy hardcoded hex — the chart can never fail to render).
5// Tokens resolve to hex literals, so downstream alphaColor(hex) parsing stays valid.
6function chartTokenPalette() {
7  try {
8    const s = getComputedStyle(document.documentElement)
9    const vals = ["--chart-s1", "--chart-s2", "--chart-s3", "--chart-s4", "--chart-s5", "--chart-s6", "--chart-s7", "--chart-s8", "--chart-other"]
10      .map((v) => s.getPropertyValue(v).trim())
11      .filter((c) => c.startsWith("#"))
12    return vals.length >= 8 ? vals : null
13  } catch (e) {
14    return null
15  }
16}
17
18export default class extends Controller {
19  static targets = ["canvas"]
20  static values = {
21    labels: Array,
22    datasets: Array,
23    currency: { type: Boolean, default: false },
24    title: { type: String, default: "" },
25    stacked: { type: Boolean, default: false },
26    area: { type: Boolean, default: true },
27    priorLabels: { type: Array, default: [] }
28  }
29
30  connect() {
31    if (typeof Chart === "undefined") {
32      console.warn("Chart.js not loaded yet")
33      return
34    }
35    this.render()
36  }
37
38  disconnect() {
39    if (this.chart) {
40      this.chart.destroy()
41      this.chart = null
42    }
43    // Remove custom tooltip element
44    if (this._tooltipEl) {
45      this._tooltipEl.remove()
46      this._tooltipEl = null
47    }
48  }
49
50  render() {
51    const ctx = this.canvasTarget.getContext("2d")
52    const isDark = document.documentElement.getAttribute("data-theme") === "dark"
53    const gridColor = isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)"
54    const textColor = isDark ? "#e4e4e7" : "#64748b"
55
56    const rawDs = this.datasetsValue
57    const hasPrior = this.priorLabelsValue && this.priorLabelsValue.length > 0
58
59    const datasets = rawDs.map((ds, i) => {
60      const color = ds.color || this.palette(i)
61      const isDashed = ds.dashed === true
62      return {
63        label: ds.label || `Series ${i + 1}`,
64        data: ds.data,
65        borderColor: color,
66        backgroundColor: isDashed
67          ? "transparent"
68          : this.areaValue
69            ? this.alphaColor(color, 0.12)
70            : "transparent",
71        fill: isDashed ? false : this.areaValue,
72        tension: 0.35,
73        borderWidth: isDashed ? 1.5 : 2.5,
74        borderDash: isDashed ? [6, 4] : [],
75        pointRadius: isDashed ? 0 : (ds.data.length > 30 ? 0 : 3),
76        pointHoverRadius: isDashed ? 3 : 5,
77        pointBackgroundColor: color,
78        order: isDashed ? 1 : 0,
79      }
80    })
81
82    // Build tooltip config — use custom HTML tooltip when prior data exists
83    const tooltipConfig = hasPrior
84      ? { enabled: false, external: (context) => this.externalTooltip(context) }
85      : {
86          backgroundColor: isDark ? "#2c2c2e" : "#1e293b",
87          titleColor: "#f8fafc",
88          bodyColor: "#f8fafc",
89          borderColor: isDark ? "#404040" : "#334155",
90          borderWidth: 1,
91          padding: 12,
92          cornerRadius: 8,
93          callbacks: this.currencyValue ? {
94            label: (ctx) => `${ctx.dataset.label}: $${ctx.parsed.y.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
95          } : {}
96        }
97
98    this.chart = new Chart(ctx, {
99      type: "line",
100      data: { labels: this.labelsValue, datasets },
101      options: {
102        responsive: true,
103        maintainAspectRatio: false,
104        interaction: { mode: "index", intersect: false },
105        plugins: {
106          legend: {
107            display: datasets.length > 1,
108            position: "top",
109            labels: {
110              color: textColor,
111              padding: 16,
112              font: { size: 12 },
113              usePointStyle: false,
114              boxWidth: 24,
115              boxHeight: 2,
116              generateLabels: (chart) => {
117                return chart.data.datasets.map((ds, i) => {
118                  const isDashed = rawDs[i]?.dashed === true
119                  return {
120                    text: ds.label,
121                    fillStyle: "transparent",
122                    strokeStyle: ds.borderColor,
123                    lineWidth: isDashed ? 1.5 : 2.5,
124                    lineDash: isDashed ? [6, 4] : [],
125                    hidden: !chart.isDatasetVisible(i),
126                    datasetIndex: i,
127                    fontColor: textColor,
128                  }
129                })
130              }
131            }
132          },
133          tooltip: tooltipConfig
134        },
135        scales: {
136          x: {
137            grid: { display: false },
138            ticks: { color: textColor, font: { size: 11 }, maxRotation: 45 }
139          },
140          y: {
141            stacked: this.stackedValue,
142            beginAtZero: true,
143            grid: { color: gridColor },
144            ticks: {
145              color: textColor,
146              font: { size: 11 },
147              callback: this.currencyValue
148                ? (v) => "$" + v.toLocaleString()
149                : undefined
150            }
151          }
152        }
153      }
154    })
155
156    this.observeTheme()
157  }
158
159  externalTooltip(context) {
160    const { chart, tooltip } = context
161    const rawDs = this.datasetsValue
162    const priorLabels = this.priorLabelsValue
163    const isCurrency = this.currencyValue
164
165    // Get or create tooltip element inside a position:relative wrapper
166    let el = this._tooltipEl
167    if (!el) {
168      // Ensure parent is position:relative so absolute positioning works
169      const parent = chart.canvas.parentNode
170      if (getComputedStyle(parent).position === "static") {
171        parent.style.position = "relative"
172      }
173      el = document.createElement("div")
174      el.className = "cl-custom-tooltip"
175      el.innerHTML = "<div class='cl-tt-inner'></div>"
176      parent.appendChild(el)
177      this._tooltipEl = el
178    }
179
180    // Hide if no tooltip
181    if (tooltip.opacity === 0) {
182      el.style.opacity = "0"
183      el.style.pointerEvents = "none"
184      return
185    }
186
187    const idx = tooltip.dataPoints?.[0]?.dataIndex ?? 0
188    const currentDate = tooltip.title?.[0] || this.labelsValue[idx] || ""
189    const priorDate = priorLabels[idx] || ""
190
191    // Separate current vs prior items
192    const currentItems = []
193    const priorItems = []
194    tooltip.dataPoints.forEach(dp => {
195      const ds = rawDs[dp.datasetIndex]
196      const isDashed = ds?.dashed === true
197      const color = dp.dataset.borderColor
198      const val = isCurrency
199        ? "$" + dp.parsed.y.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
200        : dp.parsed.y.toLocaleString()
201      const label = isDashed ? (ds.label || "").replace(/^Prior\s+/i, "") : (ds.label || "")
202      const item = { color, label, val }
203      if (isDashed) priorItems.push(item)
204      else currentItems.push(item)
205    })
206
207    // Build table HTML
208    let html = `<div class="cl-tt-inner"><table class="cl-tt-table"><thead><tr>`
209    html += `<th></th>`
210    html += `<th class="cl-tt-cur-head">${currentDate}</th>`
211    if (priorDate) html += `<th class="cl-tt-pri-head">${priorDate}</th>`
212    html += `</tr></thead><tbody>`
213
214    const rowCount = Math.max(currentItems.length, priorItems.length)
215    for (let r = 0; r < rowCount; r++) {
216      const cur = currentItems[r]
217      const pri = priorItems[r]
218      const label = cur ? cur.label : (pri ? pri.label : "")
219      const dotColor = cur ? cur.color : (pri ? pri.color : "#666")
220
221      html += `<tr>`
222      html += `<td><span class="cl-tt-dot" style="background:${dotColor}"></span>${label}</td>`
223      html += `<td class="cl-tt-cur">${cur ? cur.val : "—"}</td>`
224      if (priorDate) html += `<td class="cl-tt-pri">${pri ? pri.val : "—"}</td>`
225      html += `</tr>`
226    }
227
228    html += `</tbody></table></div>`
229    el.innerHTML = html
230
231    // Position
232    el.style.opacity = "1"
233    el.style.pointerEvents = "none"
234
235    const canvasRect = chart.canvas.getBoundingClientRect()
236    const ttWidth = el.offsetWidth
237    const ttHeight = el.offsetHeight
238    let left = tooltip.caretX + 12
239    let top = tooltip.caretY - ttHeight / 2
240
241    // Flip to left side if it would overflow
242    if (left + ttWidth > canvasRect.width) {
243      left = tooltip.caretX - ttWidth - 12
244    }
245    // Clamp vertical
246    if (top < 4) top = 4
247    if (top + ttHeight > canvasRect.height - 4) top = canvasRect.height - ttHeight - 4
248
249    el.style.left = left + "px"
250    el.style.top = top + "px"
251  }
252
253  palette(i) {
254    const colors = chartTokenPalette() || ["#23AAE0", "#8B5CF6", "#10B981", "#F59E0B", "#EF4444", "#EC4899", "#06B6D4", "#6366F1", "#94A3B8"]
255    return colors[i % colors.length]
256  }
257
258  alphaColor(hex, alpha) {
259    const r = parseInt(hex.slice(1, 3), 16)
260    const g = parseInt(hex.slice(3, 5), 16)
261    const b = parseInt(hex.slice(5, 7), 16)
262    return `rgba(${r},${g},${b},${alpha})`
263  }
264
265  observeTheme() {
266    this._observer = new MutationObserver(() => {
267      if (this.chart) {
268        this.chart.destroy()
269        this.render()
270      }
271    })
272    this._observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] })
273  }
274};

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.