UNPKG

dojox

Version:

Dojo eXtensions, a rollup of many useful sub-projects and varying states of maturity – from very stable and robust, to alpha and experimental. See individual projects contain README files for details.

802 lines (738 loc) 26.7 kB
define(["dojo/_base/lang", "dojo/_base/array" ,"dojo/_base/declare", "dojo/dom-geometry", "dojo/_base/Color", "./Base", "./_PlotEvents", "./common", "dojox/gfx", "dojox/gfx/matrix", "dojox/lang/functional", "dojox/lang/utils","dojo/has"], function(lang, arr, declare, domGeom, Color, Base, PlotEvents, dc, g, m, df, du, has){ /*===== declare("dojox.charting.plot2d.__PieCtorArgs", dojox.charting.plot2d.__DefaultCtorArgs, { // summary: // Specialized keyword arguments object for use in defining parameters on a Pie chart. // labels: Boolean? // Whether or not to draw labels for each pie slice. Default is true. labels: true, // ticks: Boolean? // Whether or not to draw ticks to labels within each slice. Default is false. ticks: false, // fixed: Boolean? // Whether a fixed precision must be applied to data values for display. Default is true. fixed: true, // precision: Number? // The precision at which to round data values for display. Default is 0. precision: 1, // labelOffset: Number? // The amount in pixels by which to offset labels. Default is 20. labelOffset: 20, // labelStyle: String? // Options as to where to draw labels. Values include "default", and "columns". Default is "default". labelStyle: "default", // default/columns // omitLabels: Boolean? // Whether labels of slices small to the point of not being visible are omitted. Default false. omitLabels: false, // htmlLabels: Boolean? // Whether or not to use HTML to render slice labels. Default is true. htmlLabels: true, // radGrad: String? // The type of radial gradient to use in rendering. Default is "native". radGrad: "native", // fanSize: Number? // The amount for a radial gradient. Default is 5. fanSize: 5, // startAngle: Number? // Where to being rendering gradients in slices, in degrees. Default is 0. startAngle: 0, // radius: Number? // The size of the radial gradient. Default is 0. radius: 0, // shadow: dojox.gfx.Stroke? // An optional stroke to use to draw any shadows for a series on a plot. shadow: {}, // fill: dojox.gfx.Fill? // Any fill to be used for elements on the plot. fill: {}, // filter: dojox.gfx.Filter? // An SVG filter to be used for elements on the plot. gfx SVG renderer must be used and dojox/gfx/svgext must // be required for this to work. filter: {}, // styleFunc: Function? // A function that returns a styling object for the a given data item. styleFunc: null, // innerRadius: Number? // The inner radius of a ring in percent (0-100). If value < 0 // then it is assumed to be pixels, not percent. innerRadius: 0, // minWidth: Number? // The minimum width of a pie slice at its chord. The default is 10px. minWidth: 10 }); =====*/ var FUDGE_FACTOR = 0.2; // use to overlap fans return declare("dojox.charting.plot2d.Pie", [Base, PlotEvents], { // summary: // The plot that represents a typical pie chart. defaultParams: { labels: true, ticks: false, fixed: true, precision: 1, labelOffset: 20, labelStyle: "default", // default/columns htmlLabels: true, // use HTML to draw labels radGrad: "native", // or "linear", or "fan" fanSize: 5, // maximum fan size in degrees startAngle: 0, // start angle for slices in degrees innerRadius: 0, // inner radius in pixels minWidth: 0, // minimal width of degenerated slices zeroDataMessage: "" // The message to display when there is no data, if provided by the user. }, optionalParams: { radius: 0, omitLabels: false, // theme components stroke: {}, outline: {}, shadow: {}, fill: {}, filter: {}, styleFunc: null, font: "", fontColor: "", labelWiring: {} }, constructor: function(chart, kwArgs){ // summary: // Create a pie plot. this.opt = lang.clone(this.defaultParams); du.updateWithObject(this.opt, kwArgs); du.updateWithPattern(this.opt, kwArgs, this.optionalParams); this.axes = []; this.run = null; this.dyn = []; this.runFilter = []; if(kwArgs && kwArgs.hasOwnProperty("innerRadius")){ this._plotSetInnerRadius = true; } }, clear: function(){ // summary: // Clear out all of the information tied to this plot. // returns: dojox/charting/plot2d/Pie // A reference to this plot for functional chaining. this.inherited(arguments); this.dyn = []; this.run = null; return this; // dojox/charting/plot2d/Pie }, setAxis: function(axis){ // summary: // Dummy method, since axes are irrelevant with a Pie chart. // returns: dojox/charting/plot2d/Pie // The reference to this plot for functional chaining. return this; // dojox/charting/plot2d/Pie }, addSeries: function(run){ // summary: // Add a series of data to this plot. // returns: dojox/charting/plot2d/Pie // The reference to this plot for functional chaining. this.run = run; return this; // dojox/charting/plot2d/Pie }, getSeriesStats: function(){ // summary: // Returns default stats (irrelevant for this type of plot). // returns: Object // {hmin, hmax, vmin, vmax} min/max in both directions. return lang.delegate(dc.defaultStats); // Object }, getRequiredColors: function(){ // summary: // Return the number of colors needed to draw this plot. return this.run ? this.run.data.length : 0; }, render: function(dim, offsets){ // summary: // Render the plot on the chart. // dim: Object // An object of the form { width, height }. // offsets: Object // An object of the form { l, r, t, b }. // returns: dojox/charting/plot2d/Pie // A reference to this plot for functional chaining. if(!this.dirty){ return this; } this.resetEvents(); this.dirty = false; this._eventSeries = {}; this.cleanGroup(); var s = this.group, t = this.chart.theme; if(!this._plotSetInnerRadius && t && t.pieInnerRadius){ this.opt.innerRadius = t.pieInnerRadius; } // calculate the geometry var rx = (dim.width - offsets.l - offsets.r) / 2, ry = (dim.height - offsets.t - offsets.b) / 2, r = Math.min(rx, ry), taFont = "font" in this.opt ? this.opt.font : t.axis.tick.titleFont || "", size = taFont ? g.normalizedLength(g.splitFontString(taFont).size) : 0, taFontColor = this.opt.hasOwnProperty("fontColor") ? this.opt.fontColor : t.axis.tick.fontColor, startAngle = m._degToRad(this.opt.startAngle), start = startAngle, filteredRun, slices, labels, shift, labelR, run = this.run.data, events = this.events(); /* Added to handle no data case */ var noDataFunc = lang.hitch(this, function(){ var ct = t.clone(); var themes = df.map(run, function(v){ var tMixin = [this.opt, this.run]; if(v !== null && typeof v != "number"){ tMixin.push(v); } if(this.opt.styleFunc){ tMixin.push(this.opt.styleFunc(v)); } return ct.next("slice", tMixin, true); }, this); // Draw initial pie, with text in it noting 0 data. if("radius" in this.opt){ r = this.opt.radius < r ? this.opt.radius : r; } var circle = { cx: offsets.l + rx, cy: offsets.t + ry, r: r }; var rColor = new Color(taFontColor); // If we have a radius, we'll need to fade the ring some if(this.opt.innerRadius){ rColor.a = 0.1; } var ring = this._createRing(s, circle).setStroke(rColor); if(this.opt.innerRadius){ // If we have a radius, fill it with the faded color. ring.setFill(rColor); } if(this.opt.zeroDataMessage){ this.renderLabel(s, circle.cx, circle.cy + size/3, this.opt.zeroDataMessage, { series: { font: taFont, fontColor: taFontColor } }, null, "middle"); } this.dyn = []; arr.forEach(run, function(item, i){ this.dyn.push({ fill: this._plotFill(themes[i].series.fill, dim, offsets), stroke: themes[i].series.stroke}); }, this); }); /* END Added to handle no data case */ // Draw over circle! if(!this.run && !this.run.data.ength){ noDataFunc(); return this; } if(typeof run[0] == "number"){ filteredRun = df.map(run, "x ? Math.max(x, 0) : 0"); if(df.every(filteredRun, "<= 0")){ noDataFunc(); return this; } slices = df.map(filteredRun, "/this", df.foldl(filteredRun, "+", 0)); if(this.opt.labels){ labels = arr.map(slices, function(x){ return x > 0 ? this._getLabel(x * 100) + "%" : ""; }, this); } }else{ filteredRun = df.map(run, "x ? Math.max(x.y, 0) : 0"); if(!filteredRun.length || df.every(filteredRun, "<= 0")){ noDataFunc(); return this; } slices = df.map(filteredRun, "/this", df.foldl(filteredRun, "+", 0)); if(this.opt.labels){ labels = arr.map(slices, function(x, i){ if(x < 0){ return ""; } var v = run[i]; return v.hasOwnProperty("text") ? v.text : this._getLabel(x * 100) + "%"; }, this); } } var themes = df.map(run, function(v){ var tMixin = [this.opt, this.run]; if(v !== null && typeof v != "number"){ tMixin.push(v); } if(this.opt.styleFunc){ tMixin.push(this.opt.styleFunc(v)); } return t.next("slice", tMixin, true); }, this); if(this.opt.labels) { shift = df.foldl1(df.map(labels, function(label, i){ var font = themes[i].series.font; return g._base._getTextBox(label, {font: font}).w; }, this), "Math.max(a, b)") / 2; if(this.opt.labelOffset < 0){ r = Math.min(rx - 2 * shift, ry - size) + this.opt.labelOffset; } } if(this.opt.hasOwnProperty("radius")){ r = this.opt.radius < r * 0.9 ? this.opt.radius : r * 0.9; } if (!this.opt.radius && this.opt.labels && this.opt.labelStyle == "columns") { r = r / 2; if (rx > ry && rx > r * 2) { r *= rx / (r * 2); } if (r >= ry * 0.8) { r = ry * 0.8; } } else { if (r >= ry * 0.9) { r = ry * 0.9; } } labelR = r - this.opt.labelOffset; var circle = { cx: offsets.l + rx, cy: offsets.t + ry, r: r }; this.dyn = []; // draw slices var eventSeries = new Array(slices.length); // Calulate primarily size for each slice var slicesSteps = [], localStart = start; var minWidth = this.opt.minWidth; arr.forEach(slices, function(slice, i){ if(slice === 0){ slicesSteps[i] = { step: 0, end: localStart, start: localStart, weak: false }; return; } var end = localStart + slice * 2 * Math.PI; if(i === slices.length - 1){ end = startAngle + 2 * Math.PI; } var step = end - localStart, dist = step * r; slicesSteps[i] = { step: step, start: localStart, end: end, weak: dist < minWidth }; localStart = end; }); if(minWidth > 0){ var weakCount = 0, weakCoef = minWidth / r, oldWeakCoefSum = 0, i; for(i = slicesSteps.length - 1; i >= 0; i--){ if(slicesSteps[i].weak){ ++weakCount; oldWeakCoefSum += slicesSteps[i].step; slicesSteps[i].step = weakCoef; } } // make sure that our steps are small enough var weakCoefSum = weakCount * weakCoef; if(weakCoefSum > Math.PI){ weakCoef = Math.PI / weakCount; for(i = 0; i < slicesSteps.length; ++i){ if(slicesSteps[i].weak){ slicesSteps[i].step = weakCoef; } } weakCoefSum = Math.PI; } // now let's redistribute percentage if(weakCount > 0){ weakCoef = 1 - (weakCoefSum - oldWeakCoefSum) / 2 / Math.PI; for(i = 0; i < slicesSteps.length; ++i){ if(!slicesSteps[i].weak){ slicesSteps[i].step = weakCoef * slicesSteps[i].step; } } } // now let's update start and end values for(i = 0; i < slicesSteps.length; ++i){ slicesSteps[i].start = i ? slicesSteps[i].end : localStart; slicesSteps[i].end = slicesSteps[i].start + slicesSteps[i].step; } // let's make sure that our last end is exactly 2 * Math.PI for(i = slicesSteps.length - 1; i >= 0; --i){ if(slicesSteps[i].step !== 0){ slicesSteps[i].end = localStart + 2 * Math.PI; break; } } } localStart = start; var o, specialFill; arr.some(slices, function(slice, i){ var shape; var v = run[i], theme = themes[i]; if(slice >= 1){ // whole pie specialFill = this._plotFill(theme.series.fill, dim, offsets); specialFill = this._shapeFill(specialFill, { x: circle.cx - circle.r, y: circle.cy - circle.r, width: 2 * circle.r, height: 2 * circle.r }); specialFill = this._pseudoRadialFill(specialFill, {x: circle.cx, y: circle.cy}, circle.r); shape = this._createRing(s, circle).setFill(specialFill).setStroke(theme.series.stroke); this.dyn.push({fill: specialFill, stroke: theme.series.stroke}); if(events){ o = { element: "slice", index: i, run: this.run, shape: shape, x: i, y: typeof v == "number" ? v : v.y, cx: circle.cx, cy: circle.cy, cr: r }; this._connectEvents(o); eventSeries[i] = o; } var k; for(k = i + 1; k < slices.length; k++){ theme = themes[k]; this.dyn.push({fill: theme.series.fill, stroke: theme.series.stroke}); } return true; // stop iteration } if(slicesSteps[i].step === 0){ // degenerated slice // But we still want a fill since this will be skipped and we need the fill // for the label. this.dyn.push({fill: theme.series.fill, stroke: theme.series.stroke}); return false; // continue } // calculate the geometry of the slice var step = slicesSteps[i].step, x1 = circle.cx + r * Math.cos(localStart), y1 = circle.cy + r * Math.sin(localStart), x2 = circle.cx + r * Math.cos(localStart + step), y2 = circle.cy + r * Math.sin(localStart + step); // draw the slice var fanSize = m._degToRad(this.opt.fanSize), stroke; if(theme.series.fill && theme.series.fill.type === "radial" && this.opt.radGrad === "fan" && step > fanSize){ var group = s.createGroup(), nfans = Math.ceil(step / fanSize), delta = step / nfans; specialFill = this._shapeFill(theme.series.fill, {x: circle.cx - circle.r, y: circle.cy - circle.r, width: 2 * circle.r, height: 2 * circle.r}); var j, alpha, beta, fansx, fansy, fanex, faney; for(j = 0; j < nfans; ++j){ alpha = localStart + (j - FUDGE_FACTOR) * delta; beta = localStart + (j + 1 + FUDGE_FACTOR) * delta; fansx = j == 0 ? x1 : circle.cx + r * Math.cos(alpha); fansy = j == 0 ? y1 : circle.cy + r * Math.sin(alpha); fanex = j == nfans - 1 ? x2 : circle.cx + r * Math.cos(beta); faney = j == nfans - 1 ? y2 : circle.cy + r * Math.sin(beta); this._createSlice(group, circle, r, fansx, fansy, fanex, faney, alpha, delta). setFill(this._pseudoRadialFill(specialFill, {x: circle.cx, y: circle.cy}, r, localStart + (j + 0.5) * delta, localStart + (j + 0.5) * delta)); } stroke = theme.series.stroke; this._createSlice(group, circle, r, x1, y1, x2, y2, localStart, step).setStroke(stroke); shape = group; }else{ stroke = theme.series.stroke; shape = this._createSlice(s, circle, r, x1, y1, x2, y2, localStart, step).setStroke(stroke); specialFill = theme.series.fill; if(specialFill && specialFill.type === "radial"){ specialFill = this._shapeFill(specialFill, {x: circle.cx - circle.r, y: circle.cy - circle.r, width: 2 * circle.r, height: 2 * circle.r}); if(this.opt.radGrad === "linear"){ specialFill = this._pseudoRadialFill(specialFill, {x: circle.cx, y: circle.cy}, r, localStart, localStart + step); } }else if(specialFill && specialFill.type === "linear"){ var bbox = lang.clone(shape.getBoundingBox()); if(g.renderer === "svg"){ // Try to fix the bounding box calculations for // height. Only really works for SVG. var pos = {w: 0, h: 0}; try{ pos = domGeom.position(shape.rawNode); }catch(ignore){} if(pos.h > bbox.height){ bbox.height = pos.h; } if(pos.w > bbox.width){ bbox.width = pos.w; } } specialFill = this._plotFill(specialFill, dim, offsets); specialFill = this._shapeFill(specialFill, bbox); } shape.setFill(specialFill); } this.dyn.push({fill: specialFill, stroke: theme.series.stroke}); if(events){ o = { element: "slice", index: i, run: this.run, shape: shape, x: i, y: typeof v == "number" ? v : v.y, cx: circle.cx, cy: circle.cy, cr: r }; this._connectEvents(o); eventSeries[i] = o; } localStart = localStart + step; return false; // continue }, this); // draw labels if(this.opt.labels){ var isRtl = has("dojo-bidi") && this.chart.isRightToLeft(); if(this.opt.labelStyle == "default"){ start = startAngle; localStart = start; arr.some(slices, function(slice, i){ if(slice <= 0 && !this.opt.minWidth){ // degenerated slice return false; // continue } var theme = themes[i]; if(slice >= 1){ // whole pie this.renderLabel(s, circle.cx, circle.cy + size / 2, labels[i], theme, this.opt.labelOffset > 0); return true; // stop iteration } // calculate the geometry of the slice var end = start + slice * 2 * Math.PI; if(i + 1 == slices.length){ end = startAngle + 2 * Math.PI; } if(this.opt.omitLabels && end-start < 0.001){ return false; // continue } var labelAngle = localStart + (slicesSteps[i].step / 2),//(start + end) / 2, x = circle.cx + labelR * Math.cos(labelAngle), y = circle.cy + labelR * Math.sin(labelAngle) + size / 2; // draw the label this.renderLabel(s, isRtl ? dim.width - x : x, y, labels[i], theme, this.opt.labelOffset > 0); localStart += slicesSteps[i].step; start = end; return false; // continue }, this); }else if(this.opt.labelStyle == "columns"){ //calculate label angles var omitLabels = this.opt.omitLabels; start = startAngle; localStart = start; var labeledSlices = [], significantCount = 0, k; for(k = slices.length - 1; k >= 0; --k){ if(slices[k]){ ++significantCount; } } arr.forEach(slices, function(slice, i){ var end = start + slice * 2 * Math.PI; if(i + 1 == slices.length){ end = startAngle + 2 * Math.PI; } if(this.minWidth !== 0 || end - start >= 0.001){ // var labelAngle = (start + end) / 2; var labelAngle = localStart + (slicesSteps[i].step / 2);//(start + end) / 2, if(significantCount === 1 && !this.opt.minWidth){ labelAngle = (start + end) / 2; } labeledSlices.push({ angle: labelAngle, left: Math.cos(labelAngle) < 0, theme: themes[i], index: i, omit: omitLabels? end - start < 0.001:false }); } start = end; localStart += slicesSteps[i].step; }, this); //calculate label radius to each slice var labelHeight = g._base._getTextBox("a", {font:taFont, whiteSpace: "nowrap"}).h; this._getProperLabelRadius(labeledSlices, labelHeight, circle.r * 1.1); //draw label and wiring var leftColumn = circle.cx - circle.r * 2, rightColumn = circle.cx + circle.r * 2; arr.forEach(labeledSlices, function(slice){ if(slice.omit){ return; } var cTheme = themes[slice.index], lrPadding = 0; if(cTheme && cTheme.axis && cTheme.axis.tick && cTheme.axis.tick.labelGap){ // Try to pad the lable a bit, the same as a tick gap. lrPadding = cTheme.axis.tick.labelGap; } var labelWidth = g._base._getTextBox(labels[slice.index], {font: cTheme.series.font, whiteSpace: "nowrap", paddingLeft: lrPadding + "px"}).w, x = circle.cx + slice.labelR * Math.cos(slice.angle), y = circle.cy + slice.labelR * Math.sin(slice.angle), jointX = (slice.left) ? (leftColumn + labelWidth) : (rightColumn - labelWidth), labelX = (slice.left) ? leftColumn : jointX + lrPadding, newRadius = circle.r, wiring = s.createPath().moveTo(circle.cx + newRadius * Math.cos(slice.angle), circle.cy + newRadius * Math.sin(slice.angle)); if(Math.abs(slice.labelR * Math.cos(slice.angle)) < circle.r * 2 - labelWidth){ wiring.lineTo(x, y); } wiring.lineTo(jointX, y).setStroke(slice.theme.series.labelWiring); // Push the wiring to the back so that highlight/magnify actions don't bleed the wire. wiring.moveToBack(); // Try to adjust the wiring position here. The browser always adds a bit // of padding on height, so divide by 3 instead of 2. var mid = labelHeight/3 + y; var elem = this.renderLabel(s, labelX, mid || 0, labels[slice.index], cTheme, false, "left"); if(events && !this.opt.htmlLabels){ var fontWidth = g._base._getTextBox(labels[slice.index], {font: slice.theme.series.font}).w || 0, fontHeight = g.normalizedLength(g.splitFontString(slice.theme.series.font).size); o = { element: "labels", index: slice.index, run: this.run, shape: elem, x: labelX, y: y, label: labels[slice.index] }; var shp = elem.getShape(), lt = domGeom.position(this.chart.node, true), aroundRect = lang.mixin({ type : 'rect' }, { x: shp.x, y: shp.y - 2 * fontHeight }); aroundRect.x += lt.x; aroundRect.y += lt.y; aroundRect.x = Math.round(aroundRect.x); aroundRect.y = Math.round(aroundRect.y); aroundRect.width = Math.ceil(fontWidth); aroundRect.height = Math.ceil(fontHeight); o.aroundRect = aroundRect; this._connectEvents(o); eventSeries[slices.length + slice.index] = o; } }, this); } } // post-process events to restore the original indexing var esi = 0; this._eventSeries[this.run.name] = df.map(run, function(v){ return v <= 0 ? null : eventSeries[esi++]; }); // chart mirroring starts if(has("dojo-bidi")){ this._checkOrientation(this.group, dim, offsets); } return this; // dojox/charting/plot2d/Pie }, _getProperLabelRadius: function(slices, labelHeight, minRadius){ if(slices.length == 1){ slices[0].labelR = minRadius; return; } var leftCenterSlice = {}, rightCenterSlice = {}, leftMinSIN = 2, rightMinSIN = 2, i; var tempSIN; for(i = 0; i < slices.length; ++i){ tempSIN = Math.abs(Math.sin(slices[i].angle)); if(slices[i].left){ if(leftMinSIN > tempSIN){ leftMinSIN = tempSIN; leftCenterSlice = slices[i]; } }else{ if(rightMinSIN > tempSIN){ rightMinSIN = tempSIN; rightCenterSlice = slices[i]; } } } leftCenterSlice.labelR = rightCenterSlice.labelR = minRadius; this._caculateLabelR(leftCenterSlice, slices, labelHeight); this._caculateLabelR(rightCenterSlice, slices, labelHeight); }, _caculateLabelR: function(firstSlice, slices, labelHeight){ var i, j, k, length = slices.length, currentLabelR = firstSlice.labelR, nextLabelR, step = slices[firstSlice.index].left ? -labelHeight : labelHeight; for(k = 0, i = firstSlice.index, j = (i + 1) % length; k < length && slices[i].left === slices[j].left; ++k){ nextLabelR = (Math.sin(slices[i].angle) * currentLabelR + step) / Math.sin(slices[j].angle); currentLabelR = Math.max(firstSlice.labelR, nextLabelR); slices[j].labelR = currentLabelR; i = (i + 1) % length; j = (j + 1) % length; } if(k >= length){ slices[0].labelR = firstSlice.labelR; } for(k = 0, i = firstSlice.index, j = (i || length) - 1; k < length && slices[i].left === slices[j].left; ++k){ nextLabelR = (Math.sin(slices[i].angle) * currentLabelR - step) / Math.sin(slices[j].angle); currentLabelR = Math.max(firstSlice.labelR, nextLabelR); slices[j].labelR = currentLabelR; i = (i || length) - 1; j = (j || length) - 1; } }, _createRing: function(group, circle){ var r = this.opt.innerRadius; if(r > 0){ // Percentage, use circle. Anything < 0 for innerRadius // is assumed to be a multiple of the radius. So 0.25 innerRadius value // is computed to be 25% of the outer radius. r = circle.r * (r/100); }else if(r < 0){ r = -r; // Assume it is pixels, fixed size hole. } if(r){ return group.createPath({}).setAbsoluteMode(true). moveTo(circle.cx, circle.cy - circle.r). arcTo(circle.r, circle.r, 0, false, true, circle.cx + circle.r, circle.cy). arcTo(circle.r, circle.r, 0, true, true, circle.cx, circle.cy - circle.r). closePath(). moveTo(circle.cx, circle.cy - r). arcTo(r, r, 0, false, true, circle.cx + r, circle.cy). arcTo(r, r, 0, true, true, circle.cx, circle.cy - r). closePath(); } return group.createCircle(circle); }, _createSlice: function(group, circle, R, x1, y1, x2, y2, fromAngle, stepAngle){ var r = this.opt.innerRadius; if(r > 0){ // Percentage, use circle. Anything < 0 for innerRadius // is assumed to be a multiple of the radius. So 0.25 innerRadius value // is computed to be 25% of the outer radius. r = circle.r * (r/100); }else if(r < 0){ r = -r; // Assume it is pixels, fixed size hole. } if(r){ var innerX1 = circle.cx + r * Math.cos(fromAngle), innerY1 = circle.cy + r * Math.sin(fromAngle), innerX2 = circle.cx + r * Math.cos(fromAngle + stepAngle), innerY2 = circle.cy + r * Math.sin(fromAngle + stepAngle); return group.createPath({}).setAbsoluteMode(true). moveTo(innerX1, innerY1). lineTo(x1, y1). arcTo(R, R, 0, stepAngle > Math.PI, true, x2, y2). lineTo(innerX2, innerY2). arcTo(r, r, 0, stepAngle > Math.PI, false, innerX1, innerY1). closePath(); } return group.createPath({}).setAbsoluteMode(true). moveTo(circle.cx, circle.cy). lineTo(x1, y1). arcTo(R, R, 0, stepAngle > Math.PI, true, x2, y2). lineTo(circle.cx, circle.cy). closePath(); } }); });