I'd like to create a scatter or bubble chart with elliptical markers, with the ellipse center and axes determined by data values.
Is there any simple approach I can take, perhaps using a custom template ?
Here's an example of one way you could implement an elliptical marker template:
var data = [{ x: 2, y: 5, value1: 3, value2: 8 }, { x: 3, y: 3, value1: 5, value2: 2 }, { x: 1, y: 2, value1: 1, value2: 1 }, { x: 3, y: 2, value1: 4, value2: 3 }, { x: 5, y: 1, value1: 2, value2: 4 }, { x: 4, y: 3, value1: 2, value2: 2 }]; var minRx; var maxRx; var minRy; var maxRy; var minRadius = 5; var maxRadius = 20; var ellipticalMarker = { passStarting: function (passInfo) { maxRx = Number.MIN_VALUE; minRx = Number.MAX_VALUE; maxRy = Number.MIN_VALUE; minRy = Number.MAX_VALUE; for (var i = 0; i < data.length; i++) { maxRx = Math.max(maxRx, data[i].value1); minRx = Math.min(minRx, data[i].value1); maxRy = Math.max(maxRy, data[i].value2); minRy = Math.min(minRy, data[i].value2); } }, measure: function (measureInfo) { var item = measureInfo.data.item(), width = ((item.value1 - minRx) / (maxRx - minRx)) * (maxRadius - minRadius) + minRadius, height = ((item.value2 - minRy) / (maxRy - minRy)) * (maxRadius - minRadius) + minRadius; measureInfo.width = width; measureInfo.height = height; }, render: function (renderInfo) { var ctx = renderInfo.context, x = renderInfo.xPosition, y = renderInfo.yPosition, item = renderInfo.data.item(), width = renderInfo.availableWidth, height = renderInfo.availableHeight; ctx.fillStyle = renderInfo.data.actualItemBrush().fill(); ctx.strokeStyle = renderInfo.data.outline().fill(); ctx.lineWidth = 1.0 / Math.max(width, height); ctx.save(); ctx.translate(x, y); ctx.scale(width, height); ctx.translate(-x, -y); ctx.beginPath(); ctx.arc(x, y, 1, 0, 2.0 * Math.PI, false); ctx.fill(); ctx.stroke(); ctx.restore(); } }; $("#container").igDataChart({ width: "500px", height: "500px", dataSource: data, axes: [{ name: "xAxis", type: "numericX", minimumValue: 0, maximumValue: 8 }, { name: "yAxis", type: "numericY", minimumValue: 0, maximumValue: 8 }], series: [{ name: "series1", title: "Test Series", type: "scatter", xAxis: "xAxis", yAxis: "yAxis", xMemberPath: "x", yMemberPath: "y", markerTemplate: ellipticalMarker, showTooltip: true, tooltipTemplate: "tooltipTemplate" }], horizontalZoomable: true, verticalZoomable: true, windowResponse: "immediate", overviewPlusDetailPaneVisibility: "visible" }); });
Hope this helps! -Graham
Thanks Graham - this is almost what I need, and illustrates the fantastic flexibility of igDataChart !
However I'm getting a little lost in the scaling that's going on. For instance, the code sample above produces a marker at (2,5) with pixel radii independent of chart zoom. I would like this to be rendered as an ellipse extending from -1 to 5 on the x-axis (2-3 to 2+3) and -3 to 13 on the y-axis (5-8 to 5+8)
Can this be achieved with a simple modification ?
Hi,
So you would like the value1 and value2 to directly map to the width and height? I was basically scaling them based on the range of values that appeared in the data source. But if you want them to directly map to the width and height range we can do that too, and I'm guessing you'd want the size to adjust as you zoom in and out too. I'll see if I can adjust the sample to do that.
-Graham
Thanks Graham - that works. Many thanks for your excellent support
Ah, passing posts - please ignore this
Yes, in the case of logarithmic axes the apparent width will be dependent on position, and yes an ellipse (x - x0)^2/X^2 + (y - y0)^2/Y^2 = 1 will not be elliptical when plotted on logarithmic axes, but use of an ellipse is conventional and good enough !
I imagine that I'd need to calculate the scale factor for each point in measure ? In the call:
var x1 = $("#chart").igDataChart("scaleValue", "xAxis", 0);
is the coordinate in data space or in pixel space ? If the fomer I could presumably write within measure
var x1 = $("#chart").igDataChart("scaleValue", "xAxis", item.x - 0.5);var x2 = $("#chart").igDataChart("scaleValue", "xAxis", item.x + 0.5);var y1 = $("#chart").igDataChart("scaleValue", "yAxis", item.y - 0.5);var y2 = $("#chart").igDataChart("scaleValue", "yAxis", item.y + 0.5);
xDist = Math.abs(x1 - x2);yDist = Math.abs(y1 - y2);
and proceed as before ?
Here's an example of how you might try to correct the issues I describe above:
var data = [{ x: 2, y: 5, value1: 1.3, value2: .8 }, { x: 3, y: 3, value1: 1.5, value2: .8 }, { x: 1, y: 2, value1: 2, value2: 1 }, { x: 3, y: 2, value1: 1, value2: 1 }, { x: 5, y: 1, value1: 1.0, value2: 1.4 }, { x: 4, y: 3, value1: .2, value2: 1.2 }]; var xDist = 0; var yDist = 0; var chartWidget = $("#container"); var ellipticalMarker = { passStarting: function (passInfo) { }, measure: function (measureInfo) { //can't measure in this case, measure doesn't give you x,y position yet. }, render: function (renderInfo) { var ctx = renderInfo.context, x = renderInfo.xPosition, y = renderInfo.yPosition, item = renderInfo.data.item(), width = item.value1, height = item.value2, ux, uy; //get the axis values that equate to where this marker is being rendered. ux = chartWidget.igDataChart("unscaleValue", "xAxis", x); uy = chartWidget.igDataChart("unscaleValue", "yAxis", y); //see how wide the marker is in terms of pixels based on the item values being used as radius. width = (chartWidget.igDataChart("scaleValue", "xAxis", ux + width / 2.0) - x) * 2.0; height = (chartWidget.igDataChart("scaleValue", "yAxis", uy + height / 2.0) - y) * 2.0; //since we didn't measure, must update with how much size we actually used. renderInfo.availableWidth = width; renderInfo.availableHeight = height; //the chart is telling use what colors to use here ctx.fillStyle = renderInfo.data.actualItemBrush().fill(); ctx.strokeStyle = renderInfo.data.outline().fill(); ctx.lineWidth = 1.0 / Math.max(width, height); ctx.save(); //html5 canvas does support non circular arcs, so we either need to use a transform or rasterize it ourselves. ctx.translate(x, y); ctx.scale(width / 2.0, height / 2.0); ctx.translate(-x, -y); ctx.beginPath(); ctx.arc(x, y, 1, 0, 2.0 * Math.PI, false); ctx.fill(); ctx.stroke(); ctx.restore(); } }; $("#container").igDataChart({ width: "500px", height: "500px", dataSource: data, axes: [{ name: "xAxis", type: "numericX", minimumValue: 0, maximumValue: 8, isLogarithmic: true }, { name: "yAxis", type: "numericY", minimumValue: 0, maximumValue: 8, isLogarithmic: true }], series: [{ name: "series1", title: "Test Series", type: "scatter", xAxis: "xAxis", yAxis: "yAxis", xMemberPath: "x", yMemberPath: "y", markerTemplate: ellipticalMarker, showTooltip: true, tooltipTemplate: "tooltipTemplate" }], horizontalZoomable: true, verticalZoomable: true, windowResponse: "immediate" });
Note, though, that it is still drawing ellipses, and an ellipse that had radii that are defined in terms of the underlying scale would actually have a different radius in each direction, as far as I can tell. So this is not necessarily 100% correct. This leaves the issue of the OPD. Its a bit of an open issue at the moment that markers render at the same size in the OPD as in the normal chart. And is something we will be looking at adjusting in the future. There are some ways to work around this though, as the passStarting method will be notified of the size of the area the markers are being rendered in. But compensating for this gets a bit complex. The bubble series actually has a targetted correction here were it will scale the size of the markers being rendered in the OPD, but I'm not sure if the code we have above will play nicely with the bubble series. As the bubble series only has the one radius mapping it expects. -Graham
In addition, there is actually a completely seperate render phase that is used solely to generate a hit buffer, to aid in tooltips and mouse events over the markers. You are notified when this is occuring via the isHitTestRender property. When that is true, you can render some more simplified bounding box if you want, but you MUST use the color passed in by chart, as it helps the chart identify when the mouse is over the series.