uPlot icon indicating copy to clipboard operation
uPlot copied to clipboard

Bubble Charts

Open cloudlena opened this issue 3 years ago • 3 comments

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
];

cloudlena avatar Oct 07 '22 21:10 cloudlena

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.

leeoniya avatar Oct 07 '22 21:10 leeoniya

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.

cloudlena avatar Oct 10 '22 20:10 cloudlena

@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?

cloudlena avatar Oct 16 '22 15:10 cloudlena