juxscript 1.1.397 → 1.1.399

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +428 -200
  2. package/bin/cli.js +2 -212
  3. package/dist/charts/barChart.d.ts +119 -0
  4. package/dist/charts/barChart.d.ts.map +1 -0
  5. package/dist/charts/barChart.js +644 -0
  6. package/dist/charts/barChart.js.map +1 -0
  7. package/dist/charts/lineChart.d.ts +104 -0
  8. package/dist/charts/lineChart.d.ts.map +1 -0
  9. package/dist/charts/lineChart.js +466 -0
  10. package/dist/charts/lineChart.js.map +1 -0
  11. package/dist/charts/pieChart.d.ts +93 -0
  12. package/dist/charts/pieChart.d.ts.map +1 -0
  13. package/dist/charts/pieChart.js +397 -0
  14. package/dist/charts/pieChart.js.map +1 -0
  15. package/dist/components/barChart.d.ts +18 -2
  16. package/dist/components/barChart.d.ts.map +1 -1
  17. package/dist/components/barChart.js +175 -140
  18. package/dist/components/barChart.js.map +1 -1
  19. package/dist/components/button.d.ts +6 -0
  20. package/dist/components/button.d.ts.map +1 -1
  21. package/dist/components/button.js +18 -0
  22. package/dist/components/button.js.map +1 -1
  23. package/dist/components/checkbox.d.ts +6 -0
  24. package/dist/components/checkbox.d.ts.map +1 -1
  25. package/dist/components/checkbox.js +34 -0
  26. package/dist/components/checkbox.js.map +1 -1
  27. package/dist/components/input.d.ts +3 -0
  28. package/dist/components/input.d.ts.map +1 -1
  29. package/dist/components/input.js +17 -0
  30. package/dist/components/input.js.map +1 -1
  31. package/dist/components/lineChart.d.ts +19 -2
  32. package/dist/components/lineChart.d.ts.map +1 -1
  33. package/dist/components/lineChart.js +233 -97
  34. package/dist/components/lineChart.js.map +1 -1
  35. package/dist/components/link.d.ts +3 -0
  36. package/dist/components/link.d.ts.map +1 -1
  37. package/dist/components/link.js +17 -0
  38. package/dist/components/link.js.map +1 -1
  39. package/dist/components/list.d.ts +3 -0
  40. package/dist/components/list.d.ts.map +1 -1
  41. package/dist/components/list.js +17 -0
  42. package/dist/components/list.js.map +1 -1
  43. package/dist/components/nav.d.ts +3 -0
  44. package/dist/components/nav.d.ts.map +1 -1
  45. package/dist/components/nav.js +17 -0
  46. package/dist/components/nav.js.map +1 -1
  47. package/dist/components/pieChart.d.ts +7 -0
  48. package/dist/components/pieChart.d.ts.map +1 -1
  49. package/dist/components/pieChart.js +113 -16
  50. package/dist/components/pieChart.js.map +1 -1
  51. package/dist/components/radio.d.ts +3 -0
  52. package/dist/components/radio.d.ts.map +1 -1
  53. package/dist/components/radio.js +17 -0
  54. package/dist/components/radio.js.map +1 -1
  55. package/dist/components/select.d.ts +3 -0
  56. package/dist/components/select.d.ts.map +1 -1
  57. package/dist/components/select.js +17 -0
  58. package/dist/components/select.js.map +1 -1
  59. package/dist/components/table.d.ts +3 -0
  60. package/dist/components/table.d.ts.map +1 -1
  61. package/dist/components/table.js +17 -0
  62. package/dist/components/table.js.map +1 -1
  63. package/dist/components/tabs.d.ts +3 -0
  64. package/dist/components/tabs.d.ts.map +1 -1
  65. package/dist/components/tabs.js +17 -0
  66. package/dist/components/tabs.js.map +1 -1
  67. package/dist/components/tag.d.ts +3 -0
  68. package/dist/components/tag.d.ts.map +1 -1
  69. package/dist/components/tag.js +18 -0
  70. package/dist/components/tag.js.map +1 -1
  71. package/dist/index.d.ts +23 -19
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +23 -19
  74. package/dist/index.js.map +1 -1
  75. package/dist/shapes/c.d.ts +53 -0
  76. package/dist/shapes/c.d.ts.map +1 -0
  77. package/dist/shapes/c.js +127 -0
  78. package/dist/shapes/c.js.map +1 -0
  79. package/dist/shapes/flex.d.ts +49 -0
  80. package/dist/shapes/flex.d.ts.map +1 -0
  81. package/dist/shapes/flex.js +122 -0
  82. package/dist/shapes/flex.js.map +1 -0
  83. package/dist/shapes/g.d.ts +21 -0
  84. package/dist/shapes/g.d.ts.map +1 -0
  85. package/dist/shapes/g.js +52 -0
  86. package/dist/shapes/g.js.map +1 -0
  87. package/dist/tools/devtools.d.ts +3 -0
  88. package/dist/tools/devtools.d.ts.map +1 -0
  89. package/dist/tools/devtools.js +182 -0
  90. package/dist/tools/devtools.js.map +1 -0
  91. package/dist/utils/colors.d.ts +32 -5
  92. package/dist/utils/colors.d.ts.map +1 -1
  93. package/dist/utils/colors.js +32 -6
  94. package/dist/utils/colors.js.map +1 -1
  95. package/dist/utils/tooltip.d.ts +5 -0
  96. package/dist/utils/tooltip.d.ts.map +1 -0
  97. package/dist/utils/tooltip.js +52 -0
  98. package/dist/utils/tooltip.js.map +1 -0
  99. package/dist/utils/trend.d.ts +9 -0
  100. package/dist/utils/trend.d.ts.map +1 -0
  101. package/dist/utils/trend.js +35 -0
  102. package/dist/utils/trend.js.map +1 -0
  103. package/dist/widgets/calendar.d.ts +74 -0
  104. package/dist/widgets/calendar.d.ts.map +1 -0
  105. package/dist/widgets/calendar.js +308 -0
  106. package/dist/widgets/calendar.js.map +1 -0
  107. package/dist/widgets/sidebar.d.ts +90 -0
  108. package/dist/widgets/sidebar.d.ts.map +1 -0
  109. package/dist/widgets/sidebar.js +353 -0
  110. package/dist/widgets/sidebar.js.map +1 -0
  111. package/package.json +3 -2
  112. package/components/calendar/calendar-usage.jux +0 -0
  113. package/components/calendar/calendar.jux +0 -0
  114. package/components/sidebar/index.jux +0 -272
  115. package/components/sidebar/usage.jux +0 -7
@@ -0,0 +1,644 @@
1
+ import { pageState } from '../state/pageState.js';
2
+ import generateId from '../utils/idgen.js';
3
+ import { showTooltip, moveTooltip, hideTooltip, formatTooltipRow } from '../utils/tooltip.js';
4
+ import { computeTrend } from '../utils/trend.js';
5
+ // --- Design tokens ---
6
+ const FONT_FAMILY = `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`;
7
+ const TOKENS = {
8
+ muted: 'hsl(215, 16%, 47%)',
9
+ border: 'hsl(220, 13%, 91%)',
10
+ grid: 'hsl(220, 13%, 93%)',
11
+ bg: '#ffffff',
12
+ title: 'hsl(222, 47%, 11%)',
13
+ hoverBg: 'rgba(0,0,0,0.04)',
14
+ activeBg: 'rgba(0,0,0,0.08)',
15
+ linkColor: 'hsl(217, 91%, 60%)',
16
+ };
17
+ const RATIOS = {
18
+ '16:9': 16 / 9,
19
+ '4:3': 4 / 3,
20
+ '3:2': 3 / 2,
21
+ '1:1': 1,
22
+ '2:1': 2,
23
+ '21:9': 21 / 9,
24
+ };
25
+ // --- Bar animation styles (injected once) ---
26
+ let animInjected = false;
27
+ function injectBarAnimations() {
28
+ if (animInjected)
29
+ return;
30
+ animInjected = true;
31
+ const el = document.createElement('style');
32
+ el.id = 'jux-bar-anims';
33
+ el.textContent = `
34
+ @keyframes jux-bar-grow-h {
35
+ from { transform: scaleX(0); opacity: 0; }
36
+ to { transform: scaleX(1); opacity: 0.85; }
37
+ }
38
+ @keyframes jux-bar-grow-v {
39
+ from { transform: scaleY(0); opacity: 0; }
40
+ to { transform: scaleY(1); opacity: 0.85; }
41
+ }
42
+ @keyframes jux-bar-fade-in {
43
+ from { opacity: 0; }
44
+ to { opacity: 1; }
45
+ }
46
+ .jux-bar-h {
47
+ transform-origin: left center;
48
+ animation: jux-bar-grow-h 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
49
+ }
50
+ .jux-bar-v {
51
+ transform-origin: center bottom;
52
+ animation: jux-bar-grow-v 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
53
+ }
54
+ .jux-bar-label {
55
+ animation: jux-bar-fade-in 0.3s ease-out both;
56
+ }
57
+ .jux-bar-clickable {
58
+ cursor: pointer;
59
+ transition: background-color 0.15s ease, transform 0.1s ease;
60
+ }
61
+ .jux-bar-clickable:hover {
62
+ background-color: rgba(0,0,0,0.04);
63
+ }
64
+ .jux-bar-clickable:active {
65
+ background-color: rgba(0,0,0,0.08);
66
+ transform: scale(0.98);
67
+ }
68
+ .jux-bar-selected {
69
+ outline: 2px solid hsl(217, 91%, 60%);
70
+ outline-offset: 2px;
71
+ }
72
+ `;
73
+ document.head.appendChild(el);
74
+ }
75
+ function resolvePalette(colors) {
76
+ if (Array.isArray(colors))
77
+ return colors;
78
+ // Fallback palettes
79
+ const palettes = {
80
+ green: ['hsl(142, 71%, 45%)', 'hsl(142, 71%, 35%)', 'hsl(142, 71%, 55%)', 'hsl(142, 71%, 25%)', 'hsl(142, 71%, 65%)', 'hsl(142, 71%, 40%)'],
81
+ blue: ['hsl(217, 91%, 60%)', 'hsl(217, 91%, 50%)', 'hsl(217, 91%, 70%)', 'hsl(217, 91%, 40%)', 'hsl(217, 91%, 75%)', 'hsl(217, 91%, 55%)'],
82
+ warm: ['hsl(25, 95%, 53%)', 'hsl(15, 95%, 53%)', 'hsl(35, 95%, 53%)', 'hsl(5, 95%, 53%)', 'hsl(45, 95%, 53%)', 'hsl(20, 95%, 53%)'],
83
+ cool: ['hsl(199, 89%, 48%)', 'hsl(210, 89%, 48%)', 'hsl(188, 89%, 48%)', 'hsl(220, 89%, 48%)', 'hsl(195, 89%, 48%)', 'hsl(205, 89%, 48%)'],
84
+ };
85
+ return palettes[colors] || palettes['green'];
86
+ }
87
+ function computeLayout(values, opts) {
88
+ const { orientation, width, aspectRatio, labelWidth: labelWidthOverride, barThickness: barThicknessOverride } = opts;
89
+ const rows = values.length;
90
+ const maxVal = Math.max(...values.map(v => v.x));
91
+ const ratio = typeof aspectRatio === 'string' ? (RATIOS[aspectRatio] || 16 / 9) : (aspectRatio || 16 / 9);
92
+ const scale = width / 500;
93
+ const padding = Math.round(20 * scale);
94
+ const gap = Math.max(2, Math.round(6 * scale));
95
+ const barRadius = Math.max(3, Math.round(6 * scale));
96
+ const titleSize = Math.max(14, Math.round(18 * scale));
97
+ const subtitleSize = Math.max(11, Math.round(13 * scale));
98
+ const labelSize = Math.max(9, Math.round(12 * scale));
99
+ const tickSize = Math.max(8, Math.round(10 * scale));
100
+ let cardWidth, cardHeight, barThickness, labelWidth, maxBarLength, yAxisWidth;
101
+ const headerEstimate = padding + Math.round(titleSize * 1.4) + Math.round(subtitleSize * 1.4);
102
+ if (orientation === 'vertical') {
103
+ cardWidth = width;
104
+ barThickness = barThicknessOverride || Math.max(16, Math.round(28 * scale));
105
+ yAxisWidth = Math.round(36 * scale);
106
+ labelWidth = 0;
107
+ const labelRowHeight = Math.round(labelSize * 2) + Math.round(6 * scale);
108
+ const ratioHeight = Math.round(width / ratio);
109
+ maxBarLength = Math.max(ratioHeight - headerEstimate - labelRowHeight - padding * 2, 100);
110
+ cardHeight = headerEstimate + maxBarLength + labelRowHeight + padding * 2;
111
+ const barsNeeded = rows * (barThickness + gap);
112
+ if (barsNeeded + yAxisWidth + padding * 2 > cardWidth) {
113
+ barThickness = Math.max(8, Math.floor((cardWidth - yAxisWidth - padding * 2) / rows - gap));
114
+ }
115
+ }
116
+ else {
117
+ cardWidth = width;
118
+ barThickness = barThicknessOverride || Math.max(16, Math.round(28 * scale));
119
+ labelWidth = labelWidthOverride || Math.max(40, Math.round(70 * scale));
120
+ yAxisWidth = 0;
121
+ maxBarLength = cardWidth - labelWidth - padding * 3;
122
+ const rowHeight = barThickness + gap + gap + 1;
123
+ const tickRowHeight = Math.round(tickSize * 2);
124
+ const minContentHeight = headerEstimate + rows * rowHeight + tickRowHeight + padding * 2;
125
+ const ratioHeight = Math.round(width / ratio);
126
+ cardHeight = Math.max(ratioHeight, minContentHeight);
127
+ }
128
+ return { rows, maxVal, cardWidth, cardHeight, barThickness, labelWidth, maxBarLength, yAxisWidth, padding, gap, barRadius, titleSize, subtitleSize, labelSize, tickSize, scale };
129
+ }
130
+ function formatTick(val) {
131
+ if (val >= 1000)
132
+ return `${(val / 1000).toFixed(1)}k`;
133
+ return String(Math.round(val));
134
+ }
135
+ function mkDiv(id, css, className) {
136
+ const el = document.createElement('div');
137
+ el.id = id;
138
+ if (css)
139
+ el.setAttribute('style', css);
140
+ if (className)
141
+ el.className = className;
142
+ return el;
143
+ }
144
+ class BarChart {
145
+ constructor(id, values, options = {}) {
146
+ this._listeners = { click: [], select: [], drill: [] };
147
+ this._selectedIndex = -1;
148
+ this._lastClickDetail = null;
149
+ this._resizeObserver = null;
150
+ this._onChange = null;
151
+ this.id = id || generateId('bar');
152
+ this._values = values;
153
+ this._opts = {
154
+ orientation: options.orientation || 'horizontal',
155
+ colors: options.colors || 'green',
156
+ title: options.title || '',
157
+ subtitle: options.subtitle || '',
158
+ aspectRatio: options.aspectRatio || '16:9',
159
+ showValues: options.showValues ?? false,
160
+ showLegend: options.showLegend ?? undefined,
161
+ ticks: options.ticks ?? 4,
162
+ animate: options.animate ?? true,
163
+ stagger: options.stagger ?? 80,
164
+ responsive: options.responsive ?? true,
165
+ maintainAspectRatio: options.maintainAspectRatio ?? true,
166
+ selectable: options.selectable ?? false,
167
+ ...options,
168
+ };
169
+ this._seriesKeys = detectSeriesKeys(values, options.series);
170
+ if (this._opts.showLegend === undefined) {
171
+ this._opts.showLegend = this._seriesKeys.length > 1;
172
+ }
173
+ if (this._opts.animate)
174
+ injectBarAnimations();
175
+ this._palette = this._seriesKeys.length > 1
176
+ ? this._seriesKeys.map((s, i) => s.color || DEFAULT_SERIES_COLORS[i % DEFAULT_SERIES_COLORS.length])
177
+ : resolvePalette(this._opts.colors || 'green');
178
+ this._isInteractive = true; // always interactive — tooltips on all charts
179
+ this._wrapperEl = mkDiv(`${id}-wrapper`, `width: 100%; position: relative; box-sizing: border-box;`);
180
+ this._stateEl = document.createElement('button');
181
+ this._stateEl.id = id;
182
+ this._stateEl.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden;opacity:0;pointer-events:none;';
183
+ this._wrapperEl.appendChild(this._stateEl);
184
+ }
185
+ // ═══════════════════════════════════════════════════════════
186
+ // PAGESTATE INTEGRATION
187
+ // ═══════════════════════════════════════════════════════════
188
+ getValue() { return this._lastClickDetail; }
189
+ setValue(val) { return this; }
190
+ getElement() { return this._stateEl; }
191
+ getSelectedIndex() { return this._selectedIndex; }
192
+ getOrientation() { return this._opts.orientation; }
193
+ getTitle() { return this._opts.title; }
194
+ getSubtitle() { return this._opts.subtitle; }
195
+ getSelectable() { return this._opts.selectable; }
196
+ setSelectedIndex(idx) { return this.select(idx); }
197
+ setTitle(val) { this._opts.title = val; return this; }
198
+ setSubtitle(val) { this._opts.subtitle = val; return this; }
199
+ setContent(val) { this._opts.title = val; return this; }
200
+ title(val) { this._opts.title = val; return this; }
201
+ subtitle(val) { this._opts.subtitle = val; return this; }
202
+ onChange(fn) {
203
+ this._onChange = fn;
204
+ return this;
205
+ }
206
+ get state() { return pageState[this.id]; }
207
+ get element() { return this._wrapperEl; }
208
+ // ═══════════════════════════════════════════════════════════
209
+ // CLICK HANDLING
210
+ // ═══════════════════════════════════════════════════════════
211
+ _makeClickEvent(i, seriesIdx = 0) {
212
+ const v = this._values[i];
213
+ const sk = this._seriesKeys[seriesIdx];
214
+ return {
215
+ chartId: this.id, index: i, seriesKey: sk.key,
216
+ label: v.label || `#${i + 1}`, value: v[sk.key] ?? v.x ?? 0,
217
+ drillKey: v.drill || null, href: v.href || null,
218
+ item: v, orientation: this._opts.orientation,
219
+ };
220
+ }
221
+ _handleBarClick(i, seriesIdx = 0) {
222
+ const detail = this._makeClickEvent(i, seriesIdx);
223
+ if (this._opts.selectable) {
224
+ const prev = this._selectedIndex;
225
+ this._selectedIndex = (this._selectedIndex === i) ? -1 : i;
226
+ this._updateSelection(prev, this._selectedIndex);
227
+ detail.selected = this._selectedIndex === i;
228
+ this._emit('select', detail);
229
+ }
230
+ this._lastClickDetail = detail;
231
+ if (this._opts.onClick)
232
+ this._opts.onClick(detail);
233
+ if (this._onChange)
234
+ this._onChange(detail);
235
+ this._emit('click', detail);
236
+ if (this._opts.stateKey) {
237
+ pageState[this.id][this._opts.stateKey] = detail;
238
+ this._emit('drill', detail);
239
+ }
240
+ this._stateEl.click();
241
+ if (detail.href) {
242
+ this._wrapperEl.dispatchEvent(new CustomEvent('jux:navigate', { bubbles: true, detail }));
243
+ }
244
+ this._wrapperEl.dispatchEvent(new CustomEvent('jux:bar-click', { bubbles: true, detail }));
245
+ }
246
+ _emit(type, detail) {
247
+ (this._listeners[type] || []).forEach(fn => fn(detail));
248
+ }
249
+ _updateSelection(prevIdx, newIdx) {
250
+ if (prevIdx >= 0) {
251
+ const prevEl = document.getElementById(`${this.id}-bar-${prevIdx}`);
252
+ if (prevEl)
253
+ prevEl.classList.remove('jux-bar-selected');
254
+ }
255
+ if (newIdx >= 0) {
256
+ const newEl = document.getElementById(`${this.id}-bar-${newIdx}`);
257
+ if (newEl)
258
+ newEl.classList.add('jux-bar-selected');
259
+ }
260
+ }
261
+ _wireInteractivity(containerEl, barEl, index, seriesIdx = 0) {
262
+ containerEl.classList.add('jux-bar-clickable');
263
+ containerEl.setAttribute('role', 'button');
264
+ containerEl.setAttribute('tabindex', '0');
265
+ const sk = this._seriesKeys[seriesIdx];
266
+ const v = this._values[index];
267
+ const label = v.label || `Item ${index + 1}`;
268
+ const val = v[sk.key] ?? v.x ?? 0;
269
+ containerEl.setAttribute('aria-label', `${label}: ${val}`);
270
+ containerEl.addEventListener('click', () => this._handleBarClick(index, seriesIdx));
271
+ containerEl.addEventListener('keydown', (e) => {
272
+ if (e.key === 'Enter' || e.key === ' ') {
273
+ e.preventDefault();
274
+ this._handleBarClick(index, seriesIdx);
275
+ }
276
+ });
277
+ const color = this._palette[seriesIdx % this._palette.length];
278
+ containerEl.addEventListener('mouseenter', (e) => {
279
+ barEl.style.opacity = '1';
280
+ let html = `<div style="font-weight:600;margin-bottom:2px;">${label}</div>`;
281
+ if (this._seriesKeys.length > 1) {
282
+ this._seriesKeys.forEach((s, si) => {
283
+ const c = this._palette[si % this._palette.length];
284
+ html += formatTooltipRow(s.label || s.key, v[s.key] ?? 0, c);
285
+ });
286
+ }
287
+ else {
288
+ html += formatTooltipRow('Value', val, color);
289
+ }
290
+ showTooltip(e, html);
291
+ });
292
+ containerEl.addEventListener('mousemove', (e) => moveTooltip(e));
293
+ containerEl.addEventListener('mouseleave', () => {
294
+ if (this._selectedIndex !== index)
295
+ barEl.style.opacity = '0.85';
296
+ hideTooltip();
297
+ });
298
+ }
299
+ _buildVertical(cardEl, L, id, values, palette, ticks, animate, stagger, showValues, numSeries, seriesKeys) {
300
+ const areaEl = mkDiv(`${id}-area`, `display: flex; flex-direction: row; align-items: flex-end; height: ${L.maxBarLength}px;`);
301
+ cardEl.appendChild(areaEl);
302
+ const yaxisEl = mkDiv(`${id}-yaxis`, `width: ${L.yAxisWidth}px; height: ${L.maxBarLength}px; position: relative; flex-shrink: 0;`);
303
+ areaEl.appendChild(yaxisEl);
304
+ for (let t = 0; t <= ticks; t++) {
305
+ const val = Math.round((L.maxVal / ticks) * t);
306
+ const bottom = Math.round((val / L.maxVal) * L.maxBarLength);
307
+ const tickEl = mkDiv(`${id}-ytick-${t}`, `position: absolute; bottom: ${bottom}px; right: 4px; font-size: ${L.tickSize}px; color: ${TOKENS.muted}; line-height: 1; transform: translateY(50%);`);
308
+ tickEl.textContent = formatTick(val);
309
+ yaxisEl.appendChild(tickEl);
310
+ }
311
+ const barsEl = mkDiv(`${id}-bars`, `display: flex; flex-direction: row; align-items: flex-end; gap: ${L.gap}px; flex: 1; height: ${L.maxBarLength}px; border-bottom: 1px solid ${TOKENS.grid}; border-left: 1px solid ${TOKENS.grid}; padding: 0 ${L.gap}px; position: relative;`);
312
+ areaEl.appendChild(barsEl);
313
+ for (let t = 1; t <= ticks; t++) {
314
+ const val = Math.round((L.maxVal / ticks) * t);
315
+ const bottom = Math.round((val / L.maxVal) * L.maxBarLength);
316
+ barsEl.appendChild(mkDiv(`${id}-hgrid-${t}`, `position: absolute; left: 0; right: 0; bottom: ${bottom}px; height: 1px; background: ${TOKENS.grid};`));
317
+ }
318
+ const subBarWidth = numSeries > 1 ? Math.max(6, Math.floor(L.barThickness / numSeries)) : L.barThickness;
319
+ values.forEach((v, i) => {
320
+ const colEl = mkDiv(`${id}-col-${i}`, `display: flex; flex-direction: column; align-items: center; justify-content: flex-end; flex: 1; min-width: 0; height: 100%; border-radius: 4px; padding: 2px;`);
321
+ barsEl.appendChild(colEl);
322
+ const groupEl = mkDiv(`${id}-grp-${i}`, `display:flex;flex-direction:row;align-items:flex-end;gap:1px;`);
323
+ colEl.appendChild(groupEl);
324
+ seriesKeys.forEach((sk, si) => {
325
+ const val = v[sk.key] ?? 0;
326
+ const color = palette[si % palette.length];
327
+ const barHeight = Math.max(2, Math.round((val / L.maxVal) * L.maxBarLength));
328
+ const delay = animate ? `animation-delay: ${(i * numSeries + si) * (stagger / numSeries)}ms;` : '';
329
+ const barClass = animate ? 'jux-bar-v' : '';
330
+ const barEl = mkDiv(`${id}-bar-${i}-${si}`, `width: ${subBarWidth}px; height: ${barHeight}px; background-color: ${color}; border-radius: ${L.barRadius}px ${L.barRadius}px 0 0; opacity: 0.85; transition: opacity 0.15s ease; ${delay}`, barClass);
331
+ groupEl.appendChild(barEl);
332
+ if (this._isInteractive)
333
+ this._wireInteractivity(barEl, barEl, i, si);
334
+ });
335
+ if (showValues && numSeries === 1) {
336
+ const valDelay = animate ? `animation-delay: ${i * stagger + 200}ms;` : '';
337
+ const valClass = animate ? 'jux-bar-label' : '';
338
+ const valEl = mkDiv(`${id}-val-${i}`, `font-size: ${L.tickSize}px; color: ${TOKENS.muted}; margin-bottom: 2px; text-align: center; ${valDelay}`, valClass);
339
+ valEl.textContent = String(v[seriesKeys[0].key] ?? 0);
340
+ colEl.insertBefore(valEl, groupEl);
341
+ }
342
+ });
343
+ const xlabelsEl = mkDiv(`${id}-xlabels`, `display: flex; flex-direction: row; gap: ${L.gap}px; padding-left: ${L.yAxisWidth}px; padding-top: ${Math.round(6 * L.scale)}px;`);
344
+ cardEl.appendChild(xlabelsEl);
345
+ values.forEach((v, i) => {
346
+ const label = v.label || `#${i + 1}`;
347
+ const lblDelay = animate ? `animation-delay: ${i * stagger}ms;` : '';
348
+ const lblClass = animate ? 'jux-bar-label' : '';
349
+ const lblEl = mkDiv(`${id}-lbl-${i}`, `flex: 1; text-align: center; font-size: ${L.tickSize}px; color: ${TOKENS.muted}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 2px; ${lblDelay}`, lblClass);
350
+ lblEl.textContent = label;
351
+ xlabelsEl.appendChild(lblEl);
352
+ });
353
+ }
354
+ _buildHorizontal(cardEl, L, id, values, palette, ticks, animate, stagger, showValues, numSeries, seriesKeys) {
355
+ const areaEl = mkDiv(`${id}-area`, `display: flex; flex-direction: column; gap: 0; width: 100%;`);
356
+ cardEl.appendChild(areaEl);
357
+ const subBarHeight = numSeries > 1 ? Math.max(6, Math.floor(L.barThickness / numSeries)) : L.barThickness;
358
+ const rowHeight = numSeries > 1 ? subBarHeight * numSeries + (numSeries - 1) : L.barThickness;
359
+ values.forEach((v, i) => {
360
+ const label = v.label || `#${i + 1}`;
361
+ const rowEl = mkDiv(`${id}-row-${i}`, `display: flex; flex-direction: row; align-items: center; min-height: ${rowHeight}px; margin-bottom: ${L.gap}px; border-bottom: 1px solid ${TOKENS.grid}; padding-bottom: ${L.gap}px; border-radius: 4px; padding: 2px;`);
362
+ areaEl.appendChild(rowEl);
363
+ const lblDelay = animate ? `animation-delay: ${i * stagger}ms;` : '';
364
+ const lblClass = animate ? 'jux-bar-label' : '';
365
+ const lblEl = mkDiv(`${id}-lbl-${i}`, `width: ${L.labelWidth}px; flex-shrink: 0; text-align: right; padding-right: ${Math.round(10 * L.scale)}px; font-size: ${L.labelSize}px; color: ${TOKENS.muted}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; box-sizing: border-box; ${lblDelay}`, lblClass);
366
+ lblEl.textContent = label;
367
+ rowEl.appendChild(lblEl);
368
+ const barsCol = mkDiv(`${id}-bcol-${i}`, `display:flex;flex-direction:column;gap:1px;flex:1;`);
369
+ rowEl.appendChild(barsCol);
370
+ seriesKeys.forEach((sk, si) => {
371
+ const val = v[sk.key] ?? 0;
372
+ const barWidth = Math.max(2, Math.round((val / L.maxVal) * L.maxBarLength));
373
+ const color = palette[si % palette.length];
374
+ const delay = animate ? `animation-delay: ${(i * numSeries + si) * (stagger / numSeries)}ms;` : '';
375
+ const barClass = animate ? 'jux-bar-h' : '';
376
+ const barEl = mkDiv(`${id}-bar-${i}-${si}`, `width: ${barWidth}px; height: ${subBarHeight}px; background-color: ${color}; border-radius: ${L.barRadius}px; opacity: 0.85; transition: opacity 0.15s ease; ${delay}`, barClass);
377
+ barsCol.appendChild(barEl);
378
+ if (this._isInteractive)
379
+ this._wireInteractivity(barEl, barEl, i, si);
380
+ });
381
+ if (showValues && numSeries === 1) {
382
+ const valDelay = animate ? `animation-delay: ${i * stagger + 200}ms;` : '';
383
+ const valClass = animate ? 'jux-bar-label' : '';
384
+ const valEl = mkDiv(`${id}-val-${i}`, `font-size: ${L.tickSize}px; color: ${TOKENS.muted}; margin-left: ${Math.round(6 * L.scale)}px; ${valDelay}`, valClass);
385
+ valEl.textContent = String(v[seriesKeys[0].key] ?? 0);
386
+ rowEl.appendChild(valEl);
387
+ }
388
+ });
389
+ const xaxisEl = mkDiv(`${id}-xaxis`, `display: flex; flex-direction: row; justify-content: space-between; padding-left: ${L.labelWidth}px; padding-top: ${Math.round(4 * L.scale)}px;`);
390
+ cardEl.appendChild(xaxisEl);
391
+ for (let t = 0; t <= ticks; t++) {
392
+ const val = Math.round((L.maxVal / ticks) * t);
393
+ const tickEl = mkDiv(`${id}-xtick-${t}`, `font-size: ${L.tickSize}px; color: ${TOKENS.muted};`);
394
+ tickEl.textContent = formatTick(val);
395
+ xaxisEl.appendChild(tickEl);
396
+ }
397
+ }
398
+ // ═══════════════════════════════════════════════════════════
399
+ // BUILD CHART
400
+ // ═══════════════════════════════════════════════════════════
401
+ _hasTrend() {
402
+ const { trendTitle, trendSubtitle, autoTrend } = this._opts;
403
+ return !!(trendTitle || trendSubtitle || autoTrend);
404
+ }
405
+ _buildChart(resolvedWidth) {
406
+ this._wrapperEl.innerHTML = '';
407
+ this._wrapperEl.appendChild(this._stateEl);
408
+ const { orientation, aspectRatio, title, subtitle, showValues, showLegend, ticks, animate, stagger, maintainAspectRatio, labelWidth: labelWidthOverride, barThickness: barThicknessOverride } = this._opts;
409
+ const values = this._values;
410
+ const palette = this._palette;
411
+ const seriesKeys = this._seriesKeys;
412
+ const numSeries = seriesKeys.length;
413
+ const id = this.id;
414
+ // Compute global max across all series
415
+ let globalMax = 0;
416
+ for (const sk of seriesKeys) {
417
+ for (const v of values) {
418
+ const val = v[sk.key] ?? 0;
419
+ if (val > globalMax)
420
+ globalMax = val;
421
+ }
422
+ }
423
+ if (globalMax === 0)
424
+ globalMax = 1;
425
+ const ratio = typeof aspectRatio === 'string' ? (RATIOS[aspectRatio] || 16 / 9) : (aspectRatio || 16 / 9);
426
+ const effectiveWidth = resolvedWidth;
427
+ const prelimL = computeLayout(values, { orientation, width: effectiveWidth, aspectRatio, labelWidth: labelWidthOverride, barThickness: barThicknessOverride });
428
+ const ratioHeight = Math.round(effectiveWidth / ratio);
429
+ const scale = prelimL.scale;
430
+ const labelSize = Math.max(9, Math.round(11 * scale));
431
+ const legendH = showLegend ? Math.round(labelSize * 2) + prelimL.padding : 0;
432
+ const trendH = this._hasTrend() ? Math.round(56 * prelimL.scale) : 0;
433
+ let minContentHeight;
434
+ const headerEstimate = prelimL.padding + Math.round(prelimL.titleSize * 1.4) + Math.round(prelimL.subtitleSize * 1.4);
435
+ if (orientation === 'vertical') {
436
+ const labelRowHeight = Math.round(prelimL.labelSize * 2) + Math.round(6 * prelimL.scale);
437
+ const minBarArea = Math.max(100, values.length * 20);
438
+ minContentHeight = headerEstimate + minBarArea + labelRowHeight + prelimL.padding * 2 + legendH + trendH;
439
+ }
440
+ else {
441
+ const rowHeight = prelimL.barThickness + prelimL.gap + prelimL.gap + 1;
442
+ const tickRowHeight = Math.round(prelimL.tickSize * 2);
443
+ minContentHeight = headerEstimate + values.length * rowHeight + tickRowHeight + prelimL.padding * 2 + legendH + trendH;
444
+ }
445
+ const effectiveHeight = maintainAspectRatio
446
+ ? Math.max(ratioHeight + trendH, minContentHeight)
447
+ : Math.max(this._wrapperEl.clientHeight || ratioHeight + trendH, minContentHeight);
448
+ this._wrapperEl.style.height = `${effectiveHeight}px`;
449
+ const L = computeLayout(values, { orientation, width: effectiveWidth, aspectRatio, labelWidth: labelWidthOverride, barThickness: barThicknessOverride });
450
+ L.cardWidth = effectiveWidth;
451
+ L.cardHeight = effectiveHeight;
452
+ L.maxVal = globalMax;
453
+ const finalHeaderEstimate = L.padding + Math.round(L.titleSize * 1.4) + Math.round(L.subtitleSize * 1.4);
454
+ if (orientation === 'vertical') {
455
+ const labelRowHeight = Math.round(L.labelSize * 2) + Math.round(6 * L.scale);
456
+ L.maxBarLength = Math.max(100, effectiveHeight - finalHeaderEstimate - labelRowHeight - L.padding * 2 - legendH - trendH);
457
+ }
458
+ else {
459
+ L.maxBarLength = effectiveWidth - L.labelWidth - L.padding * 3;
460
+ }
461
+ const cardEl = mkDiv(`${id}-card`, `background: ${TOKENS.bg}; border: 1px solid ${TOKENS.border}; border-radius: ${Math.round(12 * L.scale)}px; padding: ${L.padding}px; font-family: ${FONT_FAMILY}; box-shadow: 0 1px 3px rgba(0,0,0,0.04); width: 100%; height: 100%; box-sizing: border-box;`);
462
+ this._wrapperEl.appendChild(cardEl);
463
+ if (title) {
464
+ const titleEl = mkDiv(`${id}-title`, `font-size: ${L.titleSize}px; font-weight: 600; color: ${TOKENS.title}; letter-spacing: -0.025em; margin-bottom: 2px; line-height: 1.3;`);
465
+ titleEl.textContent = title;
466
+ cardEl.appendChild(titleEl);
467
+ }
468
+ if (subtitle) {
469
+ const subEl = mkDiv(`${id}-sub`, `font-size: ${L.subtitleSize}px; color: ${TOKENS.muted}; margin-bottom: ${L.padding}px; line-height: 1.3;`);
470
+ subEl.textContent = subtitle;
471
+ cardEl.appendChild(subEl);
472
+ }
473
+ if (orientation === 'vertical') {
474
+ this._buildVertical(cardEl, L, id, values, palette, ticks, animate, stagger, showValues, numSeries, seriesKeys);
475
+ }
476
+ else {
477
+ this._buildHorizontal(cardEl, L, id, values, palette, ticks, animate, stagger, showValues, numSeries, seriesKeys);
478
+ }
479
+ // Legend
480
+ if (showLegend && numSeries > 1) {
481
+ const legendDiv = document.createElement('div');
482
+ legendDiv.style.cssText = `display:flex;flex-wrap:wrap;gap:${Math.round(8 * L.scale)}px ${Math.round(16 * L.scale)}px;padding-top:${Math.round(8 * L.scale)}px;justify-content:center;`;
483
+ seriesKeys.forEach((sk, i) => {
484
+ const item = document.createElement('div');
485
+ item.style.cssText = `display:flex;align-items:center;gap:4px;font-size:${labelSize}px;color:${TOKENS.muted};`;
486
+ const dot = document.createElement('span');
487
+ dot.style.cssText = `width:${Math.round(10 * L.scale)}px;height:${Math.round(10 * L.scale)}px;border-radius:2px;background:${palette[i % palette.length]};flex-shrink:0;`;
488
+ item.appendChild(dot);
489
+ const lbl = document.createElement('span');
490
+ lbl.textContent = sk.label || sk.key;
491
+ item.appendChild(lbl);
492
+ legendDiv.appendChild(item);
493
+ });
494
+ cardEl.appendChild(legendDiv);
495
+ }
496
+ // Trend footer
497
+ this._renderTrend(cardEl, L.scale, L.padding);
498
+ }
499
+ _renderTrend(card, scale, padding) {
500
+ const { trendTitle, trendSubtitle, trendIcon, autoTrend } = this._opts;
501
+ let title = trendTitle;
502
+ let subtitle = trendSubtitle;
503
+ let icon = trendIcon;
504
+ if (autoTrend && !title) {
505
+ const sk = this._seriesKeys[0];
506
+ const vals = this._values.map(v => v[sk.key] ?? 0);
507
+ const labels = this._values.map(v => v.label || '');
508
+ const trend = computeTrend(vals, labels);
509
+ title = trend.title;
510
+ subtitle = subtitle || trend.subtitle;
511
+ icon = icon || trend.icon;
512
+ }
513
+ if (!title && !subtitle)
514
+ return;
515
+ const fontSize = Math.max(11, Math.round(13 * scale));
516
+ const subFontSize = Math.max(10, Math.round(11 * scale));
517
+ const footer = document.createElement('div');
518
+ footer.style.cssText = `padding-top:${Math.round(8 * scale)}px;border-top:1px solid hsl(220,13%,93%);margin-top:${Math.round(8 * scale)}px;`;
519
+ if (title) {
520
+ const titleEl = document.createElement('div');
521
+ titleEl.style.cssText = `font-size:${fontSize}px;font-weight:500;color:hsl(222,47%,11%);line-height:1.4;`;
522
+ titleEl.textContent = `${title}${icon ? ' ' + icon : ''}`;
523
+ footer.appendChild(titleEl);
524
+ }
525
+ if (subtitle) {
526
+ const subEl = document.createElement('div');
527
+ subEl.style.cssText = `font-size:${subFontSize}px;color:hsl(215,16%,47%);line-height:1.4;margin-top:1px;`;
528
+ subEl.textContent = subtitle;
529
+ footer.appendChild(subEl);
530
+ }
531
+ card.appendChild(footer);
532
+ }
533
+ // ═══════════════════════════════════════════════════════════
534
+ // PUBLIC API
535
+ // ═══════════════════════════════════════════════════════════
536
+ render(target) {
537
+ let parent = null;
538
+ if (target && typeof target === 'object' && 'element' in target) {
539
+ parent = target.element;
540
+ }
541
+ else if (target instanceof HTMLElement) {
542
+ parent = target;
543
+ }
544
+ else if (typeof target === 'string') {
545
+ parent = document.getElementById(target) || document.querySelector(target);
546
+ }
547
+ else {
548
+ parent = document.getElementById('app');
549
+ }
550
+ if (!parent)
551
+ return this;
552
+ parent.appendChild(this._wrapperEl);
553
+ const containerWidth = this._opts.responsive
554
+ ? (this._wrapperEl.clientWidth || parent.clientWidth || this._opts.width || 500)
555
+ : (this._opts.width || 500);
556
+ this._buildChart(containerWidth);
557
+ if (this._opts.responsive && typeof ResizeObserver !== 'undefined') {
558
+ let resizeTimer;
559
+ let skipCount = 2;
560
+ const ro = new ResizeObserver(() => {
561
+ if (skipCount > 0) {
562
+ skipCount--;
563
+ return;
564
+ }
565
+ clearTimeout(resizeTimer);
566
+ resizeTimer = setTimeout(() => {
567
+ const newWidth = this._wrapperEl.clientWidth;
568
+ if (newWidth > 0)
569
+ this._buildChart(newWidth);
570
+ }, 200);
571
+ });
572
+ ro.observe(this._wrapperEl);
573
+ this._resizeObserver = ro;
574
+ }
575
+ return this;
576
+ }
577
+ into(target) { return this.render(target); }
578
+ on(event, fn) {
579
+ if (!this._listeners[event])
580
+ this._listeners[event] = [];
581
+ this._listeners[event].push(fn);
582
+ return this;
583
+ }
584
+ off(event, fn) {
585
+ if (this._listeners[event])
586
+ this._listeners[event] = this._listeners[event].filter(f => f !== fn);
587
+ return this;
588
+ }
589
+ select(index) {
590
+ if (!this._opts.selectable)
591
+ return this;
592
+ const prev = this._selectedIndex;
593
+ this._selectedIndex = index;
594
+ this._updateSelection(prev, index);
595
+ return this;
596
+ }
597
+ clearSelection() {
598
+ const prev = this._selectedIndex;
599
+ this._selectedIndex = -1;
600
+ this._updateSelection(prev, -1);
601
+ return this;
602
+ }
603
+ destroy() {
604
+ if (this._resizeObserver)
605
+ this._resizeObserver.disconnect();
606
+ pageState.__unregister(this.id);
607
+ this._wrapperEl.remove();
608
+ }
609
+ }
610
+ // ═══════════════════════════════════════════════════════════
611
+ // FACTORY
612
+ // ═══════════════════════════════════════════════════════════
613
+ export function barChart(id, values, options = {}) {
614
+ const chart = new BarChart(id, values, options);
615
+ pageState.__register(chart);
616
+ return chart;
617
+ }
618
+ export { BarChart, RATIOS, TOKENS };
619
+ export default barChart;
620
+ function detectSeriesKeys(values, series) {
621
+ if (series && series.length > 0) {
622
+ return series;
623
+ }
624
+ if (values.length === 0) {
625
+ return [{ key: 'x', label: 'Value' }];
626
+ }
627
+ const firstItem = values[0];
628
+ const numericKeys = Object.keys(firstItem).filter(key => key !== 'label' && key !== 'drill' && key !== 'href' && typeof firstItem[key] === 'number');
629
+ if (numericKeys.length === 0) {
630
+ return [{ key: 'x', label: 'Value' }];
631
+ }
632
+ return numericKeys.map(key => ({
633
+ key,
634
+ label: key.charAt(0).toUpperCase() + key.slice(1),
635
+ }));
636
+ }
637
+ const DEFAULT_SERIES_COLORS = [
638
+ 'hsl(217, 91%, 60%)',
639
+ 'hsl(142, 71%, 45%)',
640
+ 'hsl(25, 95%, 53%)',
641
+ 'hsl(271, 81%, 56%)',
642
+ 'hsl(48, 96%, 53%)',
643
+ ];
644
+ //# sourceMappingURL=barChart.js.map