Skip to content

Instantly share code, notes, and snippets.

@uhop
Last active November 11, 2020 23:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save uhop/b3c363e1af032f12be6f9d8267d1abb5 to your computer and use it in GitHub Desktop.
Save uhop/b3c363e1af032f12be6f9d8267d1abb5 to your computer and use it in GitHub Desktop.
All necessary Pie chart calculations based on dojox.charting.
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);
};
}
<!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>
@uhop
Copy link
Author

uhop commented Jun 27, 2016

IE note: Sometimes IE doesn't implement classList on SVG nodes. More than that: sometimes IE implements className 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment