@xiee/utils
Version:
Miscellaneous tools and utilities to manipulate HTML pages
134 lines (125 loc) • 4.92 kB
JavaScript
// support columnar data format in G2
G2.register("data.column", (options) => {
const { value } = options;
return async () => {
let d = value;
// handle URL strings if necessary
if (typeof value === "string") {
const resp = await fetch(value);
d = await resp.json();
}
const keys = Object.keys(d);
if (keys.length === 0) return [];
// convert to row-based data
return Array.isArray(d[keys[0]]) ? d[keys[0]].map((_, i) =>
keys.reduce((row, key) => ((row[key] = d[key][i]), row), {}),
) : [d];
};
});
// modify G2 defaults: font sizes, grid opacity, point radius, shapes, and inset
(() => {
const FONT_SCALE = 4 / 3,
FONT_RE = /[fF]ontSize$/,
POINT_RADIUS = 4;
const isObj = (v) => v && typeof v === "object" && !Array.isArray(v);
const scaleFontSizes = (obj) => {
if (!isObj(obj)) return;
for (const [key, value] of Object.entries(obj)) {
if (isObj(value)) scaleFontSizes(value);
else if (typeof value === "number" && FONT_RE.test(key))
obj[key] = value * FONT_SCALE;
}
};
const patchTheme = (theme) => {
scaleFontSizes(theme);
if (
isObj(theme.legendCategory) &&
typeof theme.legendCategory.itemMarkerSize === "number"
)
theme.legendCategory.itemMarkerSize *= FONT_SCALE;
if (isObj(theme.point))
for (const style of Object.values(theme.point))
if (isObj(style) && typeof style.r === "number") style.r = POINT_RADIUS;
if (isObj(theme.axis)) theme.axis.gridStrokeOpacity = 0.25;
theme.inset = 16;
return theme;
};
for (const name of ["Light", "Dark", "Classic", "ClassicDark", "Academy"]) {
const original = G2[name];
if (typeof original !== "function") continue;
let cache;
const patched = (...args) => (cache ??= patchTheme(original(...args)));
// Re-register in G2's internal library so charts pick up the patched theme.
// The registry key is 'theme.light', 'theme.classicDark', etc.
const key = "theme." + name[0].toLowerCase() + name.slice(1);
// Previously, we used structuredClone on the theme object, but G2's Academy
// theme contains function values (like gridFilter) that can't be
// structurally cloned. To fix it, we wrote a recursive clone that copies
// functions by reference:
const safeClone = v => {
if (v === null || typeof v !== 'object') return v;
if (Array.isArray(v)) return v.map(safeClone);
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, safeClone(x)]));
};
const cloneFn = (...args) => safeClone(patched(...args));
G2[name] = cloneFn;
try { G2.register(key, cloneFn); } catch (_) {}
}
// Patch default point/symbol size. G2's MaybeSize transform hardcodes 3 as
// the constant visual size for point marks when no explicit size encoding is
// provided. The theme point.*.r values above are fallbacks that MaybeSize
// shadows, so we must also replace MaybeSize in each mark's postInference.
const CustomMaybeSize = () => (I, mark) => {
if (mark.encode.size !== undefined) return [I, mark];
const value = [];
for (const i of I) value[i] = POINT_RADIUS;
return [
I,
{
...mark,
encode: {
...mark.encode,
size: { type: "column", value, visual: true },
},
},
];
};
CustomMaybeSize.props = {};
const lib = G2.corelib();
for (const key of ["mark.point", "mark.beeswarm"])
for (const [i, e] of (lib[key]?.props?.postInference ?? []).entries())
if (e.type === G2.MaybeSize)
lib[key].props.postInference[i] = { type: CustomMaybeSize };
// Reorder point shapes: solid shapes first, then hollow ones.
for (const key of ["mark.point", "mark.beeswarm"]) {
const mark = lib[key];
if (!mark?.props?.shape) continue;
mark.props.defaultShape = "point";
const newShape = Object.fromEntries(
Object.entries(mark.props.shape).sort(([a], [b]) => /^hollow/.test(a) - /^hollow/.test(b))
);
mark.props.shape = newShape;
for (const ch of mark.props.channels || [])
if (ch.name === "shape") ch.range = Object.keys(newShape);
}
// Scale hardcoded label font sizes in composite marks.
// These marks live in G2.graphlib(), not G2.corelib().
const graphLib = G2.graphlib?.() ?? {};
for (const key of ["mark.sankey", "mark.chord", "mark.treemap"]) {
const orig = graphLib[key];
if (typeof orig !== "function") continue;
const wrapped = function (...args) {
const result = orig.apply(this, args);
for (const m of Array.isArray(result) ? result : [result]) {
if (!Array.isArray(m?.labels)) continue;
for (const l of m.labels)
if (typeof l?.fontSize === "number") l.fontSize *= FONT_SCALE;
}
return result;
};
wrapped.props = orig.props;
try {
G2.register(key, wrapped);
} catch (_) {}
}
})();