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; });