@cahyo-dimas/freeday 1.7.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.
- package/CHANGELOG.md +447 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/adapters/blazor/freeday-blazor.js +79 -0
- package/adapters/react/components/FdyAutocomplete.tsx +159 -0
- package/adapters/react/components/FdyCascade.tsx +230 -0
- package/adapters/react/components/FdyCfl.tsx +362 -0
- package/adapters/react/components/FdyChart.tsx +69 -0
- package/adapters/react/components/FdyCombo.tsx +166 -0
- package/adapters/react/components/FdyDateRange.tsx +66 -0
- package/adapters/react/components/FdyDatepicker.tsx +354 -0
- package/adapters/react/index.d.ts +37 -0
- package/adapters/react/index.js +9 -0
- package/adapters/react/tsconfig.json +14 -0
- package/adapters/react/useFreeday.js +44 -0
- package/adapters/react/usePopover.ts +64 -0
- package/adapters/vue/components/FdyAutocomplete.vue +174 -0
- package/adapters/vue/components/FdyCascade.vue +241 -0
- package/adapters/vue/components/FdyCfl.vue +354 -0
- package/adapters/vue/components/FdyChart.vue +89 -0
- package/adapters/vue/components/FdyCombo.vue +232 -0
- package/adapters/vue/components/FdyDateRange.vue +78 -0
- package/adapters/vue/components/FdyDatepicker.vue +333 -0
- package/adapters/vue/index.d.ts +44 -0
- package/adapters/vue/index.js +8 -0
- package/adapters/vue/useFreeday.js +44 -0
- package/adapters/vue/usePopover.ts +78 -0
- package/dist/freeday-autocomplete.js +135 -0
- package/dist/freeday-breakpoint.js +51 -0
- package/dist/freeday-carousel.js +111 -0
- package/dist/freeday-cascade.js +256 -0
- package/dist/freeday-cfl.js +213 -0
- package/dist/freeday-chart.js +403 -0
- package/dist/freeday-chip.js +83 -0
- package/dist/freeday-datepicker.js +321 -0
- package/dist/freeday-datetime.js +59 -0
- package/dist/freeday-drawer.js +43 -0
- package/dist/freeday-form.js +181 -0
- package/dist/freeday-mask.js +114 -0
- package/dist/freeday-menu.js +93 -0
- package/dist/freeday-popover.js +69 -0
- package/dist/freeday-rating.js +50 -0
- package/dist/freeday-select.js +194 -0
- package/dist/freeday-slider.js +34 -0
- package/dist/freeday-stepper.js +90 -0
- package/dist/freeday-table.js +475 -0
- package/dist/freeday-tabs.js +68 -0
- package/dist/freeday-timepicker.js +180 -0
- package/dist/freeday-toast.js +84 -0
- package/dist/freeday-tree.js +94 -0
- package/dist/freeday-upload.js +206 -0
- package/dist/freeday.bundle.css +1352 -0
- package/dist/freeday.css +1007 -0
- package/dist/freeday.js +3632 -0
- package/dist/freeday.tokens.css +343 -0
- package/package.json +72 -0
- package/src/base.css +20 -0
- package/src/components/.gitkeep +0 -0
- package/src/components/accordion.css +13 -0
- package/src/components/alert.css +18 -0
- package/src/components/app-shell.css +88 -0
- package/src/components/appbar.css +15 -0
- package/src/components/autocomplete.css +12 -0
- package/src/components/avatar.css +8 -0
- package/src/components/badge.css +27 -0
- package/src/components/breadcrumb.css +9 -0
- package/src/components/breakpoints.css +15 -0
- package/src/components/button.css +58 -0
- package/src/components/card.css +9 -0
- package/src/components/carousel.css +17 -0
- package/src/components/cascade.css +36 -0
- package/src/components/cfl.css +28 -0
- package/src/components/chart.css +55 -0
- package/src/components/chip.css +18 -0
- package/src/components/combo.css +27 -0
- package/src/components/datepicker.css +44 -0
- package/src/components/datetimepicker.css +8 -0
- package/src/components/description-list.css +6 -0
- package/src/components/divider.css +3 -0
- package/src/components/drawer.css +25 -0
- package/src/components/file-upload.css +34 -0
- package/src/components/filterbar.css +25 -0
- package/src/components/form-grid.css +8 -0
- package/src/components/input-group.css +22 -0
- package/src/components/input.css +13 -0
- package/src/components/kbd.css +2 -0
- package/src/components/menu.css +28 -0
- package/src/components/modal.css +22 -0
- package/src/components/pagination.css +8 -0
- package/src/components/progress.css +7 -0
- package/src/components/rating.css +10 -0
- package/src/components/selection.css +25 -0
- package/src/components/skeleton.css +7 -0
- package/src/components/slider.css +12 -0
- package/src/components/spinner.css +6 -0
- package/src/components/states.css +8 -0
- package/src/components/stepper.css +21 -0
- package/src/components/table.css +55 -0
- package/src/components/tabs.css +10 -0
- package/src/components/timeline.css +12 -0
- package/src/components/timepicker.css +22 -0
- package/src/components/toast.css +17 -0
- package/src/components/tooltip.css +6 -0
- package/src/components/tree.css +21 -0
- package/src/freeday-autocomplete.js +135 -0
- package/src/freeday-breakpoint.js +51 -0
- package/src/freeday-carousel.js +111 -0
- package/src/freeday-cascade.js +256 -0
- package/src/freeday-cfl.js +213 -0
- package/src/freeday-chart.js +403 -0
- package/src/freeday-chip.js +83 -0
- package/src/freeday-datepicker.js +321 -0
- package/src/freeday-datetime.js +59 -0
- package/src/freeday-drawer.js +43 -0
- package/src/freeday-form.js +181 -0
- package/src/freeday-mask.js +114 -0
- package/src/freeday-menu.js +93 -0
- package/src/freeday-popover.js +69 -0
- package/src/freeday-rating.js +50 -0
- package/src/freeday-select.js +194 -0
- package/src/freeday-slider.js +34 -0
- package/src/freeday-stepper.js +90 -0
- package/src/freeday-table.js +475 -0
- package/src/freeday-tabs.js +68 -0
- package/src/freeday-timepicker.js +180 -0
- package/src/freeday-toast.js +84 -0
- package/src/freeday-tree.js +94 -0
- package/src/freeday-upload.js +206 -0
- package/tokens/breakpoints.mjs +4 -0
- package/tokens/tokens.json +123 -0
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
})();
|
|
@@ -0,0 +1,403 @@
|
|
|
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 with semantic names instead.
|
|
10
|
+
* Cartesian charts (line/area, multi-series/stacked bar) draw themed axes from --chart-grid /
|
|
11
|
+
* --chart-tick and format ticks + tooltips via data-fdy-format (number|percent|currency).
|
|
12
|
+
* Charts are an enhancement — put a table/text fallback inside the element and it will be
|
|
13
|
+
* replaced on render; give the element role="img" + aria-label.
|
|
14
|
+
*
|
|
15
|
+
* Sparkline: <span data-fdy-chart="sparkline" data-values="4,6,5,8,7,10" data-fdy-color="primary">
|
|
16
|
+
* Bar: <div data-fdy-chart="bar" data-values="12,19,8,15" data-labels="Sen,Sel,Rab,Kam">
|
|
17
|
+
* Donut: <div data-fdy-chart="donut" data-values="45,30,25" data-labels="Lunas,Tertunda,Batal">
|
|
18
|
+
* Line/area/multi bar (axes + legend + formatting):
|
|
19
|
+
* <div data-fdy-chart="line" data-labels="Jan,Feb,Mar,Apr" data-fdy-format="percent"
|
|
20
|
+
* data-series='[{"label":"Posted","values":[12,19,8,15]},{"label":"Draft","values":[4,6,5,8]}]'>
|
|
21
|
+
* <div data-fdy-chart="bar" data-fdy-stacked data-labels="..." data-series='[...]'>
|
|
22
|
+
*/
|
|
23
|
+
(function () {
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
var NS = 'http://www.w3.org/2000/svg';
|
|
27
|
+
|
|
28
|
+
// Categorical chart palette: 8 validated fixed-order slots (--chart-1..8). Series index i
|
|
29
|
+
// (0-based) -> slot i+1; series beyond the 8-slot cap reuse --chart-8 (never cycled).
|
|
30
|
+
function chartSlotVar(i) { return 'var(--chart-' + (i < 8 ? i + 1 : 8) + ')'; }
|
|
31
|
+
function colorVar(name) { return 'var(--color-' + (name || 'primary') + ')'; }
|
|
32
|
+
|
|
33
|
+
function nums(el, attr) {
|
|
34
|
+
var v = el.getAttribute(attr);
|
|
35
|
+
return v ? v.split(',').map(function (s) { return parseFloat(s.trim()); }).filter(function (n) { return !isNaN(n); }) : [];
|
|
36
|
+
}
|
|
37
|
+
function strs(el, attr) {
|
|
38
|
+
var v = el.getAttribute(attr);
|
|
39
|
+
return v ? v.split(',').map(function (s) { return s.trim(); }) : [];
|
|
40
|
+
}
|
|
41
|
+
function ensureImg(el) { if (!el.hasAttribute('role')) el.setAttribute('role', 'img'); }
|
|
42
|
+
function svgEl(name) { return document.createElementNS(NS, name); }
|
|
43
|
+
|
|
44
|
+
// Multi-series model: data-series (JSON) wins; else the data-values single-series shortcut.
|
|
45
|
+
function getSeries(el) {
|
|
46
|
+
var raw = el.getAttribute('data-series');
|
|
47
|
+
if (raw) {
|
|
48
|
+
try {
|
|
49
|
+
var arr = JSON.parse(raw);
|
|
50
|
+
if (Array.isArray(arr)) return arr.map(function (s) {
|
|
51
|
+
return { label: s && s.label != null ? String(s.label) : '', values: ((s && s.values) || []).map(Number) };
|
|
52
|
+
});
|
|
53
|
+
} catch (e) { /* fall through to data-values */ }
|
|
54
|
+
}
|
|
55
|
+
var vals = nums(el, 'data-values');
|
|
56
|
+
return vals.length ? [{ label: el.getAttribute('data-fdy-label') || '', values: vals }] : [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Value formatter for ticks + tooltips.
|
|
60
|
+
function makeFormat(kind) {
|
|
61
|
+
if (kind === 'percent') return function (v) { return (Math.round(v * 10) / 10) + '%'; };
|
|
62
|
+
if (kind === 'currency' || kind === 'usd') return function (v) { return '$' + Math.round(v).toLocaleString('en-US'); };
|
|
63
|
+
return function (v) { return (Math.round(v * 100) / 100).toLocaleString(); };
|
|
64
|
+
}
|
|
65
|
+
// Round a positive number up to a "nice" axis maximum (1/2/2.5/5/10 * 10^n).
|
|
66
|
+
function niceCeil(x) {
|
|
67
|
+
if (x <= 0) return 1;
|
|
68
|
+
var exp = Math.floor(Math.log10(x)), base = Math.pow(10, exp), f = x / base;
|
|
69
|
+
var nf = f <= 1 ? 1 : f <= 2 ? 2 : f <= 2.5 ? 2.5 : f <= 5 ? 5 : 10;
|
|
70
|
+
return nf * base;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- Interactive tooltip (Chart.js-style hover readout; purely visual) ----
|
|
74
|
+
function makeTip(container) {
|
|
75
|
+
if (getComputedStyle(container).position === 'static') container.style.position = 'relative';
|
|
76
|
+
var tip = document.createElement('div');
|
|
77
|
+
tip.className = 'fdy-chart__tip';
|
|
78
|
+
tip.setAttribute('aria-hidden', 'true');
|
|
79
|
+
container.appendChild(tip);
|
|
80
|
+
return tip;
|
|
81
|
+
}
|
|
82
|
+
function showTip(tip, container, clientX, clientY, text) {
|
|
83
|
+
var r = container.getBoundingClientRect();
|
|
84
|
+
tip.textContent = text;
|
|
85
|
+
tip.style.left = (clientX - r.left) + 'px';
|
|
86
|
+
tip.style.top = (clientY - r.top) + 'px';
|
|
87
|
+
tip.classList.add('is-visible');
|
|
88
|
+
}
|
|
89
|
+
function hideTip(tip) { tip.classList.remove('is-visible'); }
|
|
90
|
+
|
|
91
|
+
// Donut sector geometry (SVG path for a ring slice between two radii/angles).
|
|
92
|
+
function polar(cx, cy, r, deg) { var a = (deg - 90) * Math.PI / 180; return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; }
|
|
93
|
+
function sectorPath(cx, cy, R, r, a0, a1) {
|
|
94
|
+
var large = (a1 - a0) > 180 ? 1 : 0;
|
|
95
|
+
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);
|
|
96
|
+
return 'M' + p0[0].toFixed(2) + ' ' + p0[1].toFixed(2)
|
|
97
|
+
+ ' A' + R + ' ' + R + ' 0 ' + large + ' 1 ' + p1[0].toFixed(2) + ' ' + p1[1].toFixed(2)
|
|
98
|
+
+ ' L' + p2[0].toFixed(2) + ' ' + p2[1].toFixed(2)
|
|
99
|
+
+ ' A' + r + ' ' + r + ' 0 ' + large + ' 0 ' + p3[0].toFixed(2) + ' ' + p3[1].toFixed(2) + ' Z';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderSparkline(el) {
|
|
103
|
+
var vals = nums(el, 'data-values');
|
|
104
|
+
if (vals.length < 2) return;
|
|
105
|
+
el.style.setProperty('--fdy-chart', colorVar(el.getAttribute('data-fdy-color')));
|
|
106
|
+
var W = 128, H = 36, P = 3;
|
|
107
|
+
var min = Math.min.apply(null, vals), max = Math.max.apply(null, vals), span = (max - min) || 1;
|
|
108
|
+
var pts = vals.map(function (v, i) {
|
|
109
|
+
var x = P + (i / (vals.length - 1)) * (W - 2 * P);
|
|
110
|
+
var y = H - P - ((v - min) / span) * (H - 2 * P);
|
|
111
|
+
return [x, y];
|
|
112
|
+
});
|
|
113
|
+
var line = pts.map(function (p) { return p[0].toFixed(1) + ',' + p[1].toFixed(1); }).join(' ');
|
|
114
|
+
var area = P + ',' + H + ' ' + line + ' ' + (W - P) + ',' + H;
|
|
115
|
+
var last = pts[pts.length - 1];
|
|
116
|
+
el.innerHTML = '<svg viewBox="0 0 ' + W + ' ' + H + '" aria-hidden="true">'
|
|
117
|
+
+ '<polygon class="fdy-sparkline__area" points="' + area + '"/>'
|
|
118
|
+
+ '<polyline class="fdy-sparkline__line" points="' + line + '" vector-effect="non-scaling-stroke"/>'
|
|
119
|
+
+ '<circle class="fdy-sparkline__dot" cx="' + last[0].toFixed(1) + '" cy="' + last[1].toFixed(1) + '" r="2.5"/>'
|
|
120
|
+
+ '</svg>';
|
|
121
|
+
ensureImg(el);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Simple single-series flex bar (legacy, unchanged look). Rich/multi-series/stacked bars
|
|
125
|
+
// with axes go through renderCartesian instead.
|
|
126
|
+
function renderBars(el) {
|
|
127
|
+
var vals = nums(el, 'data-values');
|
|
128
|
+
if (!vals.length) return;
|
|
129
|
+
var labels = strs(el, 'data-labels');
|
|
130
|
+
el.style.setProperty('--fdy-chart', colorVar(el.getAttribute('data-fdy-color')));
|
|
131
|
+
var max = Math.max.apply(null, vals) || 1;
|
|
132
|
+
el.innerHTML = '';
|
|
133
|
+
var tip = makeTip(el);
|
|
134
|
+
vals.forEach(function (v, i) {
|
|
135
|
+
var col = document.createElement('div'); col.className = 'fdy-bars__col';
|
|
136
|
+
var track = document.createElement('div'); track.className = 'fdy-bars__track';
|
|
137
|
+
var val = document.createElement('span'); val.className = 'fdy-bars__val'; val.textContent = v;
|
|
138
|
+
var bar = document.createElement('div'); bar.className = 'fdy-bars__bar';
|
|
139
|
+
bar.style.height = Math.max(2, (v / max) * 100) + '%';
|
|
140
|
+
var text = (labels[i] || ('#' + (i + 1))) + ': ' + v;
|
|
141
|
+
track.appendChild(val); track.appendChild(bar);
|
|
142
|
+
col.appendChild(track);
|
|
143
|
+
if (labels[i]) {
|
|
144
|
+
var lab = document.createElement('div'); lab.className = 'fdy-bars__label'; lab.textContent = labels[i];
|
|
145
|
+
col.appendChild(lab);
|
|
146
|
+
}
|
|
147
|
+
// Per-column hover so re-render (update) never stacks document/element listeners.
|
|
148
|
+
col.addEventListener('mousemove', function (e) { showTip(tip, el, e.clientX, e.clientY, text); });
|
|
149
|
+
col.addEventListener('mouseleave', function () { hideTip(tip); });
|
|
150
|
+
el.appendChild(col);
|
|
151
|
+
});
|
|
152
|
+
ensureImg(el);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---- Cartesian engine: line / area / grouped+stacked bar with themed axes + legend ----
|
|
156
|
+
function renderCartesian(el, type) {
|
|
157
|
+
var series = getSeries(el);
|
|
158
|
+
if (!series.length) return;
|
|
159
|
+
var labels = strs(el, 'data-labels');
|
|
160
|
+
var fmt = makeFormat(el.getAttribute('data-fdy-format'));
|
|
161
|
+
var stacked = type === 'bar' && el.hasAttribute('data-fdy-stacked');
|
|
162
|
+
var overrides = strs(el, 'data-fdy-colors');
|
|
163
|
+
var singleColor = el.getAttribute('data-fdy-color');
|
|
164
|
+
function colorFor(i) {
|
|
165
|
+
if (overrides[i]) return colorVar(overrides[i]);
|
|
166
|
+
if (series.length === 1 && singleColor) return colorVar(singleColor);
|
|
167
|
+
return chartSlotVar(i);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
var n = series.reduce(function (m, s) { return Math.max(m, s.values.length); }, 0);
|
|
171
|
+
if (!n) return;
|
|
172
|
+
|
|
173
|
+
// y-domain (baseline at 0 unless data dips below).
|
|
174
|
+
var dataMax = -Infinity, dataMin = Infinity;
|
|
175
|
+
if (stacked) {
|
|
176
|
+
for (var c = 0; c < n; c++) {
|
|
177
|
+
var sum = 0; series.forEach(function (s) { sum += (s.values[c] || 0); });
|
|
178
|
+
if (sum > dataMax) dataMax = sum; if (sum < dataMin) dataMin = sum;
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
series.forEach(function (s) { s.values.forEach(function (v) { if (v > dataMax) dataMax = v; if (v < dataMin) dataMin = v; }); });
|
|
182
|
+
}
|
|
183
|
+
if (dataMax === -Infinity) dataMax = 1;
|
|
184
|
+
var base = Math.min(0, dataMin);
|
|
185
|
+
var top = niceCeil(dataMax > base ? dataMax : base + 1);
|
|
186
|
+
if (top <= base) top = base + 1;
|
|
187
|
+
|
|
188
|
+
var W = 320, H = 180, PL = 38, PR = 10, PT = 10, PB = 22;
|
|
189
|
+
var plotW = W - PL - PR, plotH = H - PT - PB;
|
|
190
|
+
function mapY(v) { return PT + plotH * (1 - (v - base) / (top - base)); }
|
|
191
|
+
var slot = plotW / n;
|
|
192
|
+
function xCenter(i) { return PL + slot * (i + 0.5); } // bar / band centre
|
|
193
|
+
function xLine(i) { return PL + (n === 1 ? plotW / 2 : (i / (n - 1)) * plotW); }
|
|
194
|
+
|
|
195
|
+
var svg = svgEl('svg');
|
|
196
|
+
svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
|
|
197
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
198
|
+
|
|
199
|
+
// y gridlines + formatted ticks (bottom line at `base` doubles as the zero baseline).
|
|
200
|
+
var TICKS = 4;
|
|
201
|
+
for (var k = 0; k <= TICKS; k++) {
|
|
202
|
+
var val = base + (top - base) * k / TICKS;
|
|
203
|
+
var gy = mapY(val);
|
|
204
|
+
var gl = svgEl('line');
|
|
205
|
+
gl.setAttribute('x1', PL); gl.setAttribute('x2', PL + plotW);
|
|
206
|
+
gl.setAttribute('y1', gy.toFixed(1)); gl.setAttribute('y2', gy.toFixed(1));
|
|
207
|
+
gl.setAttribute('class', 'fdy-chart-xy__grid');
|
|
208
|
+
svg.appendChild(gl);
|
|
209
|
+
var tk = svgEl('text');
|
|
210
|
+
tk.setAttribute('x', PL - 5); tk.setAttribute('y', (gy + 3).toFixed(1));
|
|
211
|
+
tk.setAttribute('text-anchor', 'end'); tk.setAttribute('class', 'fdy-chart-xy__tick');
|
|
212
|
+
tk.textContent = fmt(val);
|
|
213
|
+
svg.appendChild(tk);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// x category labels with autoskip.
|
|
217
|
+
var maxTicks = Math.max(2, Math.floor(plotW / 40)), step = Math.ceil(n / maxTicks);
|
|
218
|
+
for (var xi = 0; xi < n; xi++) {
|
|
219
|
+
if (xi % step !== 0 && xi !== n - 1) continue;
|
|
220
|
+
var lx = (type === 'bar') ? xCenter(xi) : xLine(xi);
|
|
221
|
+
var xt = svgEl('text');
|
|
222
|
+
xt.setAttribute('x', lx.toFixed(1)); xt.setAttribute('y', H - 6);
|
|
223
|
+
xt.setAttribute('text-anchor', 'middle'); xt.setAttribute('class', 'fdy-chart-xy__xlabel');
|
|
224
|
+
xt.textContent = labels[xi] || ('#' + (xi + 1));
|
|
225
|
+
svg.appendChild(xt);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (type === 'bar') {
|
|
229
|
+
var S = series.length;
|
|
230
|
+
var groupW = slot * 0.72;
|
|
231
|
+
var barW = stacked ? slot * 0.55 : groupW / S;
|
|
232
|
+
var acc = []; // running top per category (stacked)
|
|
233
|
+
for (var ci = 0; ci < n; ci++) acc[ci] = base;
|
|
234
|
+
series.forEach(function (s, si) {
|
|
235
|
+
s.values.forEach(function (v, ci2) {
|
|
236
|
+
if (v == null || isNaN(v)) return;
|
|
237
|
+
var x, y0, y1;
|
|
238
|
+
if (stacked) {
|
|
239
|
+
x = xCenter(ci2) - barW / 2;
|
|
240
|
+
y0 = mapY(acc[ci2]); y1 = mapY(acc[ci2] + v); acc[ci2] += v;
|
|
241
|
+
} else {
|
|
242
|
+
x = PL + slot * ci2 + (slot - groupW) / 2 + barW * si;
|
|
243
|
+
y0 = mapY(base); y1 = mapY(v);
|
|
244
|
+
}
|
|
245
|
+
var rect = svgEl('rect');
|
|
246
|
+
rect.setAttribute('x', x.toFixed(1)); rect.setAttribute('width', Math.max(1, barW - 1).toFixed(1));
|
|
247
|
+
rect.setAttribute('y', Math.min(y0, y1).toFixed(1)); rect.setAttribute('height', Math.max(0.5, Math.abs(y1 - y0)).toFixed(1));
|
|
248
|
+
rect.setAttribute('rx', '1.5'); rect.setAttribute('class', 'fdy-chart-xy__bar');
|
|
249
|
+
rect.style.fill = colorFor(si);
|
|
250
|
+
svg.appendChild(rect);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
} else {
|
|
254
|
+
// line / area: fill (area) beneath the stroke, then a small dot per vertex.
|
|
255
|
+
series.forEach(function (s, si) {
|
|
256
|
+
var pts = [];
|
|
257
|
+
for (var i = 0; i < n; i++) { var v = s.values[i]; if (v == null || isNaN(v)) continue; pts.push([xLine(i), mapY(v)]); }
|
|
258
|
+
if (!pts.length) return;
|
|
259
|
+
var d = pts.map(function (p) { return p[0].toFixed(1) + ',' + p[1].toFixed(1); }).join(' ');
|
|
260
|
+
if (type === 'area') {
|
|
261
|
+
var y0 = mapY(base).toFixed(1);
|
|
262
|
+
var poly = svgEl('polygon');
|
|
263
|
+
poly.setAttribute('points', pts[0][0].toFixed(1) + ',' + y0 + ' ' + d + ' ' + pts[pts.length - 1][0].toFixed(1) + ',' + y0);
|
|
264
|
+
poly.setAttribute('class', 'fdy-chart-xy__area');
|
|
265
|
+
poly.style.fill = colorFor(si);
|
|
266
|
+
svg.appendChild(poly);
|
|
267
|
+
}
|
|
268
|
+
if (pts.length >= 2) {
|
|
269
|
+
var pl = svgEl('polyline');
|
|
270
|
+
pl.setAttribute('points', d); pl.setAttribute('class', 'fdy-chart-xy__line');
|
|
271
|
+
pl.style.stroke = colorFor(si);
|
|
272
|
+
svg.appendChild(pl);
|
|
273
|
+
}
|
|
274
|
+
pts.forEach(function (p) {
|
|
275
|
+
var dot = svgEl('circle');
|
|
276
|
+
dot.setAttribute('cx', p[0].toFixed(1)); dot.setAttribute('cy', p[1].toFixed(1)); dot.setAttribute('r', '2.2');
|
|
277
|
+
dot.setAttribute('class', 'fdy-chart-xy__dot'); dot.style.fill = colorFor(si);
|
|
278
|
+
svg.appendChild(dot);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Assemble: optional legend on top, plot below, tooltip layer last.
|
|
284
|
+
var legendMode = el.getAttribute('data-fdy-legend') || 'auto';
|
|
285
|
+
var showLegend = legendMode !== 'none' && (series.length >= 2 || legendMode === 'always');
|
|
286
|
+
el.classList.add('fdy-chart-xy');
|
|
287
|
+
el.innerHTML = '';
|
|
288
|
+
if (showLegend) {
|
|
289
|
+
var legend = document.createElement('ul');
|
|
290
|
+
legend.className = 'fdy-chart__legend fdy-chart__legend--row';
|
|
291
|
+
series.forEach(function (s, si) {
|
|
292
|
+
var li = document.createElement('li');
|
|
293
|
+
var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorFor(si);
|
|
294
|
+
li.appendChild(sw);
|
|
295
|
+
li.appendChild(document.createTextNode(s.label || ('Seri ' + (si + 1))));
|
|
296
|
+
legend.appendChild(li);
|
|
297
|
+
});
|
|
298
|
+
el.appendChild(legend);
|
|
299
|
+
}
|
|
300
|
+
var plot = document.createElement('div'); plot.className = 'fdy-chart-xy__plot';
|
|
301
|
+
plot.appendChild(svg);
|
|
302
|
+
el.appendChild(plot);
|
|
303
|
+
|
|
304
|
+
// Hover: one transparent band per category → readout of every series at that x.
|
|
305
|
+
var tip = makeTip(el);
|
|
306
|
+
var bandW = (type === 'bar') ? slot : (n > 1 ? plotW / (n - 1) : plotW);
|
|
307
|
+
for (var b = 0; b < n; b++) {
|
|
308
|
+
var bx = (type === 'bar') ? (PL + slot * b) : (xLine(b) - bandW / 2);
|
|
309
|
+
var band = svgEl('rect');
|
|
310
|
+
band.setAttribute('x', Math.max(0, bx).toFixed(1)); band.setAttribute('y', PT);
|
|
311
|
+
band.setAttribute('width', bandW.toFixed(1)); band.setAttribute('height', plotH);
|
|
312
|
+
band.setAttribute('class', 'fdy-chart-xy__band');
|
|
313
|
+
var parts = series.map(function (s) {
|
|
314
|
+
var v = s.values[b];
|
|
315
|
+
return (v == null || isNaN(v)) ? null : (series.length > 1 ? s.label + ' ' : '') + fmt(v);
|
|
316
|
+
}).filter(Boolean);
|
|
317
|
+
var text = (labels[b] || ('#' + (b + 1))) + (parts.length ? ' · ' + parts.join(', ') : '');
|
|
318
|
+
band.addEventListener('mousemove', (function (t) { return function (e) { showTip(tip, el, e.clientX, e.clientY, t); }; })(text));
|
|
319
|
+
band.addEventListener('mouseleave', function () { hideTip(tip); });
|
|
320
|
+
svg.appendChild(band);
|
|
321
|
+
}
|
|
322
|
+
ensureImg(el);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function renderDonut(el) {
|
|
326
|
+
var vals = nums(el, 'data-values');
|
|
327
|
+
if (!vals.length) return;
|
|
328
|
+
var labels = strs(el, 'data-labels');
|
|
329
|
+
var colors = strs(el, 'data-fdy-colors');
|
|
330
|
+
var total = vals.reduce(function (a, b) { return a + b; }, 0) || 1;
|
|
331
|
+
var stops = [], acc = 0;
|
|
332
|
+
var colorAt = function (i) { return colors[i] ? colorVar(colors[i]) : chartSlotVar(i); };
|
|
333
|
+
vals.forEach(function (v, i) {
|
|
334
|
+
var from = (acc / total) * 360, to = ((acc + v) / total) * 360;
|
|
335
|
+
acc += v;
|
|
336
|
+
stops.push(colorAt(i) + ' ' + from.toFixed(1) + 'deg ' + to.toFixed(1) + 'deg');
|
|
337
|
+
});
|
|
338
|
+
el.innerHTML = '';
|
|
339
|
+
var ring = document.createElement('div'); ring.className = 'fdy-donut__ring';
|
|
340
|
+
ring.style.background = 'conic-gradient(' + stops.join(',') + ')';
|
|
341
|
+
var center = document.createElement('div'); center.className = 'fdy-donut__center';
|
|
342
|
+
var centerLabel = el.getAttribute('data-fdy-center');
|
|
343
|
+
center.innerHTML = centerLabel ? '<b></b>' : '<b></b><span>Total</span>';
|
|
344
|
+
center.querySelector('b').textContent = centerLabel != null ? centerLabel : String(total);
|
|
345
|
+
ring.appendChild(center);
|
|
346
|
+
var svg = svgEl('svg');
|
|
347
|
+
svg.setAttribute('class', 'fdy-donut__hit');
|
|
348
|
+
svg.setAttribute('viewBox', '0 0 128 128');
|
|
349
|
+
var ringTip = makeTip(ring), acc2 = 0;
|
|
350
|
+
vals.forEach(function (v, i) {
|
|
351
|
+
var a0 = (acc2 / total) * 360, a1 = ((acc2 + v) / total) * 360; acc2 += v;
|
|
352
|
+
var path = svgEl('path');
|
|
353
|
+
path.setAttribute('class', 'fdy-donut__seg');
|
|
354
|
+
path.setAttribute('d', sectorPath(64, 64, 64, 33, a0, a1));
|
|
355
|
+
var text = (labels[i] || ('#' + (i + 1))) + ': ' + v + ' (' + Math.round((v / total) * 100) + '%)';
|
|
356
|
+
path.addEventListener('mousemove', function (e) { showTip(ringTip, ring, e.clientX, e.clientY, text); });
|
|
357
|
+
path.addEventListener('mouseleave', function () { hideTip(ringTip); });
|
|
358
|
+
svg.appendChild(path);
|
|
359
|
+
});
|
|
360
|
+
ring.appendChild(svg);
|
|
361
|
+
var legend = document.createElement('ul'); legend.className = 'fdy-chart__legend';
|
|
362
|
+
vals.forEach(function (v, i) {
|
|
363
|
+
var li = document.createElement('li');
|
|
364
|
+
var sw = document.createElement('span'); sw.className = 'fdy-chart__swatch'; sw.style.background = colorAt(i);
|
|
365
|
+
li.appendChild(sw);
|
|
366
|
+
li.appendChild(document.createTextNode((labels[i] || ('#' + (i + 1))) + ' — ' + Math.round((v / total) * 100) + '%'));
|
|
367
|
+
legend.appendChild(li);
|
|
368
|
+
});
|
|
369
|
+
el.appendChild(ring); el.appendChild(legend);
|
|
370
|
+
ensureImg(el);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Full (re-)render from current data attributes. Idempotent — safe to call repeatedly.
|
|
374
|
+
function renderChart(el) {
|
|
375
|
+
var type = el.getAttribute('data-fdy-chart');
|
|
376
|
+
if (type === 'sparkline') renderSparkline(el);
|
|
377
|
+
else if (type === 'donut') renderDonut(el);
|
|
378
|
+
else if (type === 'line' || type === 'area') renderCartesian(el, type);
|
|
379
|
+
else if (type === 'bar') {
|
|
380
|
+
if (el.getAttribute('data-series') || el.hasAttribute('data-fdy-stacked') || el.hasAttribute('data-fdy-axes')) renderCartesian(el, 'bar');
|
|
381
|
+
else renderBars(el);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Auto-init renders once; explicit update() re-renders after data changes.
|
|
386
|
+
function initChart(el) {
|
|
387
|
+
if (el.dataset.fdyChartReady === '1') return;
|
|
388
|
+
el.dataset.fdyChartReady = '1';
|
|
389
|
+
renderChart(el);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function initAll(context) {
|
|
393
|
+
Array.prototype.forEach.call((context || document).querySelectorAll('[data-fdy-chart]'), initChart);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (document.readyState === 'loading') {
|
|
397
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
|
398
|
+
} else {
|
|
399
|
+
initAll();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
window.FreedayChart = { init: initChart, initAll: initAll, update: renderChart };
|
|
403
|
+
})();
|