Last active
November 11, 2020 23:18
-
-
Save uhop/b3c363e1af032f12be6f9d8267d1abb5 to your computer and use it in GitHub Desktop.
All necessary Pie chart calculations based on dojox.charting.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var TWO_PI = 2 * Math.PI; | |
function tmpl (template, dict) { | |
return template.replace(/\$\{([^\}]*)\}/g, function (_, name) { | |
return dict[name]; | |
}); | |
} | |
function makeSegment (args, options) { | |
// args is {startAngle, angle, index, className} | |
// optional index points to a data point | |
// optional className is a CSS class | |
// default startAngle=0 (in radians) | |
// default angle=2*PI (in radians) | |
// options is {center, innerRadius, radius, gap, precision, document} | |
// default center is {x=0, y=0} | |
// default innerRadius=0 | |
// default radius=100 | |
// default gap=0 (gap between segments in pixels) | |
// default precision=6 (digits after decimal point) | |
// default document is document | |
var node = (options.document || document).createElementNS('http://www.w3.org/2000/svg', 'path'), path, | |
center = options.center || {x: 0, y: 0}, | |
innerRadius = Math.max(options.innerRadius || 0, 0), | |
radius = Math.max(options.radius || 100, innerRadius), | |
gap = Math.max(options.gap || 0, 0), | |
precision = options.precision || 6, | |
angle = typeof args.angle != 'number' || args.angle >= TWO_PI ? TWO_PI : args.angle, | |
startAngle = args.startAngle || 0; | |
var innerGapAngle = gap / innerRadius / 2, | |
gapAngle = gap / radius / 2, | |
cx = center.x, cy = center.y, | |
data = { | |
cx: cx.toFixed(precision), | |
cy: cy.toFixed(precision), | |
r: radius.toFixed(precision) | |
}; | |
if (angle >= TWO_PI) { | |
data.tr = (2 * radius).toFixed(precision); | |
// generate a circle | |
if (innerRadius <= 0) { | |
// a circle | |
path = tmpl('M${cx} ${cy}m -${r} 0a${r} ${r} 0 1 0 ${tr} 0a${r} ${r} 0 1 0 -${tr} 0z', data); | |
} else { | |
data.r0 = innerRadius.toFixed(precision); | |
data.tr0 = (2 * innerRadius).toFixed(precision); | |
// a donut | |
path = tmpl('M${cx} ${cy}m -${r} 0a${r} ${r} 0 1 0 ${tr} 0a${r} ${r} 0 1 0 -${tr} 0zM${cx} ${cy}m -${r0} 0a${r0} ${r0} 0 1 1 ${tr0} 0a${r0} ${r0} 0 1 1 -${tr0} 0z', data); | |
} | |
} else { | |
var endAngle = startAngle + angle, start = startAngle + gapAngle, finish = endAngle - gapAngle; | |
if (finish < start) { | |
start = finish = startAngle + angle / 2; | |
} | |
data.lg = angle > Math.PI ? 1 : 0; | |
data.x1 = (radius * Math.cos(start) + cx).toFixed(precision); | |
data.y1 = (radius * Math.sin(start) + cy).toFixed(precision); | |
data.x2 = (radius * Math.cos(finish) + cx).toFixed(precision); | |
data.y2 = (radius * Math.sin(finish) + cy).toFixed(precision); | |
if (innerRadius <= 0) { | |
// a pie slice | |
path = tmpl('M${cx} ${cy}L${x1} ${y1}A${r} ${r} 0 ${lg} 1 ${x2} ${y2}L${cx} ${cy}z', data); | |
} else { | |
start = startAngle + innerGapAngle; | |
finish = endAngle - innerGapAngle; | |
if (finish < start) { | |
start = finish = startAngle + angle / 2; | |
} | |
data.r0 = innerRadius.toFixed(precision); | |
data.x3 = (innerRadius * Math.cos(finish) + cx).toFixed(precision); | |
data.y3 = (innerRadius * Math.sin(finish) + cy).toFixed(precision); | |
data.x4 = (innerRadius * Math.cos(start) + cx).toFixed(precision); | |
data.y4 = (innerRadius * Math.sin(start) + cy).toFixed(precision); | |
// a segment | |
path = tmpl('M${x1} ${y1}A${r} ${r} 0 ${lg} 1 ${x2} ${y2}L${x3} ${y3}A${r0} ${r0} 0 ${lg} 0 ${x4} ${y4}L${x1} ${y1}z', data); | |
} | |
} | |
node.setAttribute('d', path); | |
if ('index' in args) { | |
node.setAttribute('data-index', args.index); | |
} | |
if (args.className) { | |
node.setAttribute('class', args.className); | |
} | |
return node; | |
} | |
function processPieRun (data, options) { | |
// data is [datum, datum...] | |
// datum is {value, className, skip, hide} | |
// value is a positive number | |
// className is an optional CSS class name | |
// skip is a flag (default: false) to skip this segment completely | |
// hide is a flag (default: false) to suppress rendering | |
// options is {center, innerRadius, radius, startAngle, minSizeInPx, skipIfLessInPx, emptyClass, precision} | |
// default center is {x=0, y=0} | |
// default innerRadius=0 | |
// default radius=100 | |
// default startAngle=0 (in radians) | |
// default gap=0 (gap between segments in pixels) | |
// default precision=6 (digits after decimal point) | |
// minSizeInPx is to make non-empty segments at least this big (default: 0). | |
// skipIfLessInPx is a threshold (default: 0), when to skip too small segments. | |
// emptyClass is a CSS class name for an empty run | |
var radius = Math.max(options.radius || 100, options.innerRadius || 0, 0), | |
gap = Math.max(options.gap || 0, 0), | |
minSizeInPx = Math.max(options.minSizeInPx || 0, 0), | |
skipIfLessInPx = Math.max(options.skipIfLessInPx || 0, gap), | |
runOptions = { | |
center: options.center, | |
innerRadius: options.innerRadius, | |
radius: radius, | |
gap: gap, | |
precision: options.precision, | |
document: options.document | |
}; | |
// sanitize data | |
data.forEach(function (datum, index) { | |
if (!datum.skip) { | |
if (isNaN(datum.value) || datum.value === null || datum.value <= 0) { | |
datum.skip = true; | |
} | |
} | |
datum.index = index; | |
}); | |
var total = data.reduce(function (acc, datum) { | |
return datum.skip ? acc : acc + datum.value; | |
}, 0), node; | |
if (total <= 0) { | |
// empty run | |
node = makeSegment({ | |
index: -1, // to denote that it is not an actionable node | |
className: options.emptyClass | |
}, runOptions); | |
return [node]; | |
} | |
var nonEmptyDatumNumber = data.reduce(function (acc, datum) { | |
return datum.skip ? acc : acc + 1; | |
}, 0); | |
if (nonEmptyDatumNumber === 1) { | |
data.some(function (datum) { | |
if (datum.skip) { | |
return false; | |
} | |
node = makeSegment({ | |
index: datum.index, | |
className: datum.className | |
}, runOptions); | |
return true; | |
}); | |
return [node]; | |
} | |
// find too small segments | |
var sizes = data.map(function (datum) { | |
var angle = 0; | |
if (!datum.skip) { | |
angle = datum.value / total * TWO_PI; | |
} | |
return {angle: angle, index: datum.index}; | |
}); | |
var minAngle, newTotal, changeRatio; | |
if (minSizeInPx > 0) { | |
// adjust angles | |
minAngle = (minSizeInPx + gap) / radius; | |
sizes.forEach(function (size, index) { | |
var datum = data[index]; | |
if (!datum.skip) { | |
if (!datum.hide && size.angle < minAngle) { | |
size.angle = minAngle; | |
} | |
} | |
}); | |
newTotal = sizes.reduce(function (acc, size) { | |
return acc + size.angle; | |
}, 0); | |
var excess = newTotal - total, | |
totalForLargeAngles = sizes.reduce(function (acc, size) { | |
return size.angle <= minAngle ? acc : acc + size.angle; | |
}, 0); | |
changeRatio = (totalForLargeAngles - excess) / totalForLargeAngles; | |
sizes.forEach(function (size) { | |
if (size.angle > minAngle) { | |
size.angle *= changeRatio; | |
} | |
}); | |
} else if (skipIfLessInPx > 0) { | |
// suppress angles | |
minAngle = skipIfLessInPx / radius; | |
sizes.forEach(function (size, index) { | |
var datum = data[index]; | |
if (!datum.skip) { | |
if (!datum.hide && size.angle < minAngle) { | |
size.angle = 0; | |
} | |
} | |
}); | |
newTotal = sizes.reduce(function (acc, size) { | |
return acc + size.angle; | |
}, 0); | |
changeRatio = TWO_PI / newTotal; | |
sizes.forEach(function (size) { | |
if (size.angle > 0) { | |
size.angle *= changeRatio; | |
} | |
}); | |
} | |
// generate shape objects | |
var startAngle = options.startAngle || 0, shapes = []; | |
data.forEach(function (datum, index) { | |
if (!datum.skip) { | |
var angle = sizes[index].angle; | |
if (!datum.hide) { | |
shapes.push({ | |
index: index, | |
startAngle: startAngle, | |
angle: angle, | |
className: datum.className | |
}); | |
} | |
startAngle += angle; | |
} | |
}); | |
return shapes.map(function (shape) { | |
return makeSegment(shape, runOptions); | |
}); | |
} | |
function addShapes (parent) { | |
return function (node) { | |
parent.appendChild(node); | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<title>Geometry playground</title> | |
<script src="./geom.js"></script> | |
<script> | |
function addSegment (surface, data, options) { | |
var path = makeSegment(data, options); | |
if (typeof surface == 'string') { | |
surface = document.getElementById(surface); | |
} | |
surface.appendChild(path); | |
} | |
function makeTestSegments () { | |
addSegment('surface1', {}, {center: {x: 125, y: 125}}); | |
addSegment('surface2', {}, {center: {x: 125, y: 125}, innerRadius: 50}); | |
addSegment('surface3', {angle: Math.PI / 4}, {center: {x: 125, y: 125}}); | |
addSegment('surface4', {angle: Math.PI / 4}, {center: {x: 125, y: 125}, innerRadius: 50}); | |
addSegment('surface5', {angle: Math.PI / 4 * 7, startAngle: Math.PI / 4}, {center: {x: 125, y: 125}}); | |
addSegment('surface6', {angle: Math.PI / 4 * 7, startAngle: Math.PI / 4}, {center: {x: 125, y: 125}, innerRadius: 50}); | |
} | |
function makeTestDonut () { | |
var runOptions = { | |
center: {x: 250, y: 250}, | |
gap: 4, | |
innerRadius: 20, | |
radius: 70, | |
startAngle: Math.PI / 4 | |
}; | |
processPieRun([{value: 3, className: 'data0'}, {value: 4, className: 'data1'}, {value: 5, className: 'data2'}], runOptions). | |
map(addShapes(document.getElementById('donut'))); | |
runOptions.innerRadius = 80; | |
runOptions.radius = 130; | |
processPieRun([{value: 1, className: 'data3'}, {value: 1, className: 'data4'}, {value: 1, className: 'data5'}, {value: 1, className: 'data6'}], runOptions). | |
map(addShapes(document.getElementById('donut'))); | |
runOptions.innerRadius = 140; | |
runOptions.radius = 190; | |
processPieRun([{value: 1, className: 'data7'}], runOptions). | |
map(addShapes(document.getElementById('donut'))); | |
} | |
function start () { | |
makeTestSegments(); | |
makeTestDonut(); | |
} | |
</script> | |
<style> | |
.sample svg, .donut svg { | |
border: 1px solid black; | |
} | |
.sample path { | |
stroke: black; | |
/*fill: red;*/ | |
fill: url(#global-gradient); | |
} | |
.initial-color { stop-color: red; stop-opacity: 1; } | |
.final-color { stop-color: red; stop-opacity: 0.6; } | |
.donut path { | |
transform-origin: 0 0; | |
transform: matrix(1, 0, 0, 1, 0, 0); | |
opacity: 1; | |
transition: transform 0.2s linear, opacity 0.2s linear; | |
stroke: #222; /*white;*/ | |
stroke-width: 1px; /*3px;*/ | |
} | |
.donut path:hover { | |
/*transform: translate(-250px, -250px) scale(1.05) translate(237px, 237px);*/ | |
transform: matrix(1.05, 0, 0, 1.05, -12.5, -12.5); | |
opacity: 0.7; | |
z-index: 10; | |
} | |
.data0 {fill: url(#data0);} | |
.data1 {fill: url(#data1);} | |
.data2 {fill: url(#data2);} | |
.data3 {fill: url(#data3);} | |
.data4 {fill: url(#data4);} | |
.data5 {fill: url(#data5);} | |
.data6 {fill: url(#data6);} | |
.data7 {fill: url(#data7);} | |
.data8 {fill: url(#data8);} | |
.data9 {fill: url(#data9);} | |
</style> | |
</head> | |
<body onload="start();"> | |
<h1>Sample</h1> | |
<div class="sample"> | |
<svg id="surface1" viewBox="0 0 250 250" style="width: 250px; height: 250px;"></svg> | |
<svg id="surface2" width="250" height="250"></svg> | |
<svg id="surface3" width="250" height="250"></svg> | |
<svg id="surface4" width="250" height="250"></svg> | |
<svg id="surface5" width="250" height="250"></svg> | |
<svg id="surface6" width="250" height="250"></svg> | |
</div> | |
<h1>Multi-level donut chart</h1> | |
<div class="donut"> | |
<svg id="donut" width="500" height="500"></svg> | |
</div> | |
<div style="width: 0; height: 0; position: absolute; visibility: hidden;"> | |
<svg viewBox="0 0 250 250"> | |
<defs> | |
<radialGradient id="global-gradient" gradientUnits="userSpaceOnUse"> | |
<stop offset="0%" class="initial-color" /> | |
<stop offset="100%" class="final-color" /> | |
</radialGradient> | |
</defs> | |
</svg> | |
<svg viewBox="0 0 500 500"> | |
<defs> | |
<radialGradient id="data0" gradientUnits="userSpaceOnUse"> | |
<stop offset="0%" stop-color="#f00" /> | |
<stop offset="100%" stop-color="#f00" stop-opacity="0.6" /> | |
</radialGradient> | |
<radialGradient id="data1" gradientUnits="userSpaceOnUse"> | |
<stop offset="0%" stop-color="#f80" /> | |
<stop offset="100%" stop-color="#f80" stop-opacity="0.6" /> | |
</radialGradient> | |
<radialGradient id="data2" gradientUnits="userSpaceOnUse"> | |
<stop offset="0%" stop-color="#ff0" /> | |
<stop offset="100%" stop-color="#ff0" stop-opacity="0.6" /> | |
</radialGradient> | |
<radialGradient id="data3" gradientUnits="userSpaceOnUse"> | |
<stop offset="0%" stop-color="#0f0" /> | |
<stop offset="100%" stop-color="#0f0" stop-opacity="0.6" /> | |
</radialGradient> | |
<radialGradient id="data4" gradientUnits="userSpaceOnUse"> | |
<stop offset="0%" stop-color="#00f" /> | |
<stop offset="100%" stop-color="#00f" stop-opacity="0.6" /> | |
</radialGradient> | |
<radialGradient id="data5" gradientUnits="userSpaceOnUse"> | |
<stop offset="0%" stop-color="#008" /> | |
<stop offset="100%" stop-color="#008" stop-opacity="0.6" /> | |
</radialGradient> | |
<radialGradient id="data6" gradientUnits="userSpaceOnUse"> | |
<stop offset="0%" stop-color="#f0f" /> | |
<stop offset="100%" stop-color="#f0f" stop-opacity="0.6" /> | |
</radialGradient> | |
<radialGradient id="data7" gradientUnits="userSpaceOnUse"> | |
<stop offset="0%" stop-color="#f44" /> | |
<stop offset="100%" stop-color="#f44" stop-opacity="0.6" /> | |
</radialGradient> | |
<radialGradient id="data8" gradientUnits="userSpaceOnUse"> | |
<stop offset="0%" stop-color="#4f4" /> | |
<stop offset="100%" stop-color="#4f4" stop-opacity="0.6" /> | |
</radialGradient> | |
<radialGradient id="data9" gradientUnits="userSpaceOnUse"> | |
<stop offset="0%" stop-color="#44f" /> | |
<stop offset="100%" stop-color="#44f" stop-opacity="0.6" /> | |
</radialGradient> | |
</defs> | |
</svg> | |
</div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
IE note: Sometimes IE doesn't implement
classList
on SVG nodes. More than that: sometimes IE implementsclassName
as a read-only property on SVG nodes. Interesting that it does not depend on an IE version (both IE10 and IE11 have this problem), but rather a Windows version. For example: IE11 on Win10 works fine, while IE10 on Win7, and IE11 on WIn7 blow the gasket.Solution: use
setAttribute('class', ...)
to assign a class list. Sigh.