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.