@cahyo-dimas/freeday 1.18.0 → 1.20.0

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 (45) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/COMPONENTS.md +748 -0
  3. package/README.id.md +9 -1
  4. package/README.md +16 -2
  5. package/USAGE.md +34 -11
  6. package/adapters/blazor/FdyTable.razor.cs +66 -7
  7. package/adapters/blazor/TableTypes.cs +6 -0
  8. package/adapters/react/components/FdyTable.tsx +32 -8
  9. package/adapters/vue/components/FdyTable.vue +30 -6
  10. package/dist/freeday.bundle.css +35 -1
  11. package/dist/freeday.css +30 -0
  12. package/dist/freeday.tokens.css +5 -1
  13. package/docs/agent-onboarding.md +151 -0
  14. package/docs/getting-started.md +454 -0
  15. package/docs/integrations.md +321 -0
  16. package/docs/reference-screen.html +461 -0
  17. package/package.json +9 -3
  18. package/src/components/list.css +29 -0
  19. package/tokens/breakpoints.d.ts +3 -0
  20. package/tokens/breakpoints.mjs +8 -1
  21. package/src/components/.gitkeep +0 -0
  22. package/src/freeday-autocomplete.js +0 -135
  23. package/src/freeday-breakpoint.js +0 -51
  24. package/src/freeday-carousel.js +0 -111
  25. package/src/freeday-cascade.js +0 -256
  26. package/src/freeday-cfl.js +0 -213
  27. package/src/freeday-chart.js +0 -429
  28. package/src/freeday-chip.js +0 -83
  29. package/src/freeday-datepicker.js +0 -321
  30. package/src/freeday-datetime.js +0 -83
  31. package/src/freeday-drawer.js +0 -43
  32. package/src/freeday-form.js +0 -181
  33. package/src/freeday-mask.js +0 -114
  34. package/src/freeday-menu.js +0 -93
  35. package/src/freeday-popover.js +0 -69
  36. package/src/freeday-rating.js +0 -50
  37. package/src/freeday-select.js +0 -218
  38. package/src/freeday-slider.js +0 -34
  39. package/src/freeday-stepper.js +0 -90
  40. package/src/freeday-table.js +0 -475
  41. package/src/freeday-tabs.js +0 -68
  42. package/src/freeday-timepicker.js +0 -180
  43. package/src/freeday-toast.js +0 -105
  44. package/src/freeday-tree.js +0 -94
  45. package/src/freeday-upload.js +0 -206
@@ -1,213 +0,0 @@
1
- /* Freeday — choose-from-list enhancer (optional, zero-dependency).
2
- * Wires a searchable master-data picker dialog to read-only fields.
3
- * Progressive enhancement over a native <dialog>; in a framework app, drive the
4
- * dialog as a controlled component (fetchPage callback + server cache), not a store.
5
- *
6
- * Markup contract (see docs):
7
- * - Field group: [data-fdy-cfl="<dialogId>"] — usually a `.fdy-input-group` holding a
8
- * read-only `.fdy-input` and a trigger `<button class="fdy-input-group__btn">`.
9
- * Optional `data-fdy-cfl-display="<key>"` picks which row dataset key fills the input
10
- * (default "value"); optional `data-fdy-cfl-summary="{n} dipilih"` formats the multi label.
11
- * - Standalone opener: [data-fdy-cfl-open="<dialogId>"] — opens the dialog with no bound field.
12
- * - Dialog: <dialog id="<dialogId>" class="fdy-modal fdy-modal--cfl"> containing
13
- * a `[data-fdy-cfl-search]` input, `.fdy-cfl__row` <tr> rows carrying `data-*` fields,
14
- * a `[data-fdy-cfl-empty]` element, and (multi only) `[data-fdy-cfl-multiple]` on the
15
- * dialog plus `[data-fdy-cfl-count]` and `[data-fdy-cfl-confirm]` in the footer.
16
- *
17
- * Emits a bubbling "fdy-cfl-select" CustomEvent on the field group (or the dialog when
18
- * opened standalone): detail {row} for single-select, {rows} for multi-select — each a plain
19
- * object copy of the row's dataset. Consumers fill sibling fields (code → name) from it.
20
- */
21
- (function () {
22
- 'use strict';
23
-
24
- function rowData(row) {
25
- var out = {};
26
- var ds = row.dataset;
27
- for (var key in ds) { if (Object.prototype.hasOwnProperty.call(ds, key)) out[key] = ds[key]; }
28
- return out;
29
- }
30
-
31
- function rowSearchText(row) {
32
- return (row.getAttribute('data-search') != null ? row.getAttribute('data-search') : row.textContent)
33
- .toLowerCase();
34
- }
35
-
36
- function initDialog(dialog) {
37
- if (dialog.dataset.fdyCflReady === '1') return;
38
- dialog.dataset.fdyCflReady = '1';
39
-
40
- var search = dialog.querySelector('[data-fdy-cfl-search]');
41
- var emptyEl = dialog.querySelector('[data-fdy-cfl-empty]');
42
- var countEl = dialog.querySelector('[data-fdy-cfl-count]');
43
- var confirmBtn = dialog.querySelector('[data-fdy-cfl-confirm]');
44
- var multiple = dialog.hasAttribute('data-fdy-cfl-multiple');
45
- var rows = Array.prototype.slice.call(dialog.querySelectorAll('.fdy-cfl__row'));
46
-
47
- var activeGroup = null; // field group that opened the dialog (null = standalone)
48
- var trigger = null; // control to restore focus to on close
49
-
50
- rows.forEach(function (row) {
51
- row.setAttribute('tabindex', '-1');
52
- if (multiple && !row.hasAttribute('aria-selected')) row.setAttribute('aria-selected', 'false');
53
- });
54
-
55
- function visibleRows() {
56
- return rows.filter(function (r) { return !r.hidden; });
57
- }
58
-
59
- function applyFilter() {
60
- var q = (search ? search.value : '').trim().toLowerCase();
61
- var shown = 0;
62
- rows.forEach(function (row) {
63
- var match = !q || rowSearchText(row).indexOf(q) !== -1;
64
- row.hidden = !match;
65
- if (match) shown++;
66
- });
67
- if (emptyEl) emptyEl.hidden = shown !== 0;
68
- }
69
-
70
- function updateCount() {
71
- if (!countEl) return;
72
- var n = rows.filter(function (r) { return r.getAttribute('aria-selected') === 'true'; }).length;
73
- countEl.textContent = n + ' dipilih';
74
- }
75
-
76
- function fillGroup(detail) {
77
- if (!activeGroup) return;
78
- var input = activeGroup.querySelector('.fdy-input');
79
- if (input) {
80
- if (detail.rows) {
81
- var tpl = activeGroup.getAttribute('data-fdy-cfl-summary') || '{n} dipilih';
82
- input.value = detail.rows.length ? tpl.replace('{n}', String(detail.rows.length)) : '';
83
- } else {
84
- var key = activeGroup.getAttribute('data-fdy-cfl-display') || 'value';
85
- input.value = detail.row[key] != null ? detail.row[key] : (detail.row.value || '');
86
- }
87
- }
88
- }
89
-
90
- function emit(detail) {
91
- (activeGroup || dialog).dispatchEvent(new CustomEvent('fdy-cfl-select', {
92
- bubbles: true, detail: detail
93
- }));
94
- }
95
-
96
- function commitSingle(row) {
97
- var detail = { row: rowData(row) };
98
- fillGroup(detail);
99
- emit(detail);
100
- dialog.close();
101
- }
102
-
103
- function toggleSelect(row) {
104
- var next = row.getAttribute('aria-selected') !== 'true';
105
- row.setAttribute('aria-selected', String(next));
106
- var box = row.querySelector('input[type="checkbox"]');
107
- if (box) box.checked = next;
108
- updateCount();
109
- }
110
-
111
- function confirmMulti() {
112
- var picked = rows.filter(function (r) { return r.getAttribute('aria-selected') === 'true'; });
113
- var detail = { rows: picked.map(rowData) };
114
- fillGroup(detail);
115
- emit(detail);
116
- dialog.close();
117
- }
118
-
119
- function focusRow(row) { if (row) row.focus(); }
120
-
121
- function moveFrom(current, delta) {
122
- var vis = visibleRows();
123
- var i = vis.indexOf(current);
124
- if (i === -1) { focusRow(vis[0]); return; }
125
- var target = i + delta;
126
- if (target < 0) { if (search) search.focus(); return; }
127
- if (target >= vis.length) return;
128
- focusRow(vis[target]);
129
- }
130
-
131
- if (search) {
132
- search.addEventListener('input', applyFilter);
133
- search.addEventListener('keydown', function (e) {
134
- if (e.key === 'ArrowDown') { e.preventDefault(); focusRow(visibleRows()[0]); }
135
- });
136
- }
137
-
138
- dialog.addEventListener('click', function (e) {
139
- var row = e.target.closest('.fdy-cfl__row');
140
- if (!row || row.hidden) return;
141
- if (multiple) toggleSelect(row); else commitSingle(row);
142
- });
143
-
144
- dialog.addEventListener('keydown', function (e) {
145
- var row = e.target.closest('.fdy-cfl__row');
146
- if (!row) return;
147
- var vis = visibleRows();
148
- switch (e.key) {
149
- case 'ArrowDown': e.preventDefault(); moveFrom(row, 1); break;
150
- case 'ArrowUp': e.preventDefault(); moveFrom(row, -1); break;
151
- case 'Home': e.preventDefault(); focusRow(vis[0]); break;
152
- case 'End': e.preventDefault(); focusRow(vis[vis.length - 1]); break;
153
- case 'Enter':
154
- case ' ':
155
- e.preventDefault();
156
- if (multiple) toggleSelect(row); else commitSingle(row);
157
- break;
158
- default: break;
159
- }
160
- });
161
-
162
- if (confirmBtn) confirmBtn.addEventListener('click', confirmMulti);
163
-
164
- dialog.addEventListener('close', function () {
165
- if (trigger && typeof trigger.focus === 'function') trigger.focus();
166
- activeGroup = null;
167
- trigger = null;
168
- });
169
-
170
- // Opening API used by triggers below.
171
- dialog.fdyCflOpen = function (group, opener) {
172
- activeGroup = group || null;
173
- trigger = opener || null;
174
- if (search) search.value = '';
175
- applyFilter();
176
- updateCount();
177
- if (typeof dialog.showModal === 'function') dialog.showModal(); else dialog.setAttribute('open', '');
178
- if (search) search.focus();
179
- };
180
- }
181
-
182
- function initTriggers(context) {
183
- var ctx = context || document;
184
-
185
- Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-cfl]'), function (group) {
186
- if (group.dataset.fdyCflBound === '1') return;
187
- var dialog = document.getElementById(group.getAttribute('data-fdy-cfl'));
188
- if (!dialog) return;
189
- initDialog(dialog);
190
- var opener = group.querySelector('[data-fdy-cfl-trigger], .fdy-input-group__btn');
191
- if (!opener) return;
192
- group.dataset.fdyCflBound = '1';
193
- opener.addEventListener('click', function () { dialog.fdyCflOpen(group, opener); });
194
- });
195
-
196
- Array.prototype.forEach.call(ctx.querySelectorAll('[data-fdy-cfl-open]'), function (opener) {
197
- if (opener.dataset.fdyCflBound === '1') return;
198
- var dialog = document.getElementById(opener.getAttribute('data-fdy-cfl-open'));
199
- if (!dialog) return;
200
- initDialog(dialog);
201
- opener.dataset.fdyCflBound = '1';
202
- opener.addEventListener('click', function () { dialog.fdyCflOpen(null, opener); });
203
- });
204
- }
205
-
206
- if (document.readyState === 'loading') {
207
- document.addEventListener('DOMContentLoaded', function () { initTriggers(); });
208
- } else {
209
- initTriggers();
210
- }
211
-
212
- window.FreedayCfl = { init: initTriggers };
213
- })();
@@ -1,429 +0,0 @@
1
- /* Freeday — chart renderer (optional, zero-dependency, no external chart lib).
2
- * Renders sparkline / bar / line / area / donut into an element from data attributes.
3
- * Auto-inits [data-fdy-chart] once; call FreedayChart.update(el) to re-render after the
4
- * data attributes change (used by the freeday/vue <FdyChart> wrapper for reactive data).
5
- *
6
- * Single-series colour (sparkline / simple bar / single line) via data-fdy-color references a
7
- * semantic token by name (primary, accent, success, warning, danger, info). Multi-series
8
- * defaults draw from the validated categorical --chart-1..8 palette in fixed order (never
9
- * cycled, 8-series cap); pass data-fdy-colors to override — a name is a semantic token
10
- * (var(--color-<name>)) OR a slot "chart-1".."chart-8" (var(--chart-N)) to pin a series to the
11
- * validated palette. Both data-fdy-color and data-fdy-colors accept the chart-N form.
12
- * Cartesian charts (line/area, multi-series/stacked bar) draw themed axes from --chart-grid /
13
- * --chart-tick and format ticks + tooltips via data-fdy-format (number|percent|currency).
14
- * Charts are an enhancement — put a table/text fallback inside the element and it will be
15
- * replaced on render; give the element role="img" + aria-label.
16
- *
17
- * Sparkline: <span data-fdy-chart="sparkline" data-values="4,6,5,8,7,10" data-fdy-color="primary">
18
- * Bar: <div data-fdy-chart="bar" data-values="12,19,8,15" data-labels="Sen,Sel,Rab,Kam">
19
- * Donut: <div data-fdy-chart="donut" data-values="45,30,25" data-labels="Lunas,Tertunda,Batal">
20
- * Line/area/multi bar (axes + legend + formatting):
21
- * <div data-fdy-chart="line" data-labels="Jan,Feb,Mar,Apr" data-fdy-format="percent"
22
- * data-series='[{"label":"Posted","values":[12,19,8,15]},{"label":"Draft","values":[4,6,5,8]}]'>
23
- * <div data-fdy-chart="bar" data-fdy-stacked data-labels="..." data-series='[...]'>
24
- */
25
- (function () {
26
- 'use strict';
27
-
28
- var NS = 'http://www.w3.org/2000/svg';
29
-
30
- // Categorical chart palette: 8 validated fixed-order slots (--chart-1..8). Series index i
31
- // (0-based) -> slot i+1; series beyond the 8-slot cap reuse --chart-8 (never cycled).
32
- function chartSlotVar(i) { return 'var(--chart-' + (i < 8 ? i + 1 : 8) + ')'; }
33
- // Resolve a colour name to a CSS var(). A semantic name (primary/accent/success/warning/
34
- // danger/info) reaches --color-<name>; a slot name "chart-1".."chart-8" reaches the validated
35
- // categorical token --chart-N instead, so a data-fdy-colors override can pin a series to the
36
- // palette (stable per category) rather than being forced onto the nearest semantic hue.
37
- function colorVar(name) {
38
- name = name || 'primary';
39
- return name.indexOf('chart-') === 0 ? 'var(--' + name + ')' : 'var(--color-' + name + ')';
40
- }
41
-
42
- function nums(el, attr) {
43
- var v = el.getAttribute(attr);
44
- return v ? v.split(',').map(function (s) { return parseFloat(s.trim()); }).filter(function (n) { return !isNaN(n); }) : [];
45
- }
46
- function strs(el, attr) {
47
- var v = el.getAttribute(attr);
48
- return v ? v.split(',').map(function (s) { return s.trim(); }) : [];
49
- }
50
- function ensureImg(el) { if (!el.hasAttribute('role')) el.setAttribute('role', 'img'); }
51
- function svgEl(name) { return document.createElementNS(NS, name); }
52
-
53
- // Multi-series model: data-series (JSON) wins; else the data-values single-series shortcut.
54
- function getSeries(el) {
55
- var raw = el.getAttribute('data-series');
56
- if (raw) {
57
- try {
58
- var arr = JSON.parse(raw);
59
- if (Array.isArray(arr)) return arr.map(function (s) {
60
- return { label: s && s.label != null ? String(s.label) : '', values: ((s && s.values) || []).map(Number) };
61
- });
62
- } catch (e) { /* fall through to data-values */ }
63
- }
64
- var vals = nums(el, 'data-values');
65
- return vals.length ? [{ label: el.getAttribute('data-fdy-label') || '', values: vals }] : [];
66
- }
67
-
68
- // Value formatter for ticks + tooltips.
69
- function makeFormat(kind) {
70
- if (kind === 'percent') return function (v) { return (Math.round(v * 10) / 10) + '%'; };
71
- if (kind === 'currency' || kind === 'usd') return function (v) { return '$' + Math.round(v).toLocaleString('en-US'); };
72
- return function (v) { return (Math.round(v * 100) / 100).toLocaleString(); };
73
- }
74
- // Round a positive number up to a "nice" axis maximum (1/2/2.5/5/10 * 10^n).
75
- function niceCeil(x) {
76
- if (x <= 0) return 1;
77
- var exp = Math.floor(Math.log10(x)), base = Math.pow(10, exp), f = x / base;
78
- var nf = f <= 1 ? 1 : f <= 2 ? 2 : f <= 2.5 ? 2.5 : f <= 5 ? 5 : 10;
79
- return nf * base;
80
- }
81
-
82
- // ---- Interactive tooltip (Chart.js-style hover readout; purely visual) ----
83
- function makeTip(container) {
84
- if (getComputedStyle(container).position === 'static') container.style.position = 'relative';
85
- var tip = document.createElement('div');
86
- tip.className = 'fdy-chart__tip';
87
- tip.setAttribute('aria-hidden', 'true');
88
- container.appendChild(tip);
89
- return tip;
90
- }
91
- function showTip(tip, container, clientX, clientY, text) {
92
- var r = container.getBoundingClientRect();
93
- tip.textContent = text;
94
- tip.style.left = (clientX - r.left) + 'px';
95
- tip.style.top = (clientY - r.top) + 'px';
96
- tip.classList.add('is-visible');
97
- }
98
- function hideTip(tip) { tip.classList.remove('is-visible'); }
99
-
100
- // Donut sector geometry (SVG path for a ring slice between two radii/angles).
101
- function polar(cx, cy, r, deg) { var a = (deg - 90) * Math.PI / 180; return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; }
102
- function sectorPath(cx, cy, R, r, a0, a1) {
103
- var large = (a1 - a0) > 180 ? 1 : 0;
104
- var p0 = polar(cx, cy, R, a0), p1 = polar(cx, cy, R, a1), p2 = polar(cx, cy, r, a1), p3 = polar(cx, cy, r, a0);
105
- return 'M' + p0[0].toFixed(2) + ' ' + p0[1].toFixed(2)
106
- + ' A' + R + ' ' + R + ' 0 ' + large + ' 1 ' + p1[0].toFixed(2) + ' ' + p1[1].toFixed(2)
107
- + ' L' + p2[0].toFixed(2) + ' ' + p2[1].toFixed(2)
108
- + ' A' + r + ' ' + r + ' 0 ' + large + ' 0 ' + p3[0].toFixed(2) + ' ' + p3[1].toFixed(2) + ' Z';
109
- }
110
-
111
- function renderSparkline(el) {
112
- var vals = nums(el, 'data-values');
113
- if (vals.length < 2) return;
114
- el.style.setProperty('--fdy-chart', colorVar(el.getAttribute('data-fdy-color')));
115
- var W = 128, H = 36, P = 3;
116
- var min = Math.min.apply(null, vals), max = Math.max.apply(null, vals), span = (max - min) || 1;
117
- var pts = vals.map(function (v, i) {
118
- var x = P + (i / (vals.length - 1)) * (W - 2 * P);
119
- var y = H - P - ((v - min) / span) * (H - 2 * P);
120
- return [x, y];
121
- });
122
- var line = pts.map(function (p) { return p[0].toFixed(1) + ',' + p[1].toFixed(1); }).join(' ');
123
- var area = P + ',' + H + ' ' + line + ' ' + (W - P) + ',' + H;
124
- var last = pts[pts.length - 1];
125
- el.innerHTML = '<svg viewBox="0 0 ' + W + ' ' + H + '" aria-hidden="true">'
126
- + '<polygon class="fdy-sparkline__area" points="' + area + '"/>'
127
- + '<polyline class="fdy-sparkline__line" points="' + line + '" vector-effect="non-scaling-stroke"/>'
128
- + '<circle class="fdy-sparkline__dot" cx="' + last[0].toFixed(1) + '" cy="' + last[1].toFixed(1) + '" r="2.5"/>'
129
- + '</svg>';
130
- ensureImg(el);
131
- }
132
-
133
- // Simple single-series flex bar (legacy, unchanged look). Rich/multi-series/stacked bars
134
- // with axes go through renderCartesian instead.
135
- function renderBars(el) {
136
- var vals = nums(el, 'data-values');
137
- if (!vals.length) return;
138
- var labels = strs(el, 'data-labels');
139
- el.style.setProperty('--fdy-chart', colorVar(el.getAttribute('data-fdy-color')));
140
- var max = Math.max.apply(null, vals) || 1;
141
- el.innerHTML = '';
142
- var tip = makeTip(el);
143
- vals.forEach(function (v, i) {
144
- var col = document.createElement('div'); col.className = 'fdy-bars__col';
145
- var track = document.createElement('div'); track.className = 'fdy-bars__track';
146
- var val = document.createElement('span'); val.className = 'fdy-bars__val'; val.textContent = v;
147
- var bar = document.createElement('div'); bar.className = 'fdy-bars__bar';
148
- bar.style.height = Math.max(2, (v / max) * 100) + '%';
149
- var text = (labels[i] || ('#' + (i + 1))) + ': ' + v;
150
- track.appendChild(val); track.appendChild(bar);
151
- col.appendChild(track);
152
- if (labels[i]) {
153
- var lab = document.createElement('div'); lab.className = 'fdy-bars__label'; lab.textContent = labels[i];
154
- col.appendChild(lab);
155
- }
156
- // Per-column hover so re-render (update) never stacks document/element listeners.
157
- col.addEventListener('mousemove', function (e) { showTip(tip, el, e.clientX, e.clientY, text); });
158
- col.addEventListener('mouseleave', function () { hideTip(tip); });
159
- el.appendChild(col);
160
- });
161
- ensureImg(el);
162
- }
163
-
164
- // ---- Cartesian engine: line / area / grouped+stacked bar with themed axes + legend ----
165
- function renderCartesian(el, type) {
166
- var series = getSeries(el);
167
- if (!series.length) return;
168
- var labels = strs(el, 'data-labels');
169
- var fmt = makeFormat(el.getAttribute('data-fdy-format'));
170
- var stacked = type === 'bar' && el.hasAttribute('data-fdy-stacked');
171
- var overrides = strs(el, 'data-fdy-colors');
172
- var singleColor = el.getAttribute('data-fdy-color');
173
- function colorFor(i) {
174
- if (overrides[i]) return colorVar(overrides[i]);
175
- if (series.length === 1 && singleColor) return colorVar(singleColor);
176
- return chartSlotVar(i);
177
- }
178
-
179
- var n = series.reduce(function (m, s) { return Math.max(m, s.values.length); }, 0);
180
- if (!n) return;
181
-
182
- // y-domain (baseline at 0 unless data dips below).
183
- var dataMax = -Infinity, dataMin = Infinity;
184
- if (stacked) {
185
- for (var c = 0; c < n; c++) {
186
- var sum = 0; series.forEach(function (s) { sum += (s.values[c] || 0); });
187
- if (sum > dataMax) dataMax = sum; if (sum < dataMin) dataMin = sum;
188
- }
189
- } else {
190
- series.forEach(function (s) { s.values.forEach(function (v) { if (v > dataMax) dataMax = v; if (v < dataMin) dataMin = v; }); });
191
- }
192
- if (dataMax === -Infinity) dataMax = 1;
193
- var base = Math.min(0, dataMin);
194
- var top = niceCeil(dataMax > base ? dataMax : base + 1);
195
- if (top <= base) top = base + 1;
196
-
197
- var W = 320, H = 180, PL = 38, PR = 10, PT = 10, PB = 22;
198
- var plotW = W - PL - PR, plotH = H - PT - PB;
199
- function mapY(v) { return PT + plotH * (1 - (v - base) / (top - base)); }
200
- var slot = plotW / n;
201
- function xCenter(i) { return PL + slot * (i + 0.5); } // bar / band centre
202
- function xLine(i) { return PL + (n === 1 ? plotW / 2 : (i / (n - 1)) * plotW); }
203
-
204
- var svg = svgEl('svg');
205
- svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
206
- svg.setAttribute('aria-hidden', 'true');
207
-
208
- // y gridlines + formatted ticks (bottom line at `base` doubles as the zero baseline).
209
- var TICKS = 4;
210
- for (var k = 0; k <= TICKS; k++) {
211
- var val = base + (top - base) * k / TICKS;
212
- var gy = mapY(val);
213
- var gl = svgEl('line');
214
- gl.setAttribute('x1', PL); gl.setAttribute('x2', PL + plotW);
215
- gl.setAttribute('y1', gy.toFixed(1)); gl.setAttribute('y2', gy.toFixed(1));
216
- gl.setAttribute('class', 'fdy-chart-xy__grid');
217
- svg.appendChild(gl);
218
- var tk = svgEl('text');
219
- tk.setAttribute('x', PL - 5); tk.setAttribute('y', (gy + 3).toFixed(1));
220
- tk.setAttribute('text-anchor', 'end'); tk.setAttribute('class', 'fdy-chart-xy__tick');
221
- tk.textContent = fmt(val);
222
- svg.appendChild(tk);
223
- }
224
-
225
- // x category labels with autoskip. Labels are author-supplied, so estimate the widest one instead
226
- // of assuming a fixed 40px slot; keep every step-th plus the last, then drop any kept label that
227
- // would collide with the one before it — including the forced last one (a fraction-of-a-slot
228
- // overlap happens when n-1 lands next to the last kept multiple of step, e.g. n=24 at most widths).
229
- var charW = 5.4; // ~9px font advance (.fdy-chart-xy__xlabel)
230
- var widest = 0;
231
- for (var wi = 0; wi < n; wi++) widest = Math.max(widest, String(labels[wi] || ('#' + (wi + 1))).length * charW);
232
- var minGap = widest + 8;
233
- var maxTicks = Math.max(2, Math.floor(plotW / minGap)), step = Math.ceil(n / maxTicks);
234
- var xPos = function (i) { return (type === 'bar') ? xCenter(i) : xLine(i); };
235
- var kept = [];
236
- for (var xi = 0; xi < n; xi++) {
237
- if (xi % step === 0 || xi === n - 1) kept.push(xi);
238
- }
239
- // The last label is worth forcing, but not on top of its neighbour — drop the penultimate one.
240
- if (kept.length >= 2 && xPos(kept[kept.length - 1]) - xPos(kept[kept.length - 2]) < minGap) {
241
- kept.splice(kept.length - 2, 1);
242
- }
243
- kept.forEach(function (xk) {
244
- var xt = svgEl('text');
245
- xt.setAttribute('x', xPos(xk).toFixed(1)); xt.setAttribute('y', H - 6);
246
- xt.setAttribute('text-anchor', 'middle'); xt.setAttribute('class', 'fdy-chart-xy__xlabel');
247
- xt.textContent = labels[xk] || ('#' + (xk + 1));
248
- svg.appendChild(xt);
249
- });
250
-
251
- if (type === 'bar') {
252
- var S = series.length;
253
- var groupW = slot * 0.72;
254
- var barW = stacked ? slot * 0.55 : groupW / S;
255
- var acc = []; // running top per category (stacked)
256
- for (var ci = 0; ci < n; ci++) acc[ci] = base;
257
- series.forEach(function (s, si) {
258
- s.values.forEach(function (v, ci2) {
259
- if (v == null || isNaN(v)) return;
260
- var x, y0, y1;
261
- if (stacked) {
262
- x = xCenter(ci2) - barW / 2;
263
- y0 = mapY(acc[ci2]); y1 = mapY(acc[ci2] + v); acc[ci2] += v;
264
- } else {
265
- x = PL + slot * ci2 + (slot - groupW) / 2 + barW * si;
266
- y0 = mapY(base); y1 = mapY(v);
267
- }
268
- var rect = svgEl('rect');
269
- rect.setAttribute('x', x.toFixed(1)); rect.setAttribute('width', Math.max(1, barW - 1).toFixed(1));
270
- rect.setAttribute('y', Math.min(y0, y1).toFixed(1)); rect.setAttribute('height', Math.max(0.5, Math.abs(y1 - y0)).toFixed(1));
271
- rect.setAttribute('rx', '1.5'); rect.setAttribute('class', 'fdy-chart-xy__bar');
272
- rect.style.fill = colorFor(si);
273
- svg.appendChild(rect);
274
- });
275
- });
276
- } else {
277
- // line / area: fill (area) beneath the stroke, then a small dot per vertex.
278
- series.forEach(function (s, si) {
279
- var pts = [];
280
- for (var i = 0; i < n; i++) { var v = s.values[i]; if (v == null || isNaN(v)) continue; pts.push([xLine(i), mapY(v)]); }
281
- if (!pts.length) return;
282
- var d = pts.map(function (p) { return p[0].toFixed(1) + ',' + p[1].toFixed(1); }).join(' ');
283
- if (type === 'area') {
284
- var y0 = mapY(base).toFixed(1);
285
- var poly = svgEl('polygon');
286
- poly.setAttribute('points', pts[0][0].toFixed(1) + ',' + y0 + ' ' + d + ' ' + pts[pts.length - 1][0].toFixed(1) + ',' + y0);
287
- poly.setAttribute('class', 'fdy-chart-xy__area');
288
- poly.style.fill = colorFor(si);
289
- svg.appendChild(poly);
290
- }
291
- if (pts.length >= 2) {
292
- var pl = svgEl('polyline');
293
- pl.setAttribute('points', d); pl.setAttribute('class', 'fdy-chart-xy__line');
294
- pl.style.stroke = colorFor(si);
295
- svg.appendChild(pl);
296
- }
297
- pts.forEach(function (p) {
298
- var dot = svgEl('circle');
299
- dot.setAttribute('cx', p[0].toFixed(1)); dot.setAttribute('cy', p[1].toFixed(1)); dot.setAttribute('r', '2.2');
300
- dot.setAttribute('class', 'fdy-chart-xy__dot'); dot.style.fill = colorFor(si);
301
- svg.appendChild(dot);
302
- });
303
- });
304
- }
305
-
306
- // Assemble: optional legend on top, plot below, tooltip layer last.
307
- var legendMode = el.getAttribute('data-fdy-legend') || 'auto';
308
- var showLegend = legendMode !== 'none' && (series.length >= 2 || legendMode === 'always');
309
- el.classList.add('fdy-chart-xy');
310
- el.innerHTML = '';
311
- if (showLegend) {
312
- var legend = document.createElement('ul');
313
- legend.className = 'fdy-chart__legend fdy-chart__legend--row';
314
- series.forEach(function (s, si) {
315
- var li = document.createElement('li');
316
- var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorFor(si);
317
- li.appendChild(sw);
318
- li.appendChild(document.createTextNode(s.label || ('Seri ' + (si + 1))));
319
- legend.appendChild(li);
320
- });
321
- el.appendChild(legend);
322
- }
323
- var plot = document.createElement('div'); plot.className = 'fdy-chart-xy__plot';
324
- plot.appendChild(svg);
325
- el.appendChild(plot);
326
-
327
- // Hover: one transparent band per category → readout of every series at that x.
328
- var tip = makeTip(el);
329
- var bandW = (type === 'bar') ? slot : (n > 1 ? plotW / (n - 1) : plotW);
330
- for (var b = 0; b < n; b++) {
331
- var bx = (type === 'bar') ? (PL + slot * b) : (xLine(b) - bandW / 2);
332
- var band = svgEl('rect');
333
- band.setAttribute('x', Math.max(0, bx).toFixed(1)); band.setAttribute('y', PT);
334
- band.setAttribute('width', bandW.toFixed(1)); band.setAttribute('height', plotH);
335
- band.setAttribute('class', 'fdy-chart-xy__band');
336
- var parts = series.map(function (s) {
337
- var v = s.values[b];
338
- return (v == null || isNaN(v)) ? null : (series.length > 1 ? s.label + ' ' : '') + fmt(v);
339
- }).filter(Boolean);
340
- var text = (labels[b] || ('#' + (b + 1))) + (parts.length ? ' · ' + parts.join(', ') : '');
341
- band.addEventListener('mousemove', (function (t) { return function (e) { showTip(tip, el, e.clientX, e.clientY, t); }; })(text));
342
- band.addEventListener('mouseleave', function () { hideTip(tip); });
343
- svg.appendChild(band);
344
- }
345
- ensureImg(el);
346
- }
347
-
348
- function renderDonut(el) {
349
- var vals = nums(el, 'data-values');
350
- if (!vals.length) return;
351
- var labels = strs(el, 'data-labels');
352
- var colors = strs(el, 'data-fdy-colors');
353
- var total = vals.reduce(function (a, b) { return a + b; }, 0) || 1;
354
- var stops = [], acc = 0;
355
- var colorAt = function (i) { return colors[i] ? colorVar(colors[i]) : chartSlotVar(i); };
356
- vals.forEach(function (v, i) {
357
- var from = (acc / total) * 360, to = ((acc + v) / total) * 360;
358
- acc += v;
359
- stops.push(colorAt(i) + ' ' + from.toFixed(1) + 'deg ' + to.toFixed(1) + 'deg');
360
- });
361
- el.innerHTML = '';
362
- var ring = document.createElement('div'); ring.className = 'fdy-donut__ring';
363
- ring.style.background = 'conic-gradient(' + stops.join(',') + ')';
364
- var center = document.createElement('div'); center.className = 'fdy-donut__center';
365
- var centerLabel = el.getAttribute('data-fdy-center');
366
- center.innerHTML = centerLabel ? '<b></b>' : '<b></b><span>Total</span>';
367
- center.querySelector('b').textContent = centerLabel != null ? centerLabel : String(total);
368
- ring.appendChild(center);
369
- var svg = svgEl('svg');
370
- svg.setAttribute('class', 'fdy-donut__hit');
371
- svg.setAttribute('viewBox', '0 0 128 128');
372
- var ringTip = makeTip(ring), acc2 = 0;
373
- vals.forEach(function (v, i) {
374
- var a0 = (acc2 / total) * 360, a1 = ((acc2 + v) / total) * 360; acc2 += v;
375
- var path = svgEl('path');
376
- path.setAttribute('class', 'fdy-donut__seg');
377
- path.setAttribute('d', sectorPath(64, 64, 64, 33, a0, a1));
378
- var text = (labels[i] || ('#' + (i + 1))) + ': ' + v + ' (' + Math.round((v / total) * 100) + '%)';
379
- path.addEventListener('mousemove', function (e) { showTip(ringTip, ring, e.clientX, e.clientY, text); });
380
- path.addEventListener('mouseleave', function () { hideTip(ringTip); });
381
- svg.appendChild(path);
382
- });
383
- ring.appendChild(svg);
384
- var legend = document.createElement('ul'); legend.className = 'fdy-chart__legend';
385
- vals.forEach(function (v, i) {
386
- var li = document.createElement('li');
387
- var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorAt(i);
388
- li.appendChild(sw);
389
- li.appendChild(document.createTextNode((labels[i] || ('#' + (i + 1))) + ' — ' + Math.round((v / total) * 100) + '%'));
390
- legend.appendChild(li);
391
- });
392
- el.appendChild(ring);
393
- // Honour data-fdy-legend for the donut too — a caller may supply its own richer legend. 'auto'
394
- // keeps today's behaviour (a donut is unreadable without labels, so it shows); 'none' suppresses it.
395
- if ((el.getAttribute('data-fdy-legend') || 'auto') !== 'none') el.appendChild(legend);
396
- ensureImg(el);
397
- }
398
-
399
- // Full (re-)render from current data attributes. Idempotent — safe to call repeatedly.
400
- function renderChart(el) {
401
- var type = el.getAttribute('data-fdy-chart');
402
- if (type === 'sparkline') renderSparkline(el);
403
- else if (type === 'donut') renderDonut(el);
404
- else if (type === 'line' || type === 'area') renderCartesian(el, type);
405
- else if (type === 'bar') {
406
- if (el.getAttribute('data-series') || el.hasAttribute('data-fdy-stacked') || el.hasAttribute('data-fdy-axes')) renderCartesian(el, 'bar');
407
- else renderBars(el);
408
- }
409
- }
410
-
411
- // Auto-init renders once; explicit update() re-renders after data changes.
412
- function initChart(el) {
413
- if (el.dataset.fdyChartReady === '1') return;
414
- el.dataset.fdyChartReady = '1';
415
- renderChart(el);
416
- }
417
-
418
- function initAll(context) {
419
- Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-chart]'), initChart);
420
- }
421
-
422
- if (document.readyState === 'loading') {
423
- document.addEventListener('DOMContentLoaded', function () { initAll(); });
424
- } else {
425
- initAll();
426
- }
427
-
428
- window.FreedayChart = { init: initChart, initAll: initAll, update: renderChart };
429
- })();