Special donut card with various rings / arcs for positive and negative values

I am trying to create a special kind of donut chart in D3 that will contain different rings for positive and negative values. Values โ€‹โ€‹can be greater than 100% or less than -100%, so there will be an arc representing the remaining value. Below is a sample image of a chart: enter image description here

The first value of the positive category (Category_1 - Gray) is 80, so 80% fill the circle with gray, leaving 20% โ€‹โ€‹for the next positive category. The next value of the positive category (Category_2 - Orange) is 160. Thus, 20% is first used to the left of category_1 (now the value 140 remains). Then it fills the next circle (up) 100% (now the value 40 remains), and for the remaining value (40) it creates a partial circle up.

Now we have category_3 (dark red) as negative (-120%), so when creating the inner circle and filling it with 100% (20 values โ€‹โ€‹left), then it creates an inner arc to stop the value (20). We have another negative category (Category_4 - red), so it will start from the place where the previous negative category (Category_3) ended and fill the area with 20%.

Editing 3: I created a very basic arc-based donut chart, and when the total value exceeds 100, I can create outer rings for the remaining values. Below is the JSFiddle link:

http://jsfiddle.net/rishabh1990/zmuqze80/

data = [20, 240]; var startAngle = 0; var previousData = 0; var exceedingData; var cumulativeData = 0; var remainder = 100; var innerRadius = 60; var outerRadius = 40; var filledFlag; var arc = d3.svg.arc() .innerRadius(innerRadius) .outerRadius(outerRadius) for (var i = 0; i < data.length; i++) { filledFlag = 0; exceedingData = 0; console.log("---------- Iteration: " + (i + 1) + "---------"); if (data[i] > remainder) { filledFlag = 1; exceedingData = data[i] - remainder; console.log("Exceeding: " + exceedingData); data[i] = data[i] - exceedingData; data.splice(i + 1, 0, exceedingData); } if( filledFlag === 1) { cumulativeData = 0; } else { cumulativeData += data[i]; } console.log("Previous: " + previousData); console.log("Data: " + data, "Current Data: " + data[i]); var endAngle = (previousData + (data[i] / 50)) * Math.PI; console.log("Start " + startAngle, "End " + endAngle); previousData = previousData + data[i] / 50; //if(i===1) endAngle = 1.4 * Math.PI; //if(i===2) endAngle = 2 * Math.PI; var vis = d3.select("#svg_donut"); arc.startAngle(startAngle).endAngle(endAngle); vis.append("path") .attr("d", arc) .attr("transform", "translate(200,200)") .style("fill", function(d) { if (i === 0) return "red"; //if (i === 1) return "green"; //if (i === 2) return "blue" //if (i === 3) return "orange" //if (i === 4) return "yellow"; }); if (exceedingData > 0) { console.log("Increasing Radius From " + outerRadius + " To " + (outerRadius + 40)); outerRadius = outerRadius + 22; innerRadius = innerRadius + 22; arc.innerRadius(innerRadius).outerRadius(outerRadius); console.log("Outer: ", outerRadius); } if (remainder === 100) { remainder = 100 - data[i]; } else { remainder = 100 - cumulativeData; }; if (filledFlag === 1) { remainder = 100; } console.log("Remainder: " + remainder); startAngle = endAngle; } 

Share some ideas for implementation.

+8
javascript html
source share
1 answer

Ok, it took a while, but it seems to work. First, let me determine that what you describe as a donut chart can also be displayed as a series of bars - using the same data. So I started from there and, in the end, processed it in a donut chart, but also left the bar implementation. Another thing is that the general solution should be able to wrap segments with any value, and not just 100, so I included a slider that allows you to change this packing value. Finally - and this is easier to explain in bars, and not in the implementation of a donut - instead of always having bars ending from left to right, for example text, it may be desirable to zigzag, i.e. Alternate the wrap from left to right, left, and so on. The effect is that when the sum is split into two segments on two separate lines, the zigzag approach will keep these two segments next to each other. I added a checkbox to enable / disable this zigzag behavior.

Here the working jsFiddle is another iteration .

Here are the important bits:

There is a function wrap(data, wrapLength) , which takes an array of data and a wrapLength , for which they should transfer these values. This function determines which data values โ€‹โ€‹should be divided into sub-segments and returns a new array of them, with each segment object having values x1 , x2 and y . x1 and x2 are the beginning and end of each bar, and y is the line string. In the donut table, these values โ€‹โ€‹are equivalent to the start angle ( x1 ), end angle ( x2 ) and radius ( y ) of each arc.

The wrap() function does not know how to consider negative vs positive values, so wrap() needs to be called twice - once with all the negatives, and then with all the positives. From there, some processing is applied selectively only to negatives, and then additional processing is applied to the combination of the two sets. The whole set of transformations described in the last 2 sections is fixed by the following fragment. I do not include the implementation of wrap() here, only the code that calls it; also not including the rendering code, which is pretty simple after creating segments .

 // Turn N data points into N + x segments, as dictated by wrapLength. Do this separately // for positive and negative values. They'll be merged further down, after we apply // a specific transformation to just the negatives var positiveSegments = wrap(data.filter(function(d) { return d.value > 0; }), wrapLength); var negativeSegments = wrap(data.filter(function(d) { return d.value < 0; }), wrapLength); // Flip and offset-by-one the y-value of every negative segment. Ie 0 becomes -1, 1 becomes -2 negativeSegments.forEach(function(segment) { segment.y = -(segment.y + 1); }); // Flip the order of the negative segments, so that their sorted from negative-most y-value and up negativeSegments.reverse() // Combine negative and positive segments segments = negativeSegments.concat(positiveSegments); if(zigzag) { segments.forEach(function(segment) { if(Math.abs(segment.y) % 2 == (segment.y < 0 ? 0 : 1)) { flipSegment(segment, wrapLength); } }); } // Offset the y of every segment (negative or positive) so that the minimum y is 0 // and goes up from there var maxNegativeY = negativeSegments[0].y * -1; segments.forEach(function(segment) { segment.y += maxNegativeY; }); 
+3
source share

All Articles