mnfst 0.5.158 → 0.5.160
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/lib/manifest.chart.css +61 -9
- package/lib/manifest.charts.js +358 -3
- package/lib/manifest.combobox.css +9 -26
- package/lib/manifest.combobox.js +45 -4
- package/lib/manifest.css +80 -36
- package/lib/manifest.integrity.json +3 -3
- package/lib/manifest.min.css +1 -1
- package/lib/manifest.table.css +10 -1
- package/lib/manifest.virtual.js +37 -26
- package/package.json +1 -1
package/lib/manifest.chart.css
CHANGED
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
stroke-width: 1.5;
|
|
74
74
|
transition: r var(--transition-duration, .1s) ease;
|
|
75
75
|
|
|
76
|
-
&:hover {
|
|
76
|
+
&:not(.map-point):hover {
|
|
77
77
|
r: 5
|
|
78
78
|
}
|
|
79
79
|
}
|
|
@@ -142,6 +142,37 @@
|
|
|
142
142
|
pointer-events: none
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/* Timeline / Gantt */
|
|
146
|
+
& rect.gantt-segment {
|
|
147
|
+
fill: var(--color-chart-color, var(--color-chart-1));
|
|
148
|
+
transition: opacity var(--transition-duration, .1s) ease;
|
|
149
|
+
|
|
150
|
+
&:hover {
|
|
151
|
+
opacity: 0.82
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
& rect.gantt-point {
|
|
156
|
+
fill: var(--color-chart-color, var(--color-content-stark, oklch(20.5% 0 0)))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
& text.gantt-track {
|
|
160
|
+
fill: var(--color-chart-label);
|
|
161
|
+
font-size: 0.75rem
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Reference marker */
|
|
165
|
+
& line.gantt-marker {
|
|
166
|
+
stroke: var(--color-chart-color, var(--color-content-subtle, oklch(55.6% 0 0)));
|
|
167
|
+
stroke-width: 1.5;
|
|
168
|
+
stroke-dasharray: 3 3
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
& text.gantt-marker-label {
|
|
172
|
+
fill: var(--color-chart-color, var(--color-content-subtle, oklch(55.6% 0 0)));
|
|
173
|
+
font-size: 0.625rem
|
|
174
|
+
}
|
|
175
|
+
|
|
145
176
|
/* Value labels drawn on/above segments */
|
|
146
177
|
& text.value {
|
|
147
178
|
fill: var(--color-content-stark, oklch(20.5% 0 0));
|
|
@@ -158,8 +189,32 @@
|
|
|
158
189
|
}
|
|
159
190
|
}
|
|
160
191
|
|
|
161
|
-
/*
|
|
162
|
-
|
|
192
|
+
/* World map city / point markers */
|
|
193
|
+
& circle.map-point {
|
|
194
|
+
fill: var(--color-chart-color, var(--color-accent-content, oklch(20.5% 0 0)));
|
|
195
|
+
stroke: var(--color-page, oklch(98.5% 0 0));
|
|
196
|
+
stroke-width: 1;
|
|
197
|
+
fill-opacity: 0.82;
|
|
198
|
+
transition: opacity var(--transition-duration, .1s) ease;
|
|
199
|
+
|
|
200
|
+
&:hover {
|
|
201
|
+
opacity: 0.82
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* World map regions */
|
|
206
|
+
& path.map-region {
|
|
207
|
+
fill: var(--color-chart-color, var(--color-surface-2, color-mix(in oklch, oklch(20.5% 0 0) 8%, transparent)));
|
|
208
|
+
stroke: var(--color-page, oklch(98.5% 0 0));
|
|
209
|
+
stroke-width: 0.4;
|
|
210
|
+
transition: opacity var(--transition-duration, .1s) ease;
|
|
211
|
+
|
|
212
|
+
&:hover {
|
|
213
|
+
opacity: 0.82
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* Heatmap cell */
|
|
163
218
|
& rect.heat-cell {
|
|
164
219
|
fill: color-mix(in oklch, var(--color-chart-heat-high, var(--color-chart-1)) var(--heat, 0%), var(--color-chart-heat-low, var(--color-surface-2)));
|
|
165
220
|
transition: opacity var(--transition-duration, .1s) ease;
|
|
@@ -169,7 +224,7 @@
|
|
|
169
224
|
}
|
|
170
225
|
}
|
|
171
226
|
|
|
172
|
-
/* Legend
|
|
227
|
+
/* Legend */
|
|
173
228
|
& footer {
|
|
174
229
|
display: flex;
|
|
175
230
|
flex-flow: row wrap;
|
|
@@ -194,9 +249,7 @@
|
|
|
194
249
|
}
|
|
195
250
|
}
|
|
196
251
|
|
|
197
|
-
/* Heatmap gradient legend
|
|
198
|
-
label. Footer is padded to the chart margins in JS so the bar aligns
|
|
199
|
-
with the columns. */
|
|
252
|
+
/* Heatmap gradient legend */
|
|
200
253
|
& footer.heat-legend {
|
|
201
254
|
flex-wrap: nowrap;
|
|
202
255
|
gap: 0.5rem;
|
|
@@ -209,8 +262,7 @@
|
|
|
209
262
|
}
|
|
210
263
|
}
|
|
211
264
|
|
|
212
|
-
/* Cursor-following tooltip
|
|
213
|
-
(.tooltip); only the pointer-tracking positioning lives here */
|
|
265
|
+
/* Cursor-following tooltip */
|
|
214
266
|
& .tooltip {
|
|
215
267
|
position: absolute;
|
|
216
268
|
left: 0;
|
package/lib/manifest.charts.js
CHANGED
|
@@ -68,6 +68,18 @@
|
|
|
68
68
|
|
|
69
69
|
const SVGNS = 'http://www.w3.org/2000/svg';
|
|
70
70
|
|
|
71
|
+
// Lazy localized country names per locale (i18n-iso-countries, keyed by
|
|
72
|
+
// alpha-2). Falls back to {} (English atlas names) if a locale isn't covered.
|
|
73
|
+
const _namePacks = {}, _namePackPromise = {};
|
|
74
|
+
function loadCountryNames(locale) {
|
|
75
|
+
if (_namePacks[locale]) return Promise.resolve(_namePacks[locale]);
|
|
76
|
+
if (_namePackPromise[locale]) return _namePackPromise[locale];
|
|
77
|
+
_namePackPromise[locale] = fetch('https://cdn.jsdelivr.net/npm/i18n-iso-countries@7/langs/' + locale + '.json')
|
|
78
|
+
.then(r => r.json()).then(d => (_namePacks[locale] = d.countries || {}))
|
|
79
|
+
.catch(() => (_namePacks[locale] = {}));
|
|
80
|
+
return _namePackPromise[locale];
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
/* ---- Lazy-load d3 micro-modules once ---------------------------- */
|
|
72
84
|
let d3Promise = null;
|
|
73
85
|
function loadD3() {
|
|
@@ -87,6 +99,42 @@
|
|
|
87
99
|
|
|
88
100
|
const prefersReducedMotion = () => { try { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; } catch (_) { return false; } };
|
|
89
101
|
|
|
102
|
+
// Reconcile the ~20 Natural Earth atlas names that differ from the
|
|
103
|
+
// country-by-continent source so every rendered country lands in a continent.
|
|
104
|
+
const _norm = s => String(s || '').toLowerCase().replace(/\./g, '').replace(/\s+/g, ' ').trim();
|
|
105
|
+
const ATLAS_CONT = { 'fiji': 'Oceania', 'w sahara': 'Africa', 'united states of america': 'North America', 'dem rep congo': 'Africa', 'dominican rep': 'North America', 'falkland is': 'South America', 'fr s antarctic lands': 'Antarctica', 'timor-leste': 'Asia', "côte d'ivoire": 'Africa', 'central african rep': 'Africa', 'eq guinea': 'Africa', 'solomon is': 'Oceania', 'taiwan': 'Asia', 'czechia': 'Europe', 'n cyprus': 'Asia', 'somaliland': 'Africa', 'bosnia and herz': 'Europe', 'macedonia': 'Europe', 'kosovo': 'Europe', 's sudan': 'Africa' };
|
|
106
|
+
|
|
107
|
+
/* ---- World map: lazy-load d3-geo + topojson + reference data ----
|
|
108
|
+
Geometry (world-atlas) plus ISO codes (i18n-iso-countries) and
|
|
109
|
+
continent membership (country-json) — all third-party CDN, fetched
|
|
110
|
+
once and indexed onto `geo.meta`. No geographic tables are bundled. */
|
|
111
|
+
let geoPromise = null;
|
|
112
|
+
function loadGeo() {
|
|
113
|
+
if (window.__manifestGeo) return Promise.resolve(window.__manifestGeo);
|
|
114
|
+
if (geoPromise) return geoPromise;
|
|
115
|
+
geoPromise = Promise.all([
|
|
116
|
+
import('https://esm.run/d3-geo@3'),
|
|
117
|
+
import('https://esm.run/topojson-client@3'),
|
|
118
|
+
fetch('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json').then(r => r.json()),
|
|
119
|
+
fetch('https://cdn.jsdelivr.net/npm/i18n-iso-countries@7/codes.json').then(r => r.json()),
|
|
120
|
+
fetch('https://cdn.jsdelivr.net/npm/country-json/src/country-by-continent.json').then(r => r.json())
|
|
121
|
+
]).then(([geo, topo, world, codes, contList]) => {
|
|
122
|
+
const a2 = {}, a3 = {}, num2a2 = {};
|
|
123
|
+
codes.forEach(c => { const n = Number(c[2]); if (!n) return; a2[c[0]] = n; a3[c[1]] = n; num2a2[n] = c[0]; });
|
|
124
|
+
const nameToCont = {}; contList.forEach(r => { nameToCont[_norm(r.country)] = r.continent; });
|
|
125
|
+
const members = {}, idToCont = {};
|
|
126
|
+
(world.objects.countries.geometries || []).forEach(gm => {
|
|
127
|
+
const id = Number(gm.id), nm = _norm(gm.properties && gm.properties.name);
|
|
128
|
+
const cont = nameToCont[nm] || ATLAS_CONT[nm];
|
|
129
|
+
if (cont) { idToCont[id] = cont; (members[cont] = members[cont] || []).push(id); }
|
|
130
|
+
});
|
|
131
|
+
const g = { ...geo, topojson: topo, world, meta: { a2, a3, num2a2, members, idToCont } };
|
|
132
|
+
window.__manifestGeo = g;
|
|
133
|
+
return g;
|
|
134
|
+
}).catch((err) => { geoPromise = null; throw err; });
|
|
135
|
+
return geoPromise;
|
|
136
|
+
}
|
|
137
|
+
|
|
90
138
|
function initializeChartsPlugin() {
|
|
91
139
|
const Alpine = window.Alpine;
|
|
92
140
|
const _registry = Alpine.reactive ? Alpine.reactive({}) : {};
|
|
@@ -116,6 +164,19 @@
|
|
|
116
164
|
/* ---- Config normalization ----------------------------------- */
|
|
117
165
|
function num(v, d) { const n = Number(v); return isNaN(n) ? d : n; }
|
|
118
166
|
|
|
167
|
+
// Timeline values: Date → ms, number → as-is, string → parsed date (or
|
|
168
|
+
// numeric fallback). NaN when absent.
|
|
169
|
+
function toTime(v) {
|
|
170
|
+
if (v == null) return NaN;
|
|
171
|
+
if (v instanceof Date) return v.getTime();
|
|
172
|
+
if (typeof v === 'number') return v;
|
|
173
|
+
const t = Date.parse(v);
|
|
174
|
+
return isNaN(t) ? Number(v) : t;
|
|
175
|
+
}
|
|
176
|
+
function chartLocale() {
|
|
177
|
+
try { return Alpine.store('locale')?.current || document.documentElement.lang || 'en'; } catch (_) { return 'en'; }
|
|
178
|
+
}
|
|
179
|
+
|
|
119
180
|
function configFromDom(el) {
|
|
120
181
|
// Declarative authoring: <figure x-chart.line><data series="Revenue" :values="..."></data></figure>
|
|
121
182
|
const cfg = { series: [], labels: [] };
|
|
@@ -150,10 +211,13 @@
|
|
|
150
211
|
cfg.gap = num(cfg.gap, 1); // heatmap tile gutter in px
|
|
151
212
|
cfg.labels = Array.isArray(cfg.labels) ? cfg.labels : [];
|
|
152
213
|
|
|
153
|
-
// Series may be omitted in favor of a single `data` array.
|
|
154
|
-
|
|
214
|
+
// Series may be omitted in favor of a single `data` array. (A map's
|
|
215
|
+
// `data` is a region→value object, left untouched for drawMap.)
|
|
216
|
+
if (!cfg.series && Array.isArray(cfg.data) && cfg.type !== 'map') cfg.series = [{ name: cfg.name || '', data: cfg.data }];
|
|
155
217
|
if (!Array.isArray(cfg.series)) cfg.series = [];
|
|
156
218
|
|
|
219
|
+
if (cfg.type === 'map') cfg.map = cfg.map || 'world';
|
|
220
|
+
|
|
157
221
|
// Pie/donut: accept [{label,value}] in data.
|
|
158
222
|
if ((cfg.type === 'pie' || cfg.type === 'donut') && cfg.series.length) {
|
|
159
223
|
const s = cfg.series[0];
|
|
@@ -163,6 +227,14 @@
|
|
|
163
227
|
}
|
|
164
228
|
}
|
|
165
229
|
|
|
230
|
+
// Gantt: series are tracks of time/numeric/category segments.
|
|
231
|
+
// (`timeline` accepted as a legacy alias.)
|
|
232
|
+
if (cfg.type === 'gantt' || cfg.type === 'timeline') {
|
|
233
|
+
cfg.type = 'gantt';
|
|
234
|
+
cfg.rowHeight = num(cfg.rowHeight, 28);
|
|
235
|
+
cfg.markers = Array.isArray(cfg.markers) ? cfg.markers : [];
|
|
236
|
+
}
|
|
237
|
+
|
|
166
238
|
// Gauge: a single value, from `value` or the first series datum.
|
|
167
239
|
if (cfg.type === 'gauge') {
|
|
168
240
|
if (cfg.value != null && !cfg.series.length) cfg.series = [{ data: [num(cfg.value, 0)] }];
|
|
@@ -285,7 +357,28 @@
|
|
|
285
357
|
const cfg = state.config; const d3 = state.d3; const el = state.el;
|
|
286
358
|
if (!cfg || !d3) return;
|
|
287
359
|
const width = Math.max(120, el.clientWidth || el.getBoundingClientRect().width || 600);
|
|
288
|
-
|
|
360
|
+
|
|
361
|
+
// Maps have their own data shape, async geo deps, and aspect-based
|
|
362
|
+
// height — handled before the cartesian/series path.
|
|
363
|
+
if (cfg.type === 'map') {
|
|
364
|
+
el.innerHTML = ''; probePalette(el);
|
|
365
|
+
const geo = window.__manifestGeo;
|
|
366
|
+
if (!geo) {
|
|
367
|
+
el.style.minHeight = (cfg.height || 320) + 'px';
|
|
368
|
+
loadGeo().then(() => state.schedule()).catch(() => state.renderError('Map data failed to load.'));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
_suppressAnim = !!state._drawn; state._drawn = true;
|
|
372
|
+
const mh = cfg.height || Math.round(width * 0.52);
|
|
373
|
+
el.style.minHeight = mh + 'px';
|
|
374
|
+
const mroot = svg('svg', { viewBox: `0 0 ${width} ${mh}`, width: '100%', height: String(mh), role: 'img', 'aria-label': cfg.title || 'map', preserveAspectRatio: 'xMidYMid meet' }, el);
|
|
375
|
+
drawMap(state, mroot, width, mh, geo);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Gantt sizes by track count, not a fixed height.
|
|
380
|
+
const height = cfg.type === 'gantt' ? ganttHeight(cfg) : cfg.height;
|
|
381
|
+
if (cfg.type === 'gantt') el.style.minHeight = height + 'px';
|
|
289
382
|
|
|
290
383
|
el.innerHTML = '';
|
|
291
384
|
probePalette(el);
|
|
@@ -304,9 +397,16 @@
|
|
|
304
397
|
if (cfg.type === 'pie' || cfg.type === 'donut') drawPie(state, root, width, height);
|
|
305
398
|
else if (cfg.type === 'gauge') drawGauge(state, root, width, height);
|
|
306
399
|
else if (cfg.type === 'heatmap') drawHeatmap(state, root, width, height);
|
|
400
|
+
else if (cfg.type === 'gantt') drawGantt(state, root, width, height);
|
|
307
401
|
else drawCartesian(state, root, width, height);
|
|
308
402
|
}
|
|
309
403
|
|
|
404
|
+
// Gantt height derives from track count (one lane per series) + axis.
|
|
405
|
+
function ganttHeight(cfg) {
|
|
406
|
+
const n = Math.max(1, (cfg.series || []).length);
|
|
407
|
+
return 4 + n * cfg.rowHeight + 22;
|
|
408
|
+
}
|
|
409
|
+
|
|
310
410
|
// Palette size is CSS-driven: count consecutive --color-chart-N custom
|
|
311
411
|
// properties (themes can extend past 8 by defining --color-chart-9, …);
|
|
312
412
|
// segment colours cycle through however many exist. Re-probed per draw
|
|
@@ -671,6 +771,261 @@
|
|
|
671
771
|
state.el.appendChild(footer);
|
|
672
772
|
}
|
|
673
773
|
|
|
774
|
+
// Gantt — each series is a track (row); its data is an array
|
|
775
|
+
// of `{ from, to, status?, label?, color? }` segments on a shared X axis.
|
|
776
|
+
// The axis adapts to the data: numbers → linear (0–10, distance, %),
|
|
777
|
+
// date-ish values → time, other strings → category (ordinal stages). A
|
|
778
|
+
// segment with no `to` is a point marker. Single track = a status strip;
|
|
779
|
+
// many tracks = swimlanes / Gantt.
|
|
780
|
+
function drawGantt(state, root, width, height) {
|
|
781
|
+
const cfg = state.config, d3 = state.d3;
|
|
782
|
+
const tracks = cfg.series;
|
|
783
|
+
const trackName = (t, i) => t.name || String(i + 1);
|
|
784
|
+
const showLabels = cfg.axis;
|
|
785
|
+
const unit = cfg.unit || '';
|
|
786
|
+
const m = { top: 4, right: 10, bottom: 22, left: showLabels ? 92 : 8 };
|
|
787
|
+
const iw = width - m.left - m.right;
|
|
788
|
+
const ih = height - m.top - m.bottom;
|
|
789
|
+
|
|
790
|
+
// Axis kind from the first segment's `from`.
|
|
791
|
+
let firstFrom;
|
|
792
|
+
for (const t of tracks) { if (t.data && t.data.length) { firstFrom = t.data[0].from; break; } }
|
|
793
|
+
const mode = (firstFrom instanceof Date) ? 'time'
|
|
794
|
+
: typeof firstFrom === 'number' ? 'numeric'
|
|
795
|
+
: (!isNaN(Date.parse(firstFrom)) ? 'time' : 'category');
|
|
796
|
+
|
|
797
|
+
let x, ticks, fmt, pos;
|
|
798
|
+
if (mode === 'category') {
|
|
799
|
+
const seen = [];
|
|
800
|
+
tracks.forEach(t => (t.data || []).forEach(s => [s.from, s.to].forEach(v => { if (v != null && seen.indexOf(String(v)) < 0) seen.push(String(v)); })));
|
|
801
|
+
const domain = (cfg.labels && cfg.labels.length) ? cfg.labels.map(String) : seen;
|
|
802
|
+
x = d3.scalePoint().domain(domain).range([0, iw]);
|
|
803
|
+
pos = v => x(String(v));
|
|
804
|
+
ticks = domain;
|
|
805
|
+
fmt = v => String(v);
|
|
806
|
+
} else {
|
|
807
|
+
const conv = mode === 'time' ? toTime : (v => num(v, 0));
|
|
808
|
+
let dmin = Infinity, dmax = -Infinity;
|
|
809
|
+
tracks.forEach(t => (t.data || []).forEach(s => {
|
|
810
|
+
const a = conv(s.from), b = conv(s.to != null ? s.to : s.from);
|
|
811
|
+
if (a < dmin) dmin = a; if (b > dmax) dmax = b;
|
|
812
|
+
}));
|
|
813
|
+
if (cfg.min != null) dmin = conv(cfg.min);
|
|
814
|
+
if (cfg.max != null) dmax = conv(cfg.max);
|
|
815
|
+
if (!isFinite(dmin) || !isFinite(dmax) || dmin === dmax) { dmin = 0; dmax = 1; }
|
|
816
|
+
x = (mode === 'time' ? d3.scaleTime() : d3.scaleLinear()).domain([dmin, dmax]).range([0, iw]);
|
|
817
|
+
pos = v => x(conv(v));
|
|
818
|
+
ticks = x.ticks(Math.max(2, Math.floor(iw / 80)));
|
|
819
|
+
fmt = mode === 'time' ? ganttFmt(dmin, dmax) : (v => String(v) + unit);
|
|
820
|
+
}
|
|
821
|
+
const tipVal = v => mode === 'time' ? fmt(toTime(v)) : String(v) + (mode === 'numeric' ? unit : '');
|
|
822
|
+
|
|
823
|
+
const lane = ih / Math.max(1, tracks.length);
|
|
824
|
+
const pad = Math.min(6, lane * 0.18);
|
|
825
|
+
const plot = svg('g', { transform: `translate(${m.left},${m.top})` }, root);
|
|
826
|
+
|
|
827
|
+
// Grid lines + axis ticks (shared, at the bottom).
|
|
828
|
+
ticks.forEach(tk => {
|
|
829
|
+
const xx = pos(tk);
|
|
830
|
+
if (xx == null || isNaN(xx)) return;
|
|
831
|
+
if (cfg.grid) svg('line', { x1: xx, x2: xx, y1: 0, y2: ih }, plot);
|
|
832
|
+
text(plot, fmt(tk), { x: xx, y: ih + 14, 'text-anchor': 'middle' });
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// Status → colour, stable across tracks. A per-segment `color` wins;
|
|
836
|
+
// else the config `colors` map; else the palette in first-seen order.
|
|
837
|
+
const statusColors = {}; let next = 0;
|
|
838
|
+
const colorFor = (s) => {
|
|
839
|
+
if (s.color) return s.color;
|
|
840
|
+
if (s.status == null) return seriesColorVar(0);
|
|
841
|
+
if (!(s.status in statusColors)) statusColors[s.status] = (cfg.colors && cfg.colors[s.status]) || seriesColorVar(next++);
|
|
842
|
+
return statusColors[s.status];
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
tracks.forEach((t, ti) => {
|
|
846
|
+
const y0 = ti * lane, by = y0 + pad, bh = Math.max(0, lane - pad * 2);
|
|
847
|
+
if (showLabels) text(plot, trackName(t, ti), { class: 'gantt-track', x: -8, y: y0 + lane / 2, 'text-anchor': 'end', 'dominant-baseline': 'central' });
|
|
848
|
+
(t.data || []).forEach(s => {
|
|
849
|
+
const x1 = pos(s.from);
|
|
850
|
+
if (x1 == null || isNaN(x1)) return;
|
|
851
|
+
const x2 = s.to != null ? pos(s.to) : x1;
|
|
852
|
+
const color = colorFor(s);
|
|
853
|
+
const label = s.status != null ? s.status : (s.label || '');
|
|
854
|
+
if (!(x2 > x1)) {
|
|
855
|
+
const pt = svg('rect', { class: 'gantt-point', x: x1 - 1.5, y: by, width: 3, height: bh, style: `--color-chart-color:${color}` }, plot);
|
|
856
|
+
applyTip(pt, (t.name ? t.name + ' · ' : '') + label + ' · ' + tipVal(s.from), cfg);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const seg = svg('rect', { class: 'gantt-segment', x: x1, y: by, width: Math.max(1, x2 - x1), height: bh, style: `--color-chart-color:${color}` }, plot);
|
|
860
|
+
applyTip(seg, (t.name ? t.name + ' · ' : '') + label + ' · ' + tipVal(s.from) + ' – ' + tipVal(s.to), cfg);
|
|
861
|
+
animate(seg, [{ opacity: 0 }, { opacity: 1 }], { duration: 300, delay: ti * 30 });
|
|
862
|
+
if (cfg.dataLabels && label && (x2 - x1) > 26) dataLabel(plot, label, (x1 + x2) / 2, y0 + lane / 2, 'middle', 'central', 'inverse');
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// Reference markers (e.g. a "now" line).
|
|
867
|
+
cfg.markers.forEach(mk => {
|
|
868
|
+
const xx = pos(mk.at);
|
|
869
|
+
if (!(xx >= 0 && xx <= iw)) return;
|
|
870
|
+
svg('line', { class: 'gantt-marker', x1: xx, x2: xx, y1: 0, y2: ih, style: mk.color ? `--color-chart-color:${mk.color}` : '' }, plot);
|
|
871
|
+
if (mk.label) text(plot, mk.label, { class: 'gantt-marker-label', x: xx, y: -1, 'text-anchor': 'middle' });
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// Status legend (categorical), when more than one is present.
|
|
875
|
+
const statuses = Object.keys(statusColors);
|
|
876
|
+
if (cfg.legend && statuses.length > 1) drawSwatchLegend(state, statuses.map(name => ({ name, color: statusColors[name] })));
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Axis/tooltip date formatter sized to the visible span.
|
|
880
|
+
function ganttFmt(dmin, dmax) {
|
|
881
|
+
const DAY = 86400000, span = dmax - dmin;
|
|
882
|
+
const opts = span <= 2 * DAY ? { hour: '2-digit', minute: '2-digit' }
|
|
883
|
+
: span <= 120 * DAY ? { month: 'short', day: 'numeric' }
|
|
884
|
+
: { month: 'short', year: 'numeric' };
|
|
885
|
+
let f; try { f = new Intl.DateTimeFormat(chartLocale(), opts); } catch (_) { f = null; }
|
|
886
|
+
return v => f ? f.format(new Date(v)) : new Date(v).toISOString().slice(0, 16);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/* ---- World map (choropleth) --------------------------------- */
|
|
890
|
+
// Resolve an author key to country numeric id(s): numeric ISO, alpha-2,
|
|
891
|
+
// alpha-3, continent (name or code → all members), or country name (atlas
|
|
892
|
+
// names + a few common aliases). Returns [] when unresolved.
|
|
893
|
+
const _mapAlias = { 'united states': 'united states of america', usa: 'united states of america', 'u.s.': 'united states of america', uk: 'united kingdom', 'great britain': 'united kingdom', russia: 'russia', czechia: 'czechia', 'czech republic': 'czechia', 'south korea': 'south korea', 'north korea': 'north korea', 'dr congo': 'dem. rep. congo', 'democratic republic of the congo': 'dem. rep. congo', tanzania: 'tanzania', bolivia: 'bolivia', laos: 'laos' };
|
|
894
|
+
const _contAlias = { 'n. america': 'north america', 'n america': 'north america', 's. america': 'south america', 's america': 'south america' };
|
|
895
|
+
let _nameIdx = null;
|
|
896
|
+
function nameIndex(geo) {
|
|
897
|
+
if (_nameIdx) return _nameIdx;
|
|
898
|
+
_nameIdx = {};
|
|
899
|
+
geo.world.objects.countries.geometries.forEach(g => { if (g.properties && g.properties.name) _nameIdx[String(g.properties.name).toLowerCase()] = Number(g.id); });
|
|
900
|
+
Object.keys(_mapAlias).forEach(a => { const id = _nameIdx[_mapAlias[a]]; if (id != null) _nameIdx[a] = id; });
|
|
901
|
+
return _nameIdx;
|
|
902
|
+
}
|
|
903
|
+
function resolveRegion(key, geo) {
|
|
904
|
+
const k = String(key).trim(); if (!k) return { kind: '', ids: [] };
|
|
905
|
+
if (/^\d+$/.test(k)) return { kind: 'country', ids: [Number(k)] };
|
|
906
|
+
const meta = geo.meta || {}, up = k.toUpperCase(), low = k.toLowerCase();
|
|
907
|
+
if (k.length === 2 && meta.a2 && meta.a2[up] != null) return { kind: 'country', ids: [meta.a2[up]] };
|
|
908
|
+
if (k.length === 3 && meta.a3 && meta.a3[up] != null) return { kind: 'country', ids: [meta.a3[up]] };
|
|
909
|
+
const members = meta.members || {};
|
|
910
|
+
const cont = Object.keys(members).find(c => c.toLowerCase() === (_contAlias[low] || low));
|
|
911
|
+
if (cont) return { kind: 'continent', code: cont, ids: members[cont] };
|
|
912
|
+
const id = nameIndex(geo)[low];
|
|
913
|
+
return id != null ? { kind: 'country', ids: [id] } : { kind: '', ids: [] };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Choropleth: country (or continent-grouped) regions coloured sequentially
|
|
917
|
+
// (numeric values → heat ramp) or categorically (string values → palette).
|
|
918
|
+
function drawMap(state, root, width, height, geo) {
|
|
919
|
+
const cfg = state.config;
|
|
920
|
+
const fc = geo.topojson.feature(geo.world, geo.world.objects.countries);
|
|
921
|
+
const projection = geo.geoNaturalEarth1().fitSize([width, height], fc);
|
|
922
|
+
const path = geo.geoPath(projection);
|
|
923
|
+
|
|
924
|
+
// Author data: an object { key: value } or an array of { id|code|name, value } / [key, value].
|
|
925
|
+
let entries = [];
|
|
926
|
+
if (Array.isArray(cfg.data)) entries = cfg.data.map(d => Array.isArray(d) ? [d[0], d[1]] : [d.id != null ? d.id : (d.code != null ? d.code : d.name), d.value]);
|
|
927
|
+
else if (cfg.data && typeof cfg.data === 'object') entries = Object.entries(cfg.data);
|
|
928
|
+
|
|
929
|
+
const valueById = {};
|
|
930
|
+
entries.forEach(([key, val]) => resolveRegion(key, geo).ids.forEach(id => { valueById[id] = val; }));
|
|
931
|
+
|
|
932
|
+
// Display-name resolution: `_ui.map.regions` override → localized
|
|
933
|
+
// pack (per $locale) → English atlas name. Async packs trigger a
|
|
934
|
+
// redraw when they arrive.
|
|
935
|
+
const num2a2 = (geo.meta && geo.meta.num2a2) || {};
|
|
936
|
+
const locale = chartLocale();
|
|
937
|
+
const ui = (window.ManifestUI && window.ManifestUI.resolve) ? window.ManifestUI.resolve('map', {}) : {};
|
|
938
|
+
const overrides = ui.regions || {};
|
|
939
|
+
let pack = null;
|
|
940
|
+
if (locale && locale !== 'en') { if (_namePacks[locale]) pack = _namePacks[locale]; else loadCountryNames(locale).then(() => state.schedule()); }
|
|
941
|
+
const displayName = (id, eng) => {
|
|
942
|
+
const a2 = num2a2[id];
|
|
943
|
+
if (a2 != null && overrides[a2] != null) return overrides[a2];
|
|
944
|
+
if (overrides[id] != null) return overrides[id];
|
|
945
|
+
if (overrides[String(id)] != null) return overrides[String(id)];
|
|
946
|
+
if (overrides[eng] != null) return overrides[eng];
|
|
947
|
+
if (pack && a2 && pack[a2]) return pack[a2];
|
|
948
|
+
return eng;
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
// Author-supplied place gazetteer (config `places` or `_ui.map.cities`)
|
|
952
|
+
// for resolving point names → coords. No city data is bundled; authors
|
|
953
|
+
// source it from a third-party library in their own project.
|
|
954
|
+
const placesSrc = cfg.places || ui.cities || null;
|
|
955
|
+
let places = null;
|
|
956
|
+
if (placesSrc) { places = {}; Object.keys(placesSrc).forEach(k => { places[k.toLowerCase()] = placesSrc[k]; }); }
|
|
957
|
+
|
|
958
|
+
// Sequential when every value is numeric, else categorical.
|
|
959
|
+
const vals = entries.map(e => e[1]);
|
|
960
|
+
const numeric = vals.length > 0 && vals.every(v => typeof v === 'number' || (typeof v === 'string' && v.trim() !== '' && !isNaN(+v)));
|
|
961
|
+
let lo = Infinity, hi = -Infinity;
|
|
962
|
+
if (numeric) { Object.values(valueById).forEach(v => { const n = +v; if (n < lo) lo = n; if (n > hi) hi = n; }); if (!isFinite(lo)) { lo = 0; hi = 1; } if (lo === hi) hi = lo + 1; }
|
|
963
|
+
const palette = {}; let pIdx = 0;
|
|
964
|
+
|
|
965
|
+
const plot = svg('g', {}, root);
|
|
966
|
+
fc.features.forEach(f => {
|
|
967
|
+
const id = Number(f.id), v = valueById[id];
|
|
968
|
+
const region = svg('path', { class: 'map-region', d: path(f) }, plot);
|
|
969
|
+
const name = displayName(id, (f.properties && f.properties.name) || String(id));
|
|
970
|
+
if (v != null && v !== '') {
|
|
971
|
+
if (numeric) { const t = Math.round((+v - lo) / (hi - lo) * 100); region.setAttribute('style', `--color-chart-color:color-mix(in oklch, var(--color-chart-heat-high) ${t}%, var(--color-chart-heat-low))`); }
|
|
972
|
+
else { if (!(v in palette)) palette[v] = seriesColorVar(pIdx++); region.setAttribute('style', `--color-chart-color:${palette[v]}`); }
|
|
973
|
+
applyTip(region, name + ': ' + v, cfg);
|
|
974
|
+
animate(region, [{ opacity: 0 }, { opacity: 1 }], { duration: 250 });
|
|
975
|
+
} else {
|
|
976
|
+
applyTip(region, name, cfg);
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
// City / point markers (proportional symbols) — projected through the
|
|
981
|
+
// same projection. `points: [{ lat, lng, value?, label?, color? }]`.
|
|
982
|
+
const pts = Array.isArray(cfg.points) ? cfg.points : [];
|
|
983
|
+
if (pts.length) {
|
|
984
|
+
const coord = (p) => {
|
|
985
|
+
let lat = p.lat != null ? p.lat : p.latitude;
|
|
986
|
+
let lng = p.lng != null ? p.lng : (p.lon != null ? p.lon : (p.long != null ? p.long : p.longitude));
|
|
987
|
+
if ((lat == null || lng == null) && places) {
|
|
988
|
+
const hit = places[String(p.city || p.name || '').toLowerCase()];
|
|
989
|
+
if (hit) { if (Array.isArray(hit)) { lat = hit[0]; lng = hit[1]; } else { lat = hit.lat; lng = hit.lng; } }
|
|
990
|
+
}
|
|
991
|
+
return (lat == null || lng == null) ? null : [+lng, +lat];
|
|
992
|
+
};
|
|
993
|
+
const pv = pts.map(p => p.value);
|
|
994
|
+
const sized = pv.length > 0 && pv.every(v => typeof v === 'number');
|
|
995
|
+
let pmin = Infinity, pmax = -Infinity;
|
|
996
|
+
if (sized) { pv.forEach(v => { if (v < pmin) pmin = v; if (v > pmax) pmax = v; }); if (!isFinite(pmin)) { pmin = 0; pmax = 1; } if (pmin === pmax) pmax = pmin + 1; }
|
|
997
|
+
const rOf = (v) => sized ? 3 + Math.sqrt((+v - pmin) / (pmax - pmin)) * 13 : 4;
|
|
998
|
+
pts.forEach(p => {
|
|
999
|
+
const c = coord(p), xy = c && projection(c);
|
|
1000
|
+
if (!xy) return;
|
|
1001
|
+
const label = p.label || p.city || p.name || '';
|
|
1002
|
+
const dot = svg('circle', { class: 'map-point', cx: xy[0], cy: xy[1], r: rOf(p.value), style: p.color ? `--color-chart-color:${p.color}` : '' }, plot);
|
|
1003
|
+
applyTip(dot, label + (p.value != null ? (label ? ': ' : '') + p.value : ''), cfg);
|
|
1004
|
+
animate(dot, [{ opacity: 0 }, { opacity: 1 }], { duration: 250 });
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (cfg.legend) {
|
|
1009
|
+
if (numeric) drawHeatLegend(state, Math.round(lo), Math.round(hi), { left: 8, right: 8 });
|
|
1010
|
+
else if (entries.length) { const items = Object.keys(palette).map(name => ({ name, color: palette[name] })); if (items.length) drawSwatchLegend(state, items); }
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Categorical swatch legend from an explicit [{ name, color }] list
|
|
1015
|
+
// (gantt statuses carry their own colour, unlike series legends).
|
|
1016
|
+
function drawSwatchLegend(state, items) {
|
|
1017
|
+
const footer = document.createElement('footer');
|
|
1018
|
+
items.forEach(it => {
|
|
1019
|
+
const item = document.createElement('span');
|
|
1020
|
+
const sw = document.createElement('i');
|
|
1021
|
+
sw.style.setProperty('--color-chart-color', it.color);
|
|
1022
|
+
const tx = document.createElement('span');
|
|
1023
|
+
tx.textContent = it.name;
|
|
1024
|
+
item.append(sw, tx); footer.appendChild(item);
|
|
1025
|
+
});
|
|
1026
|
+
state.el.appendChild(footer);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
674
1029
|
// Legend is a <footer> sibling below the SVG (inline flex), not an
|
|
675
1030
|
// absolute overlay — so it never collides with axis labels. Each item is
|
|
676
1031
|
// a <span> with an <i> swatch carrying the series colour.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
@layer components {
|
|
4
4
|
|
|
5
|
-
/*
|
|
5
|
+
/* Wrapper */
|
|
6
6
|
:where(.combobox):not(.unstyle) {
|
|
7
7
|
display: flex;
|
|
8
8
|
flex-wrap: wrap;
|
|
@@ -40,16 +40,8 @@
|
|
|
40
40
|
pointer-events: none
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/* Inner typing surface
|
|
44
|
-
manifest.input.css, which sorts after this file in the bundled CSS and
|
|
45
|
-
would otherwise re-apply its own width/background/hover to the editor. */
|
|
43
|
+
/* Inner typing surface */
|
|
46
44
|
&> :where(input:not([type=hidden]), textarea):not(.unstyle) {
|
|
47
|
-
/* flex-basis (not min-width) sets the wrap threshold: the editor fills
|
|
48
|
-
the rest of the row, and once less than 7rem is left it wraps to its
|
|
49
|
-
own line. min-width:0 is REQUIRED — a flex item's default min-width:auto
|
|
50
|
-
is its content size, and WebKit enforces it by crushing the
|
|
51
|
-
flex-shrink:0 chips to an ellipsis. Resetting it to 0 lets the editor
|
|
52
|
-
shrink/wrap instead, so the chips keep their room. */
|
|
53
45
|
flex: 1 1 7rem;
|
|
54
46
|
min-width: 0;
|
|
55
47
|
width: auto;
|
|
@@ -75,7 +67,7 @@
|
|
|
75
67
|
/* Chips */
|
|
76
68
|
:where(.combobox-chip):not(.unstyle) {
|
|
77
69
|
display: inline-flex;
|
|
78
|
-
flex: 0 0 auto;
|
|
70
|
+
flex: 0 0 auto;
|
|
79
71
|
align-items: center;
|
|
80
72
|
gap: var(--spacing, 0.25rem);
|
|
81
73
|
max-width: 100%;
|
|
@@ -117,7 +109,7 @@
|
|
|
117
109
|
}
|
|
118
110
|
}
|
|
119
111
|
|
|
120
|
-
/*
|
|
112
|
+
/* Non-removable */
|
|
121
113
|
&[data-locked] {
|
|
122
114
|
padding-inline-end: calc(var(--spacing, 0.25rem) * 2);
|
|
123
115
|
cursor: default
|
|
@@ -130,9 +122,8 @@
|
|
|
130
122
|
}
|
|
131
123
|
}
|
|
132
124
|
|
|
133
|
-
/*
|
|
134
|
-
|
|
135
|
-
:where(.combobox) > button:not(.unstyle) {
|
|
125
|
+
/* Button trigger */
|
|
126
|
+
:where(.combobox)>button:not(.unstyle) {
|
|
136
127
|
flex: 1 1 auto;
|
|
137
128
|
align-self: stretch;
|
|
138
129
|
min-width: 7rem;
|
|
@@ -161,10 +152,7 @@
|
|
|
161
152
|
}
|
|
162
153
|
}
|
|
163
154
|
|
|
164
|
-
/* Chip-less
|
|
165
|
-
the field — it fills the whole wrapper and carries the padding, so the click
|
|
166
|
-
target, caret and text selection align with the field edges instead of
|
|
167
|
-
floating inside the wrapper's padding. */
|
|
155
|
+
/* Chip-less */
|
|
168
156
|
:where(.combobox):has(> :where(input:not([type=hidden]), textarea, button):not(.unstyle)):not(:has(.combobox-chip)) {
|
|
169
157
|
padding: 0;
|
|
170
158
|
|
|
@@ -175,15 +163,10 @@
|
|
|
175
163
|
}
|
|
176
164
|
}
|
|
177
165
|
|
|
178
|
-
/* Listbox
|
|
179
|
-
Selectors deliberately AVOID :where() on the option part so they out-specify
|
|
180
|
-
dropdown.css's `menu li { display: inline-flex }` (0,1,0), which sorts after
|
|
181
|
-
this file in the bundle and would otherwise keep filtered options visible. */
|
|
166
|
+
/* Listbox */
|
|
182
167
|
:where(menu[role=listbox]):not(.unstyle) {
|
|
183
168
|
|
|
184
|
-
/* Active descendant
|
|
185
|
-
On a mouse-opened menu the hover state alone highlights, so the first
|
|
186
|
-
option isn't left with a persistent background. */
|
|
169
|
+
/* Active descendant */
|
|
187
170
|
&[data-kbd] [role=option][aria-current="true"] {
|
|
188
171
|
color: var(--color-field-inverse, oklch(43.9% 0 0));
|
|
189
172
|
background-color: var(--color-field-surface, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent))
|