PageSourceSearch

https://www.eventeny.com/js/Components/Analytics/BarChart.js?v=1790189618

js eventeny.com collected 2026-09-24 08:54:20 UTC 6,580 bytes, 194 lines download raw bytes

1import { WebComponent } from '../Base/WebComponent.js';
2
3/** Decode HTML entities (e.g. & → &) so Chart.js renders plain text, not entity strings. */
4function decodeEntities(str) {
5    if (typeof str !== 'string') return str;
6    const d = document.createElement('textarea');
7    d.innerHTML = str;
8    return d.value;
9}
10
11/**
12 * @file BarChart.js
13 * @description Bare grouped bar chart component. renders a Chart.js canvas only, with no card chrome.
14 * Intended to be slotted inside a <data-card-element> which provides the card shell,
15 * header, export button, and refresh button.
16 *
17 * @class BarChart
18 * @extends WebComponent
19 *
20 * @attribute logarithmic: Boolean. When present, the Y axis uses a logarithmic scale. Useful
21 *   when datasets have extreme value discrepancies that make smaller values unreadable on a
22 *   linear scale. Note: zero values are automatically excluded from the log scale.
23 *
24 * @example
25 * <data-card-element size="small" title="Upsells & Resells">
26 *   <bar-chart-element id="chart-upsell-resell"></bar-chart-element>
27 * </data-card-element>
28 *
29 * <!-- Logarithmic Y axis -->
30 * <bar-chart-element logarithmic></bar-chart-element>
31 *
32 * Set data via:
33 *   el.setState({
34 *     labels: [...],
35 *     datasets: [
36 *       { label: 'Upsells', data: [...], backgroundColor: '#038580' },
37 *       { label: 'Resells', data: [...], backgroundColor: '#7C3AED' },
38 *     ]
39 *   });
40 */
41class BarChart extends WebComponent {
42    state;
43    _chart = null;
44    _initialized = false;
45    _chartReady;
46
47    static get observedAttributes() { return ['logarithmic']; }
48
49    
50    constructor() {
51        super();
52        this._chartReady = this.loadChartJS();
53        this.setState(new BarChartState());
54    }
55    
56    /** Changing the scale type requires a full chart rebuild, not just a data update. */
57    attributeChangedCallback() {
58        if (!this._initialized) return;
59        if (this._chart) { this._chart.destroy(); this._chart = null; }
60        this._initialized = false;
61        this.render();
62    }
63
64    disconnectedCallback() {
65        if (this._chart) {
66            this._chart.destroy();
67            this._chart = null;
68        }
69    }
70
71    _isLogarithmic() {
72        return this.hasAttribute('logarithmic');
73    }
74
75    /**
76     * Returns the Y-axis scale configuration, switching between linear and logarithmic
77     * based on the `logarithmic` attribute. On a log scale, tick labels are shown only
78     * at powers of ten (1, 10, 100, …) to keep the axis readable.
79     */
80    _yOptions() {
81        if (this._isLogarithmic()) {
82            return {
83                type: 'logarithmic',
84                grid: { color: '#F3F4F6' },
85                ticks: {
86                    callback: v => {
87                        if (v <= 0) return null;
88                        const exp = Math.log10(v);
89                        return Math.abs(exp - Math.round(exp)) < 0.001
90                            ? Number(v).toLocaleString('en-US')
91                            : null;
92                    },
93                },
94            };
95        }
96        return {
97            beginAtZero: true,
98            grid:  { color: '#F3F4F6' },
99            ticks: { precision: 0 },
100        };
101    }
102
103    render() {
104        // On subsequent renders with an existing chart, update data in-place to avoid flicker
105        if (this._chart && this._initialized) {
106            this._chart.data.labels   = (this.state.labels ?? []).map(decodeEntities);
107            this._chart.data.datasets = this.state.datasets ?? [];
108            this._chart.update();
109            return;
110        }
111
112        if (!this.state.labels?.length) {
113            this.loadTemplate(/*html*/`
114                <style>
115                    :host { display: flex; align-items: center; justify-content: center;
116                            width: 100%; min-height: 160px; }
117                    .no-data { color: #9CA3AF; font-size: 14px; }
118                </style>
119                <span class="no-data">No data present</span>`);
120            this._initialized = false;
121            return;
122        }
123
124        const template = /*html*/`
125            <style>
126                :host { display: block; width: 100%; height: 100%; }
127                .chart-container {
128                    position: relative;
129                    width: 100%;
130                    min-height: 220px;
131                    height: 100%;
132                }
133                canvas { display: block; }
134            </style>
135            <div class="chart-container">
136                <canvas></canvas>
137            </div>`;
138
139        this.loadTemplate(template);
140        this._initialized = false;
141
142        // Wait for Chart.js to be available, then initialise the chart
143        this._chartReady.then(() => {
144            const canvas = this.shadowRoot.querySelector('canvas');
145            if (!canvas) return;
146            if (this._chart) { this._chart.destroy(); this._chart = null; }
147            if (!this.state.labels?.length) return;
148
149            this._chart = new Chart(canvas, {
150                type: 'bar',
151                data: {
152                    labels:   (this.state.labels ?? []).map(decodeEntities),
153                    datasets: this.state.datasets ?? [],
154                },
155                options: {
156                    responsive:          true,
157                    maintainAspectRatio: false,
158                    plugins: {
159                        legend: {
160                            display:  true,
161                            align: 'start',
162                            position: 'top',
163                            labels:   { boxWidth: 12, font: { size: 12 } },
164                        },
165                        tooltip: {
166                            callbacks: {
167                                label: ctx => ` ${ctx.dataset.label}: ${Number(ctx.parsed.y).toLocaleString('en-US')}`,
168                            },
169                        },
170                    },
171                    scales: {
172                        x: {
173                            grid:  { color: '#F3F4F6' },
174                            ticks: { maxTicksLimit: 12 },
175                        },
176                        y: this._yOptions(),
177                    },
178                    datasets: {
179                        bar: { barPercentage: 0.6, categoryPercentage: 0.7, borderRadius: 6, borderSkipped: 'none' },
180                    },
181                },
182            });
183
184            this._initialized = true;
185        });
186    }
187}
188
189class BarChartState {
190    labels   = [];
191    datasets = [];
192}
193
194customElements.define('bar-chart-element', BarChart);

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.