PageSourceSearch

https://pictorx.com/js/charts/chart-config.js?v=OzY_ho1N8qkLk0a661xGBXk89Gbh6oOu86LVlTyJ4A0

js pictorx.com collected 2026-10-09 04:46:28 UTC 23,689 bytes, 568 lines download raw bytes

1/** Parses chart config and resolves per-type theme options. */
2
3class ChartConfig {
4
5    static CHART_DEFAULT_SUBTITLE = 'Sample data';
6
7    /** @returns {object} From server ChartPresentationDefaults (injected in _Layout or fetched from API). */
8    static _presentation() {
9        if (!ChartConfig._presentationCache) {
10            ChartConfig._presentationCache = (typeof window !== 'undefined' && window.__chartPresentationDefaults)
11                ? window.__chartPresentationDefaults
12                : ChartConfig._PRESENTATION_FALLBACK;
13        }
14        return ChartConfig._presentationCache;
15    }
16
17    /** Load presentation defaults from the API when not injected (e.g. tests). */
18    static async loadPresentationDefaults() {
19        if (typeof window === 'undefined') {
20            return ChartConfig._PRESENTATION_FALLBACK;
21        }
22        if (window.__chartPresentationDefaults) {
23            ChartConfig._presentationCache = window.__chartPresentationDefaults;
24            return ChartConfig._presentationCache;
25        }
26        const res = await fetch('/api/charts/presentation-defaults');
27        if (!res.ok) {
28            return ChartConfig._PRESENTATION_FALLBACK;
29        }
30        ChartConfig._presentationCache = await res.json();
31        window.__chartPresentationDefaults = ChartConfig._presentationCache;
32        return ChartConfig._presentationCache;
33    }
34
35    /** Used when chart-config.js is loaded outside the web app (tests). */
36    static _PRESENTATION_FALLBACK = {
37        defaultSubtitle: ChartConfig.CHART_DEFAULT_SUBTITLE,
38        titleFontSize: 30,
39        subtitleFontSize: 15,
40        headerHeightPercent: 14.5,
41        headerHeightPercentBoth: 17.0,
42        headerTitleYPercent: 30.0,
43        headerSubtitleYPercent: 67.0,
44        categoryFontSize: 16,
45        scaleFontSize: 14,
46        valueFontSize: 14,
47        legendFontSize: 14,
48        gaugeValueFontSize: 28,
49        maxFontSize: 200,
50        referenceChartWidth: 1000,
51        referenceChartHeight: 600,
52        plotBorderColor: '#CBD5E1',
53        bodyStrokePercent: 1,
54        defaultBarColors: ['#0EA5E9', '#8B5CF6', '#10B981', '#F59E0B', '#EC4899', '#06B6D4'],
55        defaultLineColors: ['#2563EB', '#8B5CF6', '#10B981', '#F59E0B', '#EC4899'],
56        defaultPieColors: ['#0EA5E9', '#8B5CF6', '#10B981', '#F59E0B', '#EC4899', '#06B6D4'],
57        defaultVennCircleColors: ['#0EA5E9', '#EC4899', '#10B981'],
58        radarGridRings: 6,
59        chartDefaults: {
60            scatter: {
61                type: 'scatter',
62                title: 'Scatter Plot',
63                showValues: false,
64                showLegend: false,
65                scatter: { hasLegend: false, showTrendLine: true, pointRadius: 14 },
66                bar: {},
67            },
68            column: {
69                type: 'column',
70                title: 'Column Chart',
71                showValues: true,
72                bar: { orientation: 'vertical', layoutMode: 'single' },
73            },
74        },
75    };
76
77    static _presentationCache = null;
78
79    /** One color for every series/slice when set in chart properties. */
80    static pickColor(palette, index, uniformColor) {
81        if (uniformColor) return uniformColor;
82        const colors = Array.isArray(palette) && palette.length ? palette : ChartConfig.DEFAULT_BAR_COLORS;
83        return colors[index % colors.length];
84    }
85
86    /** Bubble fill: uniform pointColor, else one palette entry per bubble (by point index). */
87    static pickBubblePointColor(bubble, pointIndex) {
88        const b = bubble || {};
89        if (typeof b.pointColor === 'string' && b.pointColor) return b.pointColor;
90        return ChartConfig.pickColor(b.pointColors, pointIndex, null);
91    }
92
93    static setNestedValue(obj, path, value) {
94        const parts = String(path).split('.');
95        let cur = obj;
96        for (let i = 0; i < parts.length - 1; i++) {
97            const key = parts[i];
98            if (!cur[key] || typeof cur[key] !== 'object') cur[key] = {};
99            cur = cur[key];
100        }
101        cur[parts[parts.length - 1]] = value;
102    }
103
104    static getNestedValue(obj, path) {
105        return String(path).split('.').reduce((cur, key) => (cur && typeof cur === 'object' ? cur[key] : undefined), obj);
106    }
107
108    static get DEFAULT_SUBTITLE() {
109        return ChartConfig._presentation().defaultSubtitle || ChartConfig.CHART_DEFAULT_SUBTITLE;
110    }
111
112    static get DEFAULT_TITLE_FONT_SIZE() {
113        return ChartConfig._presentation().titleFontSize;
114    }
115
116    static get DEFAULT_SUBTITLE_FONT_SIZE() {
117        return ChartConfig._presentation().subtitleFontSize;
118    }
119
120    /** Chart body / plot card border (slightly darker than grid #E2E8F0). */
121    static get DEFAULT_PLOT_BORDER_COLOR() {
122        return ChartConfig._presentation().plotBorderColor;
123    }
124
125    /** Chart body card outline width (% of layer min side). */
126    static get CHART_BODY_STROKE_PCT() {
127        return ChartConfig._presentation().bodyStrokePercent;
128    }
129
130    static get DEFAULT_CATEGORY_FONT_SIZE() {
131        return ChartConfig._presentation().categoryFontSize;
132    }
133
134    static get DEFAULT_SCALE_FONT_SIZE() {
135        return ChartConfig._presentation().scaleFontSize;
136    }
137
138    static get DEFAULT_VALUE_FONT_SIZE() {
139        return ChartConfig._presentation().valueFontSize;
140    }
141
142    static get DEFAULT_LEGEND_FONT_SIZE() {
143        return ChartConfig._presentation().legendFontSize;
144    }
145
146    /** Center readout on gauge / ring gauge charts. */
147    static get DEFAULT_GAUGE_VALUE_FONT_SIZE() {
148        return ChartConfig._presentation().gaugeValueFontSize;
149    }
150
151    /** Header band height as a fraction of chart height (title or subtitle only). */
152    static get CHART_HEADER_HEIGHT_FRAC() {
153        return ChartConfig._presentation().headerHeightPercent / 100;
154    }
155
156    /** Taller header when both title and subtitle are shown. */
157    static get CHART_HEADER_HEIGHT_FRAC_BOTH() {
158        return ChartConfig._presentation().headerHeightPercentBoth / 100;
159    }
160
161    /** Title baseline as a fraction of header height from the top of the header band. */
162    static get CHART_HEADER_TITLE_Y_FRAC() {
163        return ChartConfig._presentation().headerTitleYPercent / 100;
164    }
165
166    /** Subtitle baseline as a fraction of header height from the top of the header band. */
167    static get CHART_HEADER_SUBTITLE_Y_FRAC() {
168        return ChartConfig._presentation().headerSubtitleYPercent / 100;
169    }
170
171    static get DEFAULT_BAR_COLORS() {
172        return ChartConfig._presentation().defaultBarColors;
173    }
174
175    static get DEFAULT_LINE_COLORS() {
176        return ChartConfig._presentation().defaultLineColors;
177    }
178
179    static get DEFAULT_PIE_COLORS() {
180        return ChartConfig._presentation().defaultPieColors;
181    }
182
183    static get DEFAULT_VENN_CIRCLE_COLORS() {
184        return ChartConfig._presentation().defaultVennCircleColors
185            || ['#0EA5E9', '#EC4899', '#10B981'];
186    }
187
188    static get RADAR_GRID_RINGS() {
189        return ChartConfig._presentation().radarGridRings ?? 6;
190    }
191
192    /** Deep-clone a chart default entry from server presentation defaults. */
193    static chartDefaults(key) {
194        const defs = ChartConfig._presentation().chartDefaults;
195        const base = defs?.[key];
196        if (!base || typeof base !== 'object') return null;
197        return JSON.parse(JSON.stringify(base));
198    }
199
200    static _applyHeaderFonts(config) {
201        const header = ChartConfig._defaultHeaderBar();
202        for (const sec of ChartConfig.CONFIG_FONT_SECTIONS) {
203            if (config[sec] != null && typeof config[sec] === 'object') {
204                config[sec] = { ...header, ...config[sec] };
205            }
206        }
207        return config;
208    }
209
210    static _applyVennCircleColors(config) {
211        const colors = ChartConfig.DEFAULT_VENN_CIRCLE_COLORS;
212        if (!config.venn || typeof config.venn !== 'object') return config;
213        config.venn = {
214            circle1Color: colors[0],
215            circle2Color: colors[1],
216            circle3Color: colors[2],
217            ...config.venn,
218        };
219        return config;
220    }
221
222    static _applyRadarRingCount(config) {
223        if (!config.radar || typeof config.radar !== 'object') return config;
224        if (config.radar.ringCount == null) {
225            config.radar.ringCount = ChartConfig.RADAR_GRID_RINGS;
226        }
227        return config;
228    }
229
230    /**
231     * Build a parsed chart config from a server-defined default key.
232     */
233    static defaultChart(key, overrides = {}) {
234        const raw = ChartConfig.chartDefaults(key);
235        if (!raw) {
236            throw new Error(`chartDefaults.${key} is not defined in chart presentation defaults`);
237        }
238        const merged = { ...raw, ...overrides };
239        if (merged.subtitle == null) {
240            merged.subtitle = ChartConfig.DEFAULT_SUBTITLE;
241        }
242        ChartConfig._applyHeaderFonts(merged);
243        ChartConfig._applyVennCircleColors(merged);
244        ChartConfig._applyRadarRingCount(merged);
245        return ChartConfig.parse(merged);
246    }
247
248    /** Design-time defaults assume this chart frame (font sizes scale proportionally). */
249    static get REFERENCE_WIDTH() {
250        return ChartConfig._presentation().referenceChartWidth ?? 1000;
251    }
252
253    static get REFERENCE_HEIGHT() {
254        return ChartConfig._presentation().referenceChartHeight ?? 600;
255    }
256
257    static get MAX_FONT_SIZE() {
258        return ChartConfig._presentation().maxFontSize ?? 200;
259    }
260
261    static CONFIG_FONT_SECTIONS = ['bar', 'line', 'area', 'pie', 'scatter', 'bubble', 'pyramid', 'radar', 'gauge', 'venn', 'heatmap', 'treemap', 'tree', 'lollipop'];
262
263    static FONT_SIZE_KEYS = new Set([
264        'titleFontSize',
265        'subtitleFontSize',
266        'categoryFontSize',
267        'scaleFontSize',
268        'valueFontSize',
269        'legendFontSize',
270        'labelFontSize',
271    ]);
272
273    /**
274     * Non-font numeric options scaled with chart frame size (1000×600 baseline).
275     * designDefault: baseline at reference size; written into config when missing before scaling.
276     */
277    static CONFIG_NUMERIC_SCALARS = [
278        { section: 'line', key: 'pointRadius', min: 2, designDefault: 14 },
279        { section: 'line', key: 'strokeWidth', min: 1, designDefault: 6 },
280        { section: 'area', key: 'pointRadius', min: 2, designDefault: 14 },
281        { section: 'area', key: 'strokeWidth', min: 1, designDefault: 6 },
282        { section: 'lollipop', key: 'dotRadius', min: 3, designDefault: 14 },
283        { section: 'lollipop', key: 'stemWidth', min: 1, designDefault: 4 },
284        { section: 'scatter', key: 'pointRadius', min: 1 },
285        { section: 'scatter', key: 'trendLineWidth', min: 1 },
286        { section: 'radar', key: 'strokeWidth', min: 1, designDefault: 6 },
287        { section: 'radar', key: 'pointRadius', min: 2, designDefault: 14 },
288    ];
289
290    static _applySectionNumericDefaults(config) {
291        for (const item of ChartConfig.CONFIG_NUMERIC_SCALARS) {
292            const { section, key, designDefault } = item;
293            if (designDefault == null) continue;
294            if (!config[section] || typeof config[section] !== 'object') {
295                config[section] = {};
296            }
297            const cur = config[section][key];
298            if (cur == null || !Number.isFinite(Number(cur))) {
299                config[section][key] = designDefault;
300            }
301        }
302        return config;
303    }
304
305    /** Baseline numeric at reference size for a properties-panel path (e.g. line.pointRadius). */
306    static designNumericForPath(path) {
307        const item = ChartConfig.CONFIG_NUMERIC_SCALARS.find(
308            (x) => `${x.section}.${x.key}` === String(path)
309        );
310        return item?.designDefault ?? null;
311    }
312
313    /** Scaled numeric default for a path and chart frame (matches scaleConfigForBounds). */
314    static scaledDesignNumericForPath(path, width, height) {
315        const base = ChartConfig.designNumericForPath(path);
316        if (base == null) return null;
317        const item = ChartConfig.CONFIG_NUMERIC_SCALARS.find(
318            (x) => `${x.section}.${x.key}` === String(path)
319        );
320        const scale = ChartConfig.scaleFactorForBounds(width, height);
321        if (scale === 1) return Math.max(item?.min ?? 1, Math.round(base));
322        return Math.max(item?.min ?? 1, Math.round(base * scale));
323    }
324
325    static scaleFactorForBounds(width, height) {
326        const w = Math.max(1, Number(width) || 1);
327        const h = Math.max(1, Number(height) || 1);
328        return Math.min(w / ChartConfig.REFERENCE_WIDTH, h / ChartConfig.REFERENCE_HEIGHT);
329    }
330
331    static scaledFontSize(baseSize, scale) {
332        const base = Number(baseSize);
333        if (!Number.isFinite(base)) return baseSize;
334        if (!Number.isFinite(scale) || scale === 1) return Math.round(base);
335        return Math.max(1, Math.round(base * scale));
336    }
337
338    static scaleSectionFonts(section, scale) {
339        if (!section || typeof section !== 'object' || scale === 1) return;
340        for (const key of ChartConfig.FONT_SIZE_KEYS) {
341            if (section[key] != null && Number.isFinite(Number(section[key]))) {
342                section[key] = ChartConfig.scaledFontSize(section[key], scale);
343            }
344        }
345    }
346
347    static scaleSectionNumerics(section, keys, scale) {
348        if (!section || typeof section !== 'object' || scale === 1) return;
349        for (const { key, min } of keys) {
350            if (section[key] != null && Number.isFinite(Number(section[key]))) {
351                section[key] = Math.max(min ?? 1, Math.round(Number(section[key]) * scale));
352            }
353        }
354    }
355
356    /** Scale all font sizes in chart config for a frame larger/smaller than the 1000×600 baseline. */
357    static scaleConfigForBounds(config, width, height) {
358        const scale = ChartConfig.scaleFactorForBounds(width, height);
359        const scaled = ChartConfig.parse(config);
360        ChartConfig._applySectionNumericDefaults(scaled);
361        if (scale === 1) return scaled;
362        for (const sec of ChartConfig.CONFIG_FONT_SECTIONS) {
363            ChartConfig.scaleSectionFonts(scaled[sec], scale);
364        }
365        for (const sec of ChartConfig.CONFIG_FONT_SECTIONS) {
366            const keys = ChartConfig.CONFIG_NUMERIC_SCALARS.filter(item => item.section === sec);
367            if (keys.length) ChartConfig.scaleSectionNumerics(scaled[sec], keys, scale);
368        }
369        return scaled;
370    }
371
372    static layerBounds(layer) {
373        const frame = layer?.chartFrame;
374        if (frame?.w > 0 && frame?.h > 0) {
375            return { width: frame.w, height: frame.h };
376        }
377        if (layer?.w > 0 && layer?.h > 0) {
378            return { width: layer.w, height: layer.h };
379        }
380        return { width: ChartConfig.REFERENCE_WIDTH, height: ChartConfig.REFERENCE_HEIGHT };
381    }
382
383    /**
384     * Ensure chart header text exists for layer rebuild (legacy configs may omit root title).
385     * Does not run for tile previews (title/subtitle intentionally cleared).
386     */
387    static ensureHeader(cfg, { fallbackTitle } = {}) {
388        if (!cfg || typeof cfg !== 'object' || cfg.tilePreview === true) {
389            return cfg;
390        }
391
392        const title = cfg.title != null ? String(cfg.title).trim() : '';
393        const subtitle = cfg.subtitle != null ? String(cfg.subtitle).trim() : '';
394
395        if (!title && !subtitle) {
396            const fb = fallbackTitle != null ? String(fallbackTitle).trim() : '';
397            if (fb) {
398                cfg.title = fb;
399            }
400        }
401
402        return cfg;
403    }
404
405    static parse(raw) {
406        if (!raw || typeof raw !== 'object') {
407            return {
408                type: 'column', showValues: true, showLegend: false,
409                showChartBodyCard: true, showPlotCard: true,
410                bar: {}, line: {}, pie: {}, area: {}, scatter: {}, bubble: {}, pyramid: {}, radar: {}, gauge: {}, venn: {}, heatmap: {}, treemap: {}, tree: {}, lollipop: {},
411            };
412        }
413        const type = String(raw.type || 'column').toLowerCase();
414        const lollipop = ChartConfig._normalizeLollipopSection(raw.lollipop);
415        let showLegend = raw.showLegend === true;
416        let showValues = raw.showValues !== false;
417        let showChartBodyCard = raw.showChartBodyCard !== false;
418        const scatter = ChartConfig._normalizeScatterSection(raw.scatter);
419        if (type === 'lollipop') {
420            showValues = lollipop.showValues !== false;
421            showLegend = lollipop.showLegend === true;
422            showChartBodyCard = lollipop.showChartBodyCard !== false;
423        }
424        if (type === 'scatter') {
425            showLegend = scatter.hasLegend === true;
426        }
427
428        return {
429            type,
430            tilePreview: raw.tilePreview === true,
431            title: raw.title ?? null,
432            subtitle: raw.subtitle ?? null,
433            xAxisLabel: raw.xAxisLabel ?? null,
434            yAxisLabel: raw.yAxisLabel ?? null,
435            maxY: raw.maxY != null ? Number(raw.maxY) : null,
436            maxX: raw.maxX != null ? Number(raw.maxX) : null,
437            showLegend,
438            showValues,
439            showChartBodyCard,
440            showPlotCard: raw.showPlotCard !== false,
441            bar: ChartConfig._normalizeBarSection(raw.bar),
442            lollipop,
443            line: raw.line && typeof raw.line === 'object' ? raw.line : {},
444            area: raw.area && typeof raw.area === 'object' ? raw.area : {},
445            pie: raw.pie && typeof raw.pie === 'object' ? raw.pie : {},
446            scatter,
447            bubble: raw.bubble && typeof raw.bubble === 'object' ? raw.bubble : {},
448            pyramid: raw.pyramid && typeof raw.pyramid === 'object' ? raw.pyramid : {},
449            radar: raw.radar && typeof raw.radar === 'object' ? raw.radar : {},
450            gauge: raw.gauge && typeof raw.gauge === 'object' ? raw.gauge : {},
451            venn: ChartConfig._normalizeVennSection(raw.venn),
452            heatmap: raw.heatmap && typeof raw.heatmap === 'object' ? raw.heatmap : {},
453            treemap: raw.treemap && typeof raw.treemap === 'object' ? raw.treemap : {},
454            tree: raw.tree && typeof raw.tree === 'object' ? raw.tree : {},
455        };
456    }
457
458    static _normalizeVennSection(venn) {
459        if (!venn || typeof venn !== 'object') return { circleCount: 2 };
460        const section = { ...venn };
461        const n = Number(section.circleCount);
462        section.circleCount = n === 3 ? 3 : 2;
463        return section;
464    }
465
466    static _normalizeScatterSection(scatter) {
467        if (!scatter || typeof scatter !== 'object') {
468            return { hasLegend: false };
469        }
470        const section = { ...scatter };
471        if (section.hasLegend == null) section.hasLegend = false;
472        if (section.pointRadius == null && section.elements?.points?.size != null) {
473            section.pointRadius = section.elements.points.size;
474        }
475        return section;
476    }
477
478    static _normalizeLollipopSection(lollipop) {
479        if (!lollipop || typeof lollipop !== 'object') {
480            return { showValues: true, showLegend: false, showChartBodyCard: true };
481        }
482        const section = { ...lollipop };
483        if (section.showValues == null) section.showValues = true;
484        if (section.showLegend == null) section.showLegend = false;
485        if (section.showChartBodyCard == null) section.showChartBodyCard = true;
486        if (section.lollipopGap == null && section.barGap != null) {
487            section.lollipopGap = section.barGap;
488        }
489        if (section.lollipopColors == null && section.barColors != null) {
490            section.lollipopColors = section.barColors;
491        }
492        if (section.lollipopColor == null && section.barColor != null) {
493            section.lollipopColor = section.barColor;
494        }
495        return section;
496    }
497
498    static _normalizeBarSection(bar) {
499        if (!bar || typeof bar !== 'object') return {};
500        const section = { ...bar };
501        section.orientation = ChartConfig.normalizeBarOrientation(section.orientation);
502        const rawMode = section.layoutMode ?? section.LayoutMode;
503        if (rawMode === 1 || rawMode === '1' || String(rawMode).toLowerCase() === 'clustered') {
504            section.layoutMode = 'clustered';
505        } else if (rawMode === 2 || rawMode === '2' || String(rawMode).toLowerCase() === 'stacked') {
506            section.layoutMode = 'stacked';
507        } else if (rawMode != null) {
508            section.layoutMode = 'single';
509        }
510        return section;
511    }
512
513    /** C# sends BarOrientation enum as 0/1 or "Vertical"/"Horizontal"; normalize for JS. */
514    static normalizeBarOrientation(orientation) {
515        if (orientation === 0 || orientation === '0') return 'vertical';
516        if (orientation === 1 || orientation === '1') return 'horizontal';
517        const o = String(orientation ?? 'horizontal').toLowerCase();
518        if (o === 'vertical') return 'vertical';
519        return 'horizontal';
520    }
521
522    static _looksLikeVennConfig(config) {
523        const v = config?.venn;
524        if (!v || typeof v !== 'object') return false;
525        return !!(v.circle1Name || v.circle2Name || v.circle3Name);
526    }
527
528    static visualKind(config) {
529        const type = String(config?.type || 'column').toLowerCase();
530        if (type === 'lollipop') return 'lollipop';
531        if (type === 'line' || type === 'area' || type === 'pie' || type === 'donut' || type === 'multipie' || type === 'multidonut' || type === 'gauge' || type === 'venn' || type === 'heatmap' || type === 'treemap' || type === 'tree' || type === 'scatter' || type === 'bubble' || type === 'pyramid' || type === 'radar') {
532            if (type === 'multipie') return 'multiPie';
533            if (type === 'multidonut') return 'multiDonut';
534            if (type === 'pie' || type === 'donut' || type === 'gauge' || type === 'venn' || type === 'heatmap' || type === 'treemap' || type === 'tree' || type === 'scatter' || type === 'bubble' || type === 'pyramid' || type === 'radar' || type === 'line' || type === 'area') return type;
535        }
536        if (type === 'column' && ChartConfig._looksLikeVennConfig(config)) return 'venn';
537        if (type === 'column') return 'column';
538        if (type === 'bar') {
539            return ChartConfig.normalizeBarOrientation(config?.bar?.orientation) === 'vertical'
540                ? 'column'
541                : 'bar';
542        }
543        return 'column';
544    }
545
546    static isHorizontalBar(config) {
547        return ChartConfig.visualKind(config) === 'bar';
548    }
549
550    static themeOptions(config) {
551        return Chart.baseThemeOptions(config);
552    }
553
554    static isTilePreview(config) {
555        return config?.tilePreview === true;
556    }
557
558    static _defaultHeaderBar() {
559        return {
560            titleFontSize: ChartConfig.DEFAULT_TITLE_FONT_SIZE,
561            subtitleFontSize: ChartConfig.DEFAULT_SUBTITLE_FONT_SIZE,
562            categoryFontSize: ChartConfig.DEFAULT_CATEGORY_FONT_SIZE,
563            scaleFontSize: ChartConfig.DEFAULT_SCALE_FONT_SIZE,
564            valueFontSize: ChartConfig.DEFAULT_VALUE_FONT_SIZE,
565            legendFontSize: ChartConfig.DEFAULT_LEGEND_FONT_SIZE,
566        };
567    }
568}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.