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
Here this may help. It determines the width of one unit by asking the axes to scale various values into the chart's pixel space, and then uses those are multipliers of the width and height. This should also make the shapes get larger as you zoom in. Please note thought, that as you start to make the markers this large you run into a few limitations in the current version of the chart. Marker visibility is determined based on whether the center is in view, at present, so the larger the marker, the odder it looks if it vanishes due to its center moving out of view. Having a different visibility mode here would need to be a feature request, unfortunately. Here is the updated logic:
$(function () { 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 minRx; // var maxRx; // var minRy; // var maxRy; //var minRadius = 5; //var maxRadius = 20; var xDist = 0; var yDist = 0; 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); // } var x1 = $("#container").igDataChart("scaleValue", "xAxis", 0); var x2 = $("#container").igDataChart("scaleValue", "xAxis", 1); var y1 = $("#container").igDataChart("scaleValue", "yAxis", 0); var y2 = $("#container").igDataChart("scaleValue", "yAxis", 1); xDist = Math.abs(x1 - x2); yDist = Math.abs(y1 - y2); }, 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; width = item.value1 * xDist; height = item.value2 * yDist; 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 / 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 }, { 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" }); });
Thanks Graham, this is just what I was looking for ! The behaviour when ellipse centers move outside the chart boundaries is quite OK
I'm now left with a few remaining questions:
The data I'm charting is scientfic and covers many orders of magnitude. I was using logarithmic x and y axes ( isLogarithmic: true ). When I use logarithmic axes with the ellipticalMarker template, the scaling goes awry. Can this be corrected ?
I have the overview pane displayed. When I zoom into the chart, the ellipses in the overview don't seem to behave correctly. Can this be corrected ?
Is there any documentation covering adding new marker types ? I'd like to be able to stop bugging you !
Nic,
For isLogarithmic, this will get the ellipses to display again:
var x1 = $("#container").igDataChart("scaleValue", "xAxis", 1);
var x2 = $("#container").igDataChart("scaleValue", "xAxis", 2);
var y1 = $("#container").igDataChart("scaleValue", "yAxis", 1);
var y2 = $("#container").igDataChart("scaleValue", "yAxis", 2);
Scaling 0 into the logarithmic axis is undefined so we need to pick a different two values in order to measure. But I don't think this will actually give you your correct behavior. As you are deciding on a static multiplier for all the ellipse widths or heights.
But if you want the "width" or "height" to be directly meaningful in terms of the values on the underlying axis, then you will basically need to scale the values in the context of the position of each ellipse. And ellipse of width 8 would have different apparent visual widths depending on where on the scale it is plotted, correct? And wouldn't it have a different radius on one side vs the other? Making it more of a blob than an ellipse?
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 ?
Ah, passing posts - please ignore this