import { Chart, Plugin, Tick, ChartConfiguration, ChartData, ChartOptions } from "chart.js"; import { Legend, Tooltip } from "chart.js"; import { DoughnutController, LineController, BarController } from "chart.js"; import { ArcElement, BarElement } from "chart.js"; import { TimeScale, LinearScale } from "chart.js"; import "chartjs-adapter-moment"; import { css, CSSResult, html, LitElement, TemplateResult } from "lit"; import { property } from "lit/decorators"; import { EVENT_REFRESH } from "../../constants"; import { FONT_COLOUR_DARK_MODE, FONT_COLOUR_LIGHT_MODE } from "../../pages/flows/FlowDiagram"; Chart.register(Legend, Tooltip); Chart.register(LineController, BarController, DoughnutController); Chart.register(ArcElement, BarElement); Chart.register(TimeScale, LinearScale); export abstract class AKChart extends LitElement { abstract apiRequest(): Promise; abstract getChartData(data: T): ChartData; chart?: Chart; @property() centerText?: string; fontColour = FONT_COLOUR_LIGHT_MODE; static get styles(): CSSResult[] { return [ css` .container { height: 100%; } canvas { width: 100px; height: 100px; } `, ]; } constructor() { super(); window.addEventListener("resize", () => { if (this.chart) { this.chart.resize(); } }); window.addEventListener(EVENT_REFRESH, () => { this.apiRequest().then((r: T) => { if (!this.chart) return; this.chart.data = this.getChartData(r); this.chart.update(); }); }); const matcher = window.matchMedia("(prefers-color-scheme: light)"); const handler = (ev?: MediaQueryListEvent) => { if (ev?.matches || matcher.matches) { this.fontColour = FONT_COLOUR_LIGHT_MODE; } else { this.fontColour = FONT_COLOUR_DARK_MODE; } this.chart?.update(); }; matcher.addEventListener("change", handler); handler(); } firstUpdated(): void { this.apiRequest().then((r) => { const canvas = this.shadowRoot?.querySelector("canvas"); if (!canvas) { console.warn("Failed to get canvas element"); return false; } const ctx = canvas.getContext("2d"); if (!ctx) { console.warn("failed to get 2d context"); return false; } this.chart = this.configureChart(r, ctx); }); } getChartType(): string { return "bar"; } getPlugins(): Plugin[] { return [ { id: "center-text", beforeDraw: (chart) => { if (!chart.ctx) return; if (!this.centerText) return; const width = chart.width || 0; const height = chart.height || 0; const fontSize = (height / 114).toFixed(2); chart.ctx.font = `${fontSize}em Overpass`; chart.ctx.textBaseline = "middle"; chart.ctx.fillStyle = this.fontColour; const textX = Math.round( (width - chart.ctx.measureText(this.centerText).width) / 2, ); const textY = height / 2; chart.ctx.fillText(this.centerText, textX, textY); }, }, ]; } getOptions(): ChartOptions { return { maintainAspectRatio: false, scales: { x: { type: "time", display: true, ticks: { callback: function ( tickValue: string | number, index: number, ticks: Tick[], ): string { const valueStamp = ticks[index]; const delta = Date.now() - valueStamp.value; const ago = Math.round(delta / 1000 / 3600); return `${ago} Hours ago`; }, autoSkip: true, maxTicksLimit: 8, }, stacked: true, grid: { color: "rgba(0, 0, 0, 0)", }, offset: true, }, y: { type: "linear", display: true, stacked: true, grid: { color: "rgba(0, 0, 0, 0)", }, }, }, } as ChartOptions; } configureChart(data: T, ctx: CanvasRenderingContext2D): Chart { const config = { type: this.getChartType(), data: this.getChartData(data), options: this.getOptions(), plugins: this.getPlugins(), }; return new Chart(ctx, config as ChartConfiguration); } render(): TemplateResult { return html`
`; } }