Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p>As <strong>Sebastian Bochan</strong> states in his answer it's much simpler if you just create the extra areas for the over / under shading directly from the data -- I don't think you really need to do the work you're doing to build your shaded areas. <em>However</em> that does't solve your data grouping problem. </p> <p>I can get close -- but not quite perfect. At first I thought that the issue was just using the same aggregate function on all the data sets, but that doesn't work as it goes wrong whenever it aggregates over a crossing point. </p> <p>What I did was create two sets of range data that go from A to B: </p> <pre><code>for (i = 0; i &lt; data1.length; i ++) { var valA = data1[i][1]; var valB = data2[i][2]; under[i] = [data1[i][0], valA, valB ]; over[i] = [data1[i][0], valB, valA ]; } </code></pre> <p>Then I gave each set of range data it's only aggregate function that changes the shape of the graph to only show the over/under as applicable. </p> <pre><code>function shadeApproxUnder(low,high) { low = average_(low); high = average_(high); if (low &gt; high) return [ high, high ]; return [ low, high ]; } function shadeApproxOver(low,high) { low = average_(low); high = average_(high); if (low &gt; high) return [ low, low ]; return [ low, high ]; } </code></pre> <p>and also one for the lines so that I know the aggregate values are the same: </p> <pre><code>function lineApprox(values) { var avg = average_(values); return avg; } </code></pre> <p>That gets close as you can see in this <a href="http://jsfiddle.net/SpaceDog/khF8n/" rel="nofollow">fiddle</a>, but you can see that it doesn't work perfectly. That's because there are points where the two lines cross where there's no data-point. Since I assume the system is drawing things in chunks between the two data points there's no way to get that to work. </p> <p>You can try returning <code>[ null, null ]</code> instead of <code>[ high, high ]</code> or <code>[ low, low ]</code> in the aggregate functions and see more clearly the gaps. </p> <p>It looks like you really need to somehow get the datapoints added at the crossover junctions, and I'm not sure how to do that. You can't just edit the data because the data grouping will change it -- but you can't do it in the aggregate function as you can't add datapoints. </p> <p>You could attempt to save all the aggregate data point values generated when you draw the lines, and use that to insert the area graph with the extra data points extrapolated -- but I'm not clear on how that'll work when you refresh the graph (I suspect it will not). </p> <p><strong>EDIT</strong> </p> <p>I had a bash at a better solution, but it still doesn't work -- maybe you or someone else can spot the flaw. </p> <p>Basically, as I said above, we want to add datapoints for the line intersections on our shading curve, this is my attempt at that -- done using the data from the chart stored in 'dataGrouping' which is the data <em>after</em> it's grouped: </p> <pre><code>function updateChart(chart, refresh) { var dir = 0; var over = Array(), under = Array(); var dataA = chart.series[2].groupedData; var dataB = chart.series[3].groupedData; j = 0; // Index into over/under for (i=0;i &lt; dataA.length; i++) { if (dataA[i].y &lt; dataB[i].y) { if (dir == -1) { over[j] = under[j] = getIntersection(dataA[i-1], dataA[i], dataB[i-i], dataB[i]); j++; } dir = 1; over[j] = [ dataA[i].x, dataA[i].y, dataB[i].y ]; under[j] = [ dataA[i].x, dataB[i].y, dataB[i].y ]; } else if (dataA[i].y == dataB[i].y) { dir = 0; over[j] = [ dataA[i].x, dataA[i].y, dataA[i].y ]; under[j] = [ dataA[i].x, dataA[i].y, dataA[i].y ]; } else { if (dir == 1) { over[j] = under[j] = getIntersection(dataA[i-1], dataA[i], dataB[i-i], dataB[i]); j++; } dir = -1; over[j] = [ dataA[i].x, dataB[i].y, dataB[i].y ]; under[j] = [ dataA[i].x, dataB[i].y, dataA[i].y ]; } j++; } chart.series[0].setData(over, refresh); chart.series[1].setData(under, refresh); } </code></pre> <p><a href="http://jsfiddle.net/SpaceDog/p7usV/" rel="nofollow">Fiddle</a>.</p> <p><strong>NOTE</strong> Sometimes this seems to get stuck in a loop, I'm not sure why. </p> <p>You can see the line intersect function in the fiddle, it's lifted from Wikipedia. There are two problems with this, first there's no sensible event to attach this function to -- in the fiddle it's attached to <code>load</code> and <code>redraw</code>. But you can't start a redraw from inside the <code>redraw</code> event (which makes sense) so the shading shown will always be one set of data behind the plot lines -- this is obvious if you switch the zoom settings for example. But, it should be reasonably easy to fix that if you're prepared to edit the Highchart source code and put a hook in that will allow you to attach the update function after the grouping but before final rendering. </p> <p>Why is the shading still not right? Either I've got the line-intersection logic wrong, or it's to do with accuracy when working with very small (accurate) numbers and very big ones. Or it's just a high chart limitation. </p> <p>The other option, which is probably not going to look right is to use the <code>redraw</code> and the <code>Renderer</code> class and just directly draw the shaded areas as an SVG path onto the graph. </p> <p>If I come up with anything else I'll edit again -- but I think I'm out of ideas for how to do this. </p> <p><strong>EDIT 2</strong></p> <p>OK, I <em>think</em> the 'correct' way to do this is probably to modify Highcharts itself and create a new chart type with it's own renderer (so it'd be an extended area chart that knew to change colors on the cross-over and draw the lines in different colors). I've no idea how easy that is. </p> <p>However, the hack to draw the SVG paths directly over the graph actually works OK and we already have all the logic from the other examples on this page, so an update chart function might look like: </p> <pre><code>var over_path = null; var under_path = null; function updateChart(chart) { // Create the paths on the first call ... if (!over_path) { over_path = chart.renderer.path(); over_path.attr({fill: '#bbeebb'}).add(); } if (!under_path) { under_path = chart.renderer.path(); under_path.attr({fill: '#eebbbb'}).add(); } // Get the data from the chart aPts = chart.series[0].points; bPts = chart.series[1].points; var i; var overPts = Array(); // SVG path instructions var underPts = Array(); var mX = chart.plotLeft; // Offset for each point var mY = chart.plotTop; var iPt; // For the intersection points // Draw the path along line B, than back along A/B // to complete a closed space. First go along B overPts[0] = underPts[0] = 'M'; // Move to start overPts[1] = underPts[1] = bPts[0].plotX + mX; overPts[2] = underPts[2] = bPts[0].plotY + mY; overPts[3] = underPts[3] = 'L'; // Draw from here for (i=1; i&lt;aPts.length; i++) { overPts[overPts.length] = underPts[underPts.length] = bPts[i].plotX + mX; overPts[overPts.length] = underPts[underPts.length] = bPts[i].plotY + mY; } // Now go backwards along A looking for the cross-overs ... var dir = 0; var newDir = 0; for (i=(aPts.length - 1); i&gt;=0; i--) { var A = aPts[i].plotY + mY; var B = bPts[i].plotY + mY; newDir = (A&gt;B)?1:((A&lt;B)?-1:0); if (dir &amp;&amp; (dir != newDir)) { // Change direction, add intersect point iPt = getIntersection(aPts[i], aPts[i+1], bPts[i], bPts[i+1]); overPts[overPts.length] = underPts[underPts.length] =iPt.plotX + mX; overPts[overPts.length] = underPts[underPts.length] = iPt.plotY + mY } dir = newDir; // Add matching data point overPts[overPts.length] = aPts[i].plotX + mX; underPts[underPts.length] = aPts[i].plotX + mX; if (A &gt; B) { overPts[overPts.length] = B; underPts[underPts.length] = A; } else { overPts[overPts.length] = A; underPts[underPts.length] = B; } } // Update new path over_path.attr({'d': overPts}); under_path.attr({'d': underPts}); } </code></pre> <p><a href="http://jsfiddle.net/SpaceDog/Twdft/" rel="nofollow">Fiddle</a></p> <p><em>Note:</em> Here I'm reading undocumented contents of the chart object to work out where the points are, since this isn't in the official API they may always change in future versions of Highcharts (although, they probably will not -- but it's good to be aware of the possibility). </p> <p>The main problem is the area appears before the initial graph draw animation, but you can probably find a way round that (you could fade it in the first time which might look cool). Since further redraws aren't animated it's not a problem. There's also some 'fuzziness' in some views which I think is due to anti-aliasing, so you might want to tweak that. </p> <p>Unless there's a different chart package that does data grouping that can take account of line intersections I think that the above is probably the best solution that doesn't involve delving into the source of a charting package.</p> <p>Anyway, thanks for the interesting problem to occupy my mind this week. :) </p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload