492 lines
17 KiB
JavaScript
492 lines
17 KiB
JavaScript
|
//>>built
|
||
|
define("dojox/charting/plot2d/Pie", ["dojo/_base/lang", "dojo/_base/array" ,"dojo/_base/declare",
|
||
|
"../Element", "./_PlotEvents", "./common", "../axis2d/common",
|
||
|
"dojox/gfx", "dojox/gfx/matrix", "dojox/lang/functional", "dojox/lang/utils"],
|
||
|
function(lang, arr, declare, Element, PlotEvents, dc, da, g, m, df, du){
|
||
|
|
||
|
/*=====
|
||
|
var Element = dojox.charting.Element;
|
||
|
var PlotEvents = dojox.charting.plot2d._PlotEvents;
|
||
|
dojo.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?
|
||
|
// TODO
|
||
|
fixed: true,
|
||
|
|
||
|
// precision: Number?
|
||
|
// The precision at which to sum/add data values. Default is 1.
|
||
|
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
|
||
|
|
||
|
// 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
|
||
|
});
|
||
|
=====*/
|
||
|
|
||
|
var FUDGE_FACTOR = 0.2; // use to overlap fans
|
||
|
|
||
|
return declare("dojox.charting.plot2d.Pie", [Element, 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
|
||
|
},
|
||
|
optionalParams: {
|
||
|
radius: 0,
|
||
|
// theme components
|
||
|
stroke: {},
|
||
|
outline: {},
|
||
|
shadow: {},
|
||
|
fill: {},
|
||
|
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.run = null;
|
||
|
this.dyn = [];
|
||
|
},
|
||
|
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.dirty = true;
|
||
|
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);
|
||
|
},
|
||
|
initializeScalers: function(){
|
||
|
// summary:
|
||
|
// Does nothing (irrelevant for this type of plot).
|
||
|
return this;
|
||
|
},
|
||
|
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.run || !this.run.data.length){
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
// 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.font,
|
||
|
size = taFont ? g.normalizedLength(g.splitFontString(taFont).size) : 0,
|
||
|
taFontColor = "fontColor" in this.opt ? this.opt.fontColor : t.axis.fontColor,
|
||
|
startAngle = m._degToRad(this.opt.startAngle),
|
||
|
start = startAngle, step, filteredRun, slices, labels, shift, labelR,
|
||
|
run = this.run.data,
|
||
|
events = this.events();
|
||
|
if(typeof run[0] == "number"){
|
||
|
filteredRun = df.map(run, "x ? Math.max(x, 0) : 0");
|
||
|
if(df.every(filteredRun, "<= 0")){
|
||
|
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(df.every(filteredRun, "<= 0")){
|
||
|
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 "text" in v ? v.text : this._getLabel(x * 100) + "%";
|
||
|
}, this);
|
||
|
}
|
||
|
}
|
||
|
var themes = df.map(run, function(v, i){
|
||
|
if(v === null || typeof v == "number"){
|
||
|
return t.next("slice", [this.opt, this.run], true);
|
||
|
}
|
||
|
return t.next("slice", [this.opt, this.run, v], 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;
|
||
|
}
|
||
|
labelR = r - this.opt.labelOffset;
|
||
|
}
|
||
|
if("radius" in this.opt){
|
||
|
r = this.opt.radius;
|
||
|
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);
|
||
|
arr.some(slices, function(slice, i){
|
||
|
if(slice < 0){
|
||
|
// degenerated slice
|
||
|
return false; // continue
|
||
|
}
|
||
|
if(slice == 0){
|
||
|
this.dyn.push({fill: null, stroke: null});
|
||
|
return false;
|
||
|
}
|
||
|
var v = run[i], theme = themes[i], specialFill;
|
||
|
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);
|
||
|
var shape = s.createCircle(circle).setFill(specialFill).setStroke(theme.series.stroke);
|
||
|
this.dyn.push({fill: specialFill, stroke: theme.series.stroke});
|
||
|
|
||
|
if(events){
|
||
|
var 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;
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
var step = end - start,
|
||
|
x1 = circle.cx + r * Math.cos(start),
|
||
|
y1 = circle.cy + r * Math.sin(start),
|
||
|
x2 = circle.cx + r * Math.cos(end),
|
||
|
y2 = circle.cy + r * Math.sin(end);
|
||
|
// draw the slice
|
||
|
var fanSize = m._degToRad(this.opt.fanSize);
|
||
|
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});
|
||
|
for(var j = 0; j < nfans; ++j){
|
||
|
var fansx = j == 0 ? x1 : circle.cx + r * Math.cos(start + (j - FUDGE_FACTOR) * delta),
|
||
|
fansy = j == 0 ? y1 : circle.cy + r * Math.sin(start + (j - FUDGE_FACTOR) * delta),
|
||
|
fanex = j == nfans - 1 ? x2 : circle.cx + r * Math.cos(start + (j + 1 + FUDGE_FACTOR) * delta),
|
||
|
faney = j == nfans - 1 ? y2 : circle.cy + r * Math.sin(start + (j + 1 + FUDGE_FACTOR) * delta),
|
||
|
fan = group.createPath().
|
||
|
moveTo(circle.cx, circle.cy).
|
||
|
lineTo(fansx, fansy).
|
||
|
arcTo(r, r, 0, delta > Math.PI, true, fanex, faney).
|
||
|
lineTo(circle.cx, circle.cy).
|
||
|
closePath().
|
||
|
setFill(this._pseudoRadialFill(specialFill, {x: circle.cx, y: circle.cy}, r, start + (j + 0.5) * delta, start + (j + 0.5) * delta));
|
||
|
}
|
||
|
group.createPath().
|
||
|
moveTo(circle.cx, circle.cy).
|
||
|
lineTo(x1, y1).
|
||
|
arcTo(r, r, 0, step > Math.PI, true, x2, y2).
|
||
|
lineTo(circle.cx, circle.cy).
|
||
|
closePath().
|
||
|
setStroke(theme.series.stroke);
|
||
|
shape = group;
|
||
|
}else{
|
||
|
shape = s.createPath().
|
||
|
moveTo(circle.cx, circle.cy).
|
||
|
lineTo(x1, y1).
|
||
|
arcTo(r, r, 0, step > Math.PI, true, x2, y2).
|
||
|
lineTo(circle.cx, circle.cy).
|
||
|
closePath().
|
||
|
setStroke(theme.series.stroke);
|
||
|
var 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, start, end);
|
||
|
}
|
||
|
}else if(specialFill && specialFill.type === "linear"){
|
||
|
specialFill = this._plotFill(specialFill, dim, offsets);
|
||
|
specialFill = this._shapeFill(specialFill, shape.getBoundingBox());
|
||
|
}
|
||
|
shape.setFill(specialFill);
|
||
|
}
|
||
|
this.dyn.push({fill: specialFill, stroke: theme.series.stroke});
|
||
|
|
||
|
if(events){
|
||
|
var 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;
|
||
|
}
|
||
|
|
||
|
start = end;
|
||
|
|
||
|
return false; // continue
|
||
|
}, this);
|
||
|
// draw labels
|
||
|
if(this.opt.labels){
|
||
|
if(this.opt.labelStyle == "default"){
|
||
|
start = startAngle;
|
||
|
arr.some(slices, function(slice, i){
|
||
|
if(slice <= 0){
|
||
|
// degenerated slice
|
||
|
return false; // continue
|
||
|
}
|
||
|
var theme = themes[i];
|
||
|
if(slice >= 1){
|
||
|
// whole pie
|
||
|
var v = run[i], elem = da.createText[this.opt.htmlLabels && g.renderer != "vml" ? "html" : "gfx"](
|
||
|
this.chart, s, circle.cx, circle.cy + size / 2, "middle", labels[i],
|
||
|
theme.series.font, theme.series.fontColor);
|
||
|
if(this.opt.htmlLabels){
|
||
|
this.htmlElements.push(elem);
|
||
|
}
|
||
|
return true; // stop iteration
|
||
|
}
|
||
|
// calculate the geometry of the slice
|
||
|
var end = start + slice * 2 * Math.PI, v = run[i];
|
||
|
if(i + 1 == slices.length){
|
||
|
end = startAngle + 2 * Math.PI;
|
||
|
}
|
||
|
var labelAngle = (start + end) / 2,
|
||
|
x = circle.cx + labelR * Math.cos(labelAngle),
|
||
|
y = circle.cy + labelR * Math.sin(labelAngle) + size / 2;
|
||
|
// draw the label
|
||
|
var elem = da.createText[this.opt.htmlLabels && g.renderer != "vml" ? "html" : "gfx"]
|
||
|
(this.chart, s, x, y, "middle", labels[i], theme.series.font, theme.series.fontColor);
|
||
|
if(this.opt.htmlLabels){
|
||
|
this.htmlElements.push(elem);
|
||
|
}
|
||
|
start = end;
|
||
|
return false; // continue
|
||
|
}, this);
|
||
|
}else if(this.opt.labelStyle == "columns"){
|
||
|
start = startAngle;
|
||
|
//calculate label angles
|
||
|
var labeledSlices = [];
|
||
|
arr.forEach(slices, function(slice, i){
|
||
|
var end = start + slice * 2 * Math.PI;
|
||
|
if(i + 1 == slices.length){
|
||
|
end = startAngle + 2 * Math.PI;
|
||
|
}
|
||
|
var labelAngle = (start + end) / 2;
|
||
|
labeledSlices.push({
|
||
|
angle: labelAngle,
|
||
|
left: Math.cos(labelAngle) < 0,
|
||
|
theme: themes[i],
|
||
|
index: i,
|
||
|
omit: end - start < 0.001
|
||
|
});
|
||
|
start = end;
|
||
|
});
|
||
|
//calculate label radius to each slice
|
||
|
var labelHeight = g._base._getTextBox("a",{font:taFont}).h;
|
||
|
this._getProperLabelRadius(labeledSlices, labelHeight, circle.r * 1.1);
|
||
|
//draw label and wiring
|
||
|
arr.forEach(labeledSlices, function(slice, i){
|
||
|
if (!slice.omit) {
|
||
|
var leftColumn = circle.cx - circle.r * 2,
|
||
|
rightColumn = circle.cx + circle.r * 2,
|
||
|
labelWidth = g._base._getTextBox(labels[i], {font: taFont}).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;
|
||
|
var wiring = s.createPath().moveTo(circle.cx + circle.r * Math.cos(slice.angle), circle.cy + circle.r * 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);
|
||
|
var elem = da.createText[this.opt.htmlLabels && g.renderer != "vml" ? "html" : "gfx"](
|
||
|
this.chart, s, labelX, y, "left", labels[i], slice.theme.series.font, slice.theme.series.fontColor);
|
||
|
if (this.opt.htmlLabels) {
|
||
|
this.htmlElements.push(elem);
|
||
|
}
|
||
|
}
|
||
|
},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++];
|
||
|
});
|
||
|
return this; // dojox.charting.plot2d.Pie
|
||
|
},
|
||
|
|
||
|
_getProperLabelRadius: function(slices, labelHeight, minRidius){
|
||
|
var leftCenterSlice = {},rightCenterSlice = {},leftMinSIN = 1, rightMinSIN = 1;
|
||
|
if (slices.length == 1) {
|
||
|
slices[0].labelR = minRidius;
|
||
|
return;
|
||
|
}
|
||
|
for(var i = 0;i<slices.length;i++){
|
||
|
var 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 = minRidius;
|
||
|
this._calculateLabelR(leftCenterSlice,slices,labelHeight);
|
||
|
this._calculateLabelR(rightCenterSlice,slices,labelHeight);
|
||
|
},
|
||
|
_calculateLabelR: function(firstSlice,slices,labelHeight){
|
||
|
var i = firstSlice.index,length = slices.length,
|
||
|
currentLabelR = firstSlice.labelR;
|
||
|
while(!(slices[i%length].left ^ slices[(i+1)%length].left)){
|
||
|
if (!slices[(i + 1) % length].omit) {
|
||
|
var nextLabelR = (Math.sin(slices[i % length].angle) * currentLabelR + ((slices[i % length].left) ? (-labelHeight) : labelHeight)) /
|
||
|
Math.sin(slices[(i + 1) % length].angle);
|
||
|
currentLabelR = (nextLabelR < firstSlice.labelR) ? firstSlice.labelR : nextLabelR;
|
||
|
slices[(i + 1) % length].labelR = currentLabelR;
|
||
|
}
|
||
|
i++;
|
||
|
}
|
||
|
i = firstSlice.index;
|
||
|
var j = (i == 0)?length-1 : i - 1;
|
||
|
while(!(slices[i].left ^ slices[j].left)){
|
||
|
if (!slices[j].omit) {
|
||
|
var nextLabelR = (Math.sin(slices[i].angle) * currentLabelR + ((slices[i].left) ? labelHeight : (-labelHeight))) /
|
||
|
Math.sin(slices[j].angle);
|
||
|
currentLabelR = (nextLabelR < firstSlice.labelR) ? firstSlice.labelR : nextLabelR;
|
||
|
slices[j].labelR = currentLabelR;
|
||
|
}
|
||
|
i--;j--;
|
||
|
i = (i < 0)?i+slices.length:i;
|
||
|
j = (j < 0)?j+slices.length:j;
|
||
|
}
|
||
|
},
|
||
|
// utilities
|
||
|
_getLabel: function(number){
|
||
|
return dc.getLabel(number, this.opt.fixed, this.opt.precision);
|
||
|
}
|
||
|
});
|
||
|
});
|