Bubble Charts
I would like to create a bubble chart, but somehow cannot follow the complex example at https://leeoniya.github.io/uPlot/demos/scatter.html. The example seems overly complicated and not well documented. Is there a simpler example, or would it even make sense to expose the point size as a simple function for Series.Points.size? That would make bubble charts much simpler to create.
For reference, my example uses the following data structure:
const data = [
[1,2,3,4,5], // x-Axis
[3,4,5,6,7], // y-Axis
[2,3,2,6,4], // Bubble radius
];
if you don't need hover support, or multiple bubble series, or optimizations that most efficiently handle thousands of bubbles, i can make a much simplified static renderer. each of those requirements complicates the existing demo.
Thanks for the quick reply, @leeoniya! I do need hover-support, have one bubble series and need to be able to handle hundreds but not thousands of bubbles.
@leeoniya, I created a custom paths function:
interface BubblePathBuilderOptions {
sizeSeriesIndex: number;
maxSize: number;
}
function bubbles(opts: BubblePathBuilderOptions): Series.PathBuilder {
const deg360 = 2 * Math.PI;
return (u, seriesIdx) => {
uPlot.orient(
u,
seriesIdx,
(series, dataX, dataY, scaleX, scaleY, valToPosX, valToPosY, xOff, yOff, xDim, yDim) => {
const dataS = u.data[opts.sizeSeriesIndex] as number[];
const maxS = dataS.reduce((p, d) => (d ? Math.max(d, p) : p), 0);
u.ctx.save();
if (typeof series.stroke === 'function') {
u.ctx.strokeStyle = series.stroke(u, seriesIdx);
}
if (typeof series.fill === 'function') {
u.ctx.fillStyle = series.fill(u, seriesIdx);
}
u.ctx.lineWidth = series.width || 0;
dataX.forEach((d, i) => {
const xVal = d;
const yVal = dataY[i];
const sVal = dataS[i];
if (
yVal !== undefined &&
yVal !== null &&
(scaleX.min === undefined || xVal >= scaleX.min) &&
(scaleX.max === undefined || xVal <= scaleX.max) &&
(scaleY.min === undefined || yVal >= scaleY.min) &&
(scaleY.max === undefined || yVal <= scaleY.max)
) {
const xPos = valToPosX(xVal, scaleX, xDim, xOff);
const yPos = valToPosY(yVal, scaleY, yDim, yOff);
const sizeRatio = sVal ? sVal / maxS : 0;
const size = opts.maxSize * sizeRatio * devicePixelRatio + 9;
u.ctx.moveTo(xPos + size / 2, yPos);
u.ctx.beginPath();
u.ctx.arc(xPos, yPos, size / 2, 0, deg360);
u.ctx.fill();
u.ctx.stroke();
}
});
u.ctx.restore();
}
);
return null;
};
}
Would it make sense to include something like that in the library as a new member of PathBuilderFactories? Or do you see any major flaws or ways to improve it?