zz-chart
Version:
Alauda Chart components by Alauda Frontend Team
362 lines • 14.1 kB
JavaScript
import uPlot from 'uplot';
import { pointWithin, Quadtree } from '../../strategy/quadtree.js';
const SPACE_BETWEEN = 1;
const SPACE_AROUND = 2;
const SPACE_EVENLY = 3;
function roundDec(val, dec) {
return Math.round(val * (dec = 10 ** dec)) / dec;
}
const coord = (i, offs, iwid, gap) => roundDec(offs + i * (iwid + gap), 6);
// eslint-disable-next-line sonarjs/cognitive-complexity
export function distr(numItems, sizeFactor, justify, onlyIdx, each) {
const space = 1 - sizeFactor;
let gap = justify === SPACE_BETWEEN
? space / (numItems - 1)
: justify === SPACE_AROUND
? space / numItems
: justify === SPACE_EVENLY
? space / (numItems + 1)
: 0;
if (isNaN(gap) || gap === Infinity)
gap = 0;
const offs = justify === SPACE_BETWEEN
? 0
: justify === SPACE_AROUND
? gap / 2
: justify === SPACE_EVENLY
? gap
: 0;
const iwid = sizeFactor / numItems;
const _iwid = roundDec(iwid, 6);
if (onlyIdx == null) {
for (let i = 0; i < numItems; i++)
each(i, coord(i, offs, iwid, gap), _iwid);
}
else
each(onlyIdx, coord(onlyIdx, offs, iwid, gap), _iwid);
}
export function seriesBarsPlugin(opts) {
let pxRatio;
// let font: string;
const { time, ignore = [], radius: _radius, ori: _ori, dir: _dir, stacked: _stacked, marginRatio, disp, } = opts;
const radius = _radius ?? 0;
// function setPxRatio() {
// pxRatio = devicePixelRatio;
// font = Math.round(10 * pxRatio) + 'px Arial';
// }
// setPxRatio();
// window.addEventListener('dppxchange', setPxRatio);
const ori = _ori;
const dir = _dir;
const stacked = _stacked;
const groupWidth = 0.9;
const groupDistr = SPACE_BETWEEN;
const barWidth = 1 - (marginRatio || 0);
const barDistr = SPACE_BETWEEN;
function distrTwo(groupCount, barCount, _groupWidth = groupWidth) {
const out = Array.from({ length: barCount }, () => ({
offs: Array.from({ length: groupCount }).fill(0),
size: Array.from({ length: groupCount }).fill(0),
}));
distr(groupCount, _groupWidth, groupDistr, null, (groupIdx, groupOffPct, groupDimPct) => {
distr(barCount, barWidth, barDistr, null, (barIdx, barOffPct, barDimPct) => {
out[barIdx].offs[groupIdx] = groupOffPct + groupDimPct * barOffPct;
out[barIdx].size[groupIdx] = groupDimPct * barDimPct;
});
});
return out;
}
function distrOne(groupCount, barCount) {
const out = Array.from({ length: barCount }, () => ({
offs: Array.from({ length: groupCount }).fill(0),
size: Array.from({ length: groupCount }).fill(0),
}));
distr(groupCount, groupWidth, groupDistr, null, (groupIdx, groupOffPct, groupDimPct) => {
distr(barCount, barWidth, barDistr, null, (barIdx, _barOffPct, _barDimPct) => {
out[barIdx].offs[groupIdx] = groupOffPct;
out[barIdx].size[groupIdx] = groupDimPct;
});
});
return out;
}
let barsPctLayout;
// eslint-disable-next-line sonarjs/no-unused-collection
let barsColors;
let qt;
const barsBuilder = uPlot.paths.bars({
radius,
disp: {
x0: {
unit: 2,
// discr: false, (unary, discrete, continuous)
values: (_u, seriesIdx, _idx0, _idx1) => barsPctLayout[seriesIdx].offs,
},
size: {
unit: 2,
// discr: true,
values: (_u, seriesIdx, _idx0, _idx1) => barsPctLayout[seriesIdx].size,
},
...disp,
/*
// e.g. variable size via scale (will compute offsets from known values)
x1: {
units: 1,
values: (u, seriesIdx, idx0, idx1) => bucketEnds[idx],
},
*/
},
each: (u, seriesIdx, dataIdx, lft, top, wid, hgt) => {
// we get back raw canvas coords (included axes & padding). translate to the plotting area origin
lft -= u.bbox.left;
top -= u.bbox.top;
qt.add({
x: lft,
y: top,
w: wid,
h: hgt,
sidx: seriesIdx,
didx: dataIdx,
});
},
});
// function drawPoints(u: uPlot, sidx: number, i0: number, i1: number) {
// u.ctx.save();
// u.ctx.font = font;
// u.ctx.fillStyle = 'black';
// uPlot.orient(
// u,
// sidx,
// (
// _series,
// _dataX,
// dataY,
// _scaleX,
// scaleY,
// _valToPosX,
// valToPosY,
// xOff,
// yOff,
// xDim,
// yDim,
// _moveTo,
// _lineTo,
// _rect,
// ) => {
// const _dir = dir * (ori == 0 ? 1 : -1);
// const wid = Math.round(barsPctLayout[sidx].size[0] * xDim);
// barsPctLayout[sidx].offs.forEach((offs: number, ix: number) => {
// if (dataY[ix] != null) {
// let x0 = xDim * offs;
// let lft = Math.round(xOff + (_dir == 1 ? x0 : xDim - x0 - wid));
// let barWid = Math.round(wid);
// let yPos = valToPosY(dataY[ix], scaleY, yDim, yOff);
// let x = ori == 0 ? Math.round(lft + barWid / 2) : Math.round(yPos);
// let y = ori == 0 ? Math.round(yPos) : Math.round(lft + barWid / 2);
// u.ctx.textAlign =
// ori == 0 ? 'center' : dataY[ix] >= 0 ? 'left' : 'right';
// u.ctx.textBaseline =
// ori == 1 ? 'middle' : dataY[ix] >= 0 ? 'bottom' : 'top';
// u.ctx.fillText(String(dataY[ix]), x, y);
// }
// });
// },
// );
// u.ctx.restore();
// }
function range(_u, _dataMin, dataMax) {
const [min, max] = uPlot.rangeNum(0, dataMax, 0.05, true);
return [min || 0, max];
}
return {
hooks: {
drawClear: (u) => {
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height);
qt.clear();
// force-clear the path cache to cause drawBars() to rebuild new quadtree
u.series.forEach((s) => {
s._paths = null;
});
if (stacked)
barsPctLayout = [null].concat(distrOne(u.data.length - 1 - ignore.length, u.data[0].length));
else if (u.series.length === 2)
barsPctLayout = [null].concat(distrOne(u.data[0].length, 1));
else
barsPctLayout = [null].concat(distrTwo(u.data[0].length, u.data.length - 1 - ignore.length, u.data[0].length === 1 ? 1 : groupWidth));
// TODOL only do on setData, not every redraw
const { disp } = opts;
if (disp?.fill != null) {
barsColors = [null];
for (let i = 1; i < u.data.length; i++) {
barsColors.push({
fill: disp.fill.values(u, i),
stroke: disp.stroke.values(u, i),
});
}
}
},
},
// eslint-disable-next-line sonarjs/cognitive-complexity
opts: (_u, opts) => {
const { axes, series } = opts;
const yScaleOpts = {
range,
ori: ori === 0 ? 1 : 0,
};
// hovered
let hRect;
uPlot.assign(opts, {
select: { show: false },
cursor: {
x: false,
y: false,
dataIdx: (u, seriesIdx) => {
if (seriesIdx === 1) {
hRect = null;
const cx = u.cursor.left * pxRatio;
const cy = u.cursor.top * pxRatio;
qt.getQ(cx, cy, 1, 1, o => {
if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h))
hRect = o;
});
}
return hRect && seriesIdx === hRect.sidx ? hRect.didx : null;
},
points: {
show: false,
// fill: 'rgba(255,255,255, 0.3)',
// bbox: (u: uPlot, seriesIdx: number) => {
// let isHovered = hRect && seriesIdx == hRect.sidx;
// return {
// left: isHovered ? hRect.x / pxRatio : -10,
// top: isHovered ? hRect.y / pxRatio : -10,
// width: isHovered ? hRect.w / pxRatio : 0,
// height: isHovered ? hRect.h / pxRatio : 0,
// };
// },
},
},
scales: {
x: {
distr: 2,
ori,
dir,
// auto: true,
range: (u) => {
let min = 0;
let max = Math.max(1, u.data[0].length - 1);
let pctOffset = 0;
distr(u.data[0].length, groupWidth, groupDistr, 0, (_di, lftPct, widPct) => {
pctOffset = lftPct + widPct / 2;
});
const rn = max - min;
if (pctOffset === 0.5)
min -= rn;
else {
const upScale = 1 / (1 - pctOffset * 2);
const offset = (upScale * rn - rn) / 2;
min -= offset;
max += offset;
}
return [min, max];
},
},
rend: yScaleOpts,
size: yScaleOpts,
mem: yScaleOpts,
inter: yScaleOpts,
toggle: yScaleOpts,
},
});
if (ori === 1) {
opts.padding = [0, null, 0, null];
}
const { xSplits } = getBarConfig({
dir,
ori,
xSpacing: dir === 1 ? 100 : -100,
});
const values = time === false ? { values: (u) => u.data[0] } : {};
uPlot.assign(axes[0], {
// splits: (u: any, _axisIdx: number) => {
// const _dir = dir * (ori === 0 ? 1 : -1);
// // TODO????
// const splits = u._data[0].slice();
// return _dir === 1 ? splits : splits.reverse();
// },
splits: xSplits,
// 设置 x 坐标展示
...values,
gap: 15,
size: ori === 0 ? 40 : 150,
labelSize: 20,
grid: { show: false },
ticks: { show: false },
side: ori === 0 ? 2 : 3,
});
series.forEach((s, i) => {
if (i > 0 && !ignore.includes(i)) {
uPlot.assign(s, {
// pxAlign: false,
// stroke: "rgba(255,0,0,0.5)",
paths: barsBuilder,
points: {
// show: drawPoints,
show: false,
},
});
}
});
},
};
}
/**
* @internal
*/
export function getBarConfig(opts) {
const { ori = 0, dir = 1, xSpacing = 0 } = opts;
const isXHorizontal = ori === 0;
const xSplits = (u) => {
const dim = isXHorizontal ? u.bbox.width : u.bbox.height;
const _dir = dir * (isXHorizontal ? 1 : -1);
const dataLen = u.data[0].length;
const lastIdx = dataLen - 1;
let skipMod = 0;
if (xSpacing !== 0) {
const cssDim = dim / devicePixelRatio;
// let maxTicks = Math.abs(Math.floor(cssDim / xSpacing));
const maxTicks = Math.abs(Math.floor(cssDim / (isXHorizontal ? xSpacing : xSpacing / 3)));
skipMod = dataLen < maxTicks ? 0 : Math.ceil(dataLen / maxTicks);
}
const splits = [];
// for distr: 2 scales, the splits array should contain indices into data[0] rather than values
u.data[0].forEach((_v, i) => {
const shouldSkip = skipMod !== 0 && (xSpacing > 0 ? i : lastIdx - i) % skipMod > 0;
if (!shouldSkip) {
splits.push(i);
}
});
return _dir === 1 ? splits : splits.reverse();
};
return { xSplits };
}
export function stack(data, omit = () => false) {
const data2 = [];
let bands = [];
const d0Len = data[0].length;
const accuse = Array.from({ length: d0Len });
for (let i = 0; i < d0Len; i++)
accuse[i] = 0;
for (let i = 1; i < data.length; i++)
data2.push(omit(i) ? data[i] : data[i].map((v, i) => (accuse[i] += +v)));
for (let i = 1; i < data.length; i++)
!omit(i) &&
bands.push({
series: [data.findIndex((_s, j) => j > i && !omit(j)), i],
});
bands = bands.filter(b => b.series[1] > -1);
return {
data: [data[0]].concat(data2),
bands,
};
}
//# sourceMappingURL=grouped-bars.js.map