source: [view]
dojo.provide("dojox.charting.plot2d.Spider");
dojo.experimental("dojox.charting.plot2d.Spider");
dojo.require("dojox.charting.Element");
dojo.require("dojox.charting.plot2d._PlotEvents");
dojo.require("dojox.charting.axis2d.common");
dojo.require("dojox.charting.plot2d.common");
dojo.require("dojox.charting.scaler.primitive");
dojo.require("dojox.lang.functional");
dojo.require("dojox.lang.utils");
dojo.require("dojox.gfx");
dojo.require("dojo.fx");
dojo.require("dojo.fx.easing");
dojo.require("dojox.gfx.fx");
(function(){
var df = dojox.lang.functional, du = dojox.lang.utils,
dc = dojox.charting.plot2d.common,
da = dojox.charting.axis2d.common,
g = dojox.gfx, m = g.matrix,
FUDGE_FACTOR = 0.2; // use to overlap fans
dojo.declare("dojox.charting.plot2d.Spider", [dojox.charting.Element, dojox.charting.plot2d._PlotEvents], {
// summary:
// The plot that represents a typical Spider chart.
defaultParams: {
labels: true,
ticks: false,
fixed: true,
precision: 1,
labelOffset: -10,
labelStyle: "default", // default/rows/auto
htmlLabels: true, // use HTML to draw labels
startAngle: -90, // start angle for slices in degrees
divisions: 3, // radius tick count
axisColor: "", // spider axis color
axisWidth: 0, // spider axis stroke width
spiderColor: "", // spider web color
spiderWidth: 0, // spider web stroke width
seriesWidth: 0, // plot border with
seriesFillAlpha: 0.2, // plot fill alpha
spiderOrigin: 0.16,
markerSize: 3, // radius of plot vertex (px)
spiderType: "polygon", //"circle"
animationType: dojo.fx.easing.backOut,
axisTickFont: "",
axisTickFontColor: "",
axisFont: "",
axisFontColor: ""
},
optionalParams: {
radius: 0,
font: "",
fontColor: ""
},
constructor: function(chart, kwArgs){
// summary:
// Create a Spider plot.
this.opt = dojo.clone(this.defaultParams);
du.updateWithObject(this.opt, kwArgs);
du.updateWithPattern(this.opt, kwArgs, this.optionalParams);
this.series = [];
this.dyn = [];
this.datas = {};
this.labelKey = [];
this.oldSeriePoints = {};
this.animations = {};
},
clear: function(){
// summary:
// Clear out all of the information tied to this plot.
// returns: dojox.charting.plot2d.Spider
// A reference to this plot for functional chaining.
this.dirty = true;
this.dyn = [];
this.series = [];
this.datas = {};
this.labelKey = [];
this.oldSeriePoints = {};
this.animations = {};
return this; // dojox.charting.plot2d.Spider
},
setAxis: function(axis){
// summary:
// Dummy method, since axes are irrelevant with a Spider chart.
// returns: dojox.charting.plot2d.Spider
// The reference to this plot for functional chaining.
return this; // dojox.charting.plot2d.Spider
},
addSeries: function(run){
// summary:
// Add a data series to this plot.
// run: dojox.charting.Series
// The series to be added.
// returns: dojox.charting.plot2d.Base
// A reference to this plot for functional chaining.
var matched = false;
this.series.push(run);
for(var key in run.data){
var val = run.data[key],
data = this.datas[key];
if(data){
data.vlist.push(val);
data.min = Math.min(data.min, val);
data.max = Math.max(data.max, val);
}else{
this.datas[key] = {min: val, max: val, vlist: [val]};
}
}
if (this.labelKey.length <= 0) {
for (var key in run.data) {
this.labelKey.push(key);
}
}
return this; // dojox.charting.plot2d.Base
},
getSeriesStats: function(){
// summary:
// Calculate the min/max on all attached series in both directions.
// returns: Object
// {hmin, hmax, vmin, vmax} min/max in both directions.
return dojox.charting.plot2d.common.collectSimpleStats(this.series);
},
calculateAxes: function(dim){
// summary:
// Stub function for running the axis calculations (depricated).
// dim: Object
// An object of the form { width, height }
// returns: dojox.charting.plot2d.Base
// A reference to this plot for functional chaining.
this.initializeScalers(dim, this.getSeriesStats());
return this; // dojox.charting.plot2d.Base
},
getRequiredColors: function(){
// summary:
// Get how many data series we have, so we know how many colors to use.
// returns: Number
// The number of colors needed.
return this.series.length; // Number
},
initializeScalers: function(dim, stats){
// summary:
// Initializes scalers using attached axes.
// dim: Object:
// Size of a plot area in pixels as {width, height}.
// stats: Object:
// Min/max of data in both directions as {hmin, hmax, vmin, vmax}.
// returns: dojox.charting.plot2d.Base
// A reference to this plot for functional chaining.
if(this._hAxis){
if(!this._hAxis.initialized()){
this._hAxis.calculate(stats.hmin, stats.hmax, dim.width);
}
this._hScaler = this._hAxis.getScaler();
}else{
this._hScaler = dojox.charting.scaler.primitive.buildScaler(stats.hmin, stats.hmax, dim.width);
}
if(this._vAxis){
if(!this._vAxis.initialized()){
this._vAxis.calculate(stats.vmin, stats.vmax, dim.height);
}
this._vScaler = this._vAxis.getScaler();
}else{
this._vScaler = dojox.charting.scaler.primitive.buildScaler(stats.vmin, stats.vmax, dim.height);
}
return this; // dojox.charting.plot2d.Base
},
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.Spider
// A reference to this plot for functional chaining.
if(!this.dirty){ return this; }
this.dirty = false;
this.cleanGroup();
var s = this.group, t = this.chart.theme;
this.resetEvents();
if(!this.series || !this.series.length){
return this;
}
// calculate the geometry
var o = this.opt, ta = t.axis,
rx = (dim.width - offsets.l - offsets.r) / 2,
ry = (dim.height - offsets.t - offsets.b) / 2,
r = Math.min(rx, ry),
axisTickFont = o.font || (ta.majorTick && ta.majorTick.font) || (ta.tick && ta.tick.font) || "normal normal normal 7pt Tahoma",
axisFont = o.axisFont || (ta.tick && ta.tick.titleFont) || "normal normal normal 11pt Tahoma",
axisTickFontColor = o.axisTickFontColor || (ta.majorTick && ta.majorTick.fontColor) || (ta.tick && ta.tick.fontColor) || "silver",
axisFontColor = o.axisFontColor || (ta.tick && ta.tick.titleFontColor) || "black",
axisColor = o.axisColor || (ta.tick && ta.tick.axisColor) || "silver",
spiderColor = o.spiderColor || (ta.tick && ta.tick.spiderColor) || "silver",
axisWidth = o.axisWidth || (ta.stroke && ta.stroke.width) || 2,
spiderWidth = o.spiderWidth || (ta.stroke && ta.stroke.width) || 2,
seriesWidth = o.seriesWidth || (ta.stroke && ta.stroke.width) || 2,
asize = g.normalizedLength(g.splitFontString(axisFont).size),
startAngle = m._degToRad(o.startAngle),
start = startAngle, step, filteredRun, slices, labels, shift, labelR,
outerPoints, innerPoints, divisionPoints, divisionRadius, labelPoints,
ro = o.spiderOrigin, dv = o.divisions >= 3 ? o.divisions : 3, ms = o.markerSize,
spt = o.spiderType, at = o.animationType, lboffset = o.labelOffset < -10 ? o.labelOffset : -10,
axisExtra = 0.2;
if(o.labels){
labels = dojo.map(this.series, function(s){
return s.name;
}, this);
shift = df.foldl1(df.map(labels, function(label, i){
var font = t.series.font;
return dojox.gfx._base._getTextBox(label, {
font: font
}).w;
}, this), "Math.max(a, b)") / 2;
r = Math.min(rx - 2 * shift, ry - asize) + lboffset;
labelR = r - lboffset;
}
if ("radius" in o) {
r = o.radius;
labelR = r - lboffset;
}
r /= (1+axisExtra);
var circle = {
cx: offsets.l + rx,
cy: offsets.t + ry,
r: r
};
for (var i = this.series.length - 1; i >= 0; i--) {
var serieEntry = this.series[i];
if (!this.dirty && !serieEntry.dirty) {
t.skip();
continue;
}
serieEntry.cleanGroup();
var run = serieEntry.data;
if (run !== null) {
var len = this._getObjectLength(run);
//construct connect points
if (!outerPoints || outerPoints.length <= 0) {
outerPoints = [], innerPoints = [], labelPoints = [];
this._buildPoints(outerPoints, len, circle, r, start, true);
this._buildPoints(innerPoints, len, circle, r*ro, start, true);
this._buildPoints(labelPoints, len, circle, labelR, start);
if(dv > 2){
divisionPoints = [], divisionRadius = [];
for (var j = 0; j < dv - 2; j++) {
divisionPoints[j] = [];
this._buildPoints(divisionPoints[j], len, circle, r*(ro + (1-ro)*(j+1)/(dv-1)), start, true);
divisionRadius[j] = r*(ro + (1-ro)*(j+1)/(dv-1));
}
}
}
}
}
//draw Spider
//axis
var axisGroup = s.createGroup(), axisStroke = {color: axisColor, width: axisWidth},
spiderStroke = {color: spiderColor, width: spiderWidth};
for (var j = outerPoints.length - 1; j >= 0; --j) {
var point = outerPoints[j],
st = {
x: point.x + (point.x - circle.cx) * axisExtra,
y: point.y + (point.y - circle.cy) * axisExtra
},
nd = {
x: point.x + (point.x - circle.cx) * axisExtra / 2,
y: point.y + (point.y - circle.cy) * axisExtra / 2
};
axisGroup.createLine({
x1: circle.cx,
y1: circle.cy,
x2: st.x,
y2: st.y
}).setStroke(axisStroke);
//arrow
this._drawArrow(axisGroup, st, nd, axisStroke);
}
// draw the label
var labelGroup = s.createGroup();
for (var j = labelPoints.length - 1; j >= 0; --j) {
var point = labelPoints[j],
fontWidth = dojox.gfx._base._getTextBox(this.labelKey[j], {font: axisFont}).w || 0,
render = this.opt.htmlLabels && dojox.gfx.renderer != "vml" ? "html" : "gfx";
elem = da.createText[render](this.chart, labelGroup, (!dojo._isBodyLtr() && render == "html") ? (point.x + fontWidth - dim.width) : point.x, point.y,
"middle", this.labelKey[j], axisFont, axisFontColor);
if (this.opt.htmlLabels) {
this.htmlElements.push(elem);
}
}
//spider web: polygon or circle
var spiderGroup = s.createGroup();
if(spt == "polygon"){
spiderGroup.createPolyline(outerPoints).setStroke(spiderStroke);
spiderGroup.createPolyline(innerPoints).setStroke(spiderStroke);
if (divisionPoints.length > 0) {
for (var j = divisionPoints.length - 1; j >= 0; --j) {
spiderGroup.createPolyline(divisionPoints[j]).setStroke(spiderStroke);
}
}
}else{//circle
var ccount = this._getObjectLength(this.datas);
spiderGroup.createCircle({cx: circle.cx, cy: circle.cy, r: r}).setStroke(spiderStroke);
spiderGroup.createCircle({cx: circle.cx, cy: circle.cy, r: r*ro}).setStroke(spiderStroke);
if (divisionRadius.length > 0) {
for (var j = divisionRadius.length - 1; j >= 0; --j) {
spiderGroup.createCircle({cx: circle.cx, cy: circle.cy, r: divisionRadius[j]}).setStroke(spiderStroke);
}
}
}
//text
var textGroup = s.createGroup(), len = this._getObjectLength(this.datas), k = 0;
for(var key in this.datas){
var data = this.datas[key], min = data.min, max = data.max, distance = max - min,
end = start + 2 * Math.PI * k / len;
for (var i = 0; i < dv; i++) {
var text = min + distance*i/(dv-1), point = this._getCoordinate(circle, r*(ro + (1-ro)*i/(dv-1)), end);
text = this._getLabel(text);
var fontWidth = dojox.gfx._base._getTextBox(text, {font: axisTickFont}).w || 0,
render = this.opt.htmlLabels && dojox.gfx.renderer != "vml" ? "html" : "gfx";
if (this.opt.htmlLabels) {
this.htmlElements.push(da.createText[render]
(this.chart, textGroup, (!dojo._isBodyLtr() && render == "html") ? (point.x + fontWidth - dim.width) : point.x, point.y,
"start", text, axisTickFont, axisTickFontColor));
}
}
k++;
}
//draw series (animation)
this.chart.seriesShapes = {};
var animationConnections = [];
for (var i = this.series.length - 1; i >= 0; i--) {
var serieEntry = this.series[i], run = serieEntry.data;
if (run !== null) {
//series polygon
var seriePoints = [], k = 0, tipData = [];
for(var key in run){
var data = this.datas[key], min = data.min, max = data.max, distance = max - min,
entry = run[key], end = start + 2 * Math.PI * k / len,
point = this._getCoordinate(circle, r*(ro + (1-ro)*(entry-min)/distance), end);
seriePoints.push(point);
tipData.push({sname: serieEntry.name, key: key, data: entry});
k++;
}
seriePoints[seriePoints.length] = seriePoints[0];
tipData[tipData.length] = tipData[0];
var polygonBoundRect = this._getBoundary(seriePoints),
theme = t.next("spider", [o, serieEntry]), ts = serieEntry.group,
f = g.normalizeColor(theme.series.fill), sk = {color: theme.series.fill, width: seriesWidth};
f.a = o.seriesFillAlpha;
serieEntry.dyn = {fill: f, stroke: sk};
var osps = this.oldSeriePoints[serieEntry.name];
var cs = this._createSeriesEntry(ts, (osps || innerPoints), seriePoints, f, sk, r, ro, ms, at);
this.chart.seriesShapes[serieEntry.name] = cs;
this.oldSeriePoints[serieEntry.name] = seriePoints;
var po = {
element: "spider_poly",
index: i,
id: "spider_poly_"+serieEntry.name,
run: serieEntry,
plot: this,
shape: cs.poly,
parent: ts,
brect: polygonBoundRect,
cx: circle.cx,
cy: circle.cy,
cr: r,
f: f,
s: s
};
this._connectEvents(po);
var so = {
element: "spider_plot",
index: i,
id: "spider_plot_"+serieEntry.name,
run: serieEntry,
plot: this,
shape: serieEntry.group
};
this._connectEvents(so);
dojo.forEach(cs.circles, function(c, i){
var shape = c.getShape(),
co = {
element: "spider_circle",
index: i,
id: "spider_circle_"+serieEntry.name+i,
run: serieEntry,
plot: this,
shape: c,
parent: ts,
tdata: tipData[i],
cx: seriePoints[i].x,
cy: seriePoints[i].y,
f: f,
s: s
};
this._connectEvents(co);
}, this);
}
}
return this; // dojox.charting.plot2d.Spider
},
_createSeriesEntry: function(ts, osps, sps, f, sk, r, ro, ms, at){
//polygon
var spoly = ts.createPolyline(osps).setFill(f).setStroke(sk), scircle = [];
for (var j = 0; j < osps.length; j++) {
var point = osps[j], cr = ms;
var circle = ts.createCircle({cx: point.x, cy: point.y, r: cr}).setFill(f).setStroke(sk);
scircle.push(circle);
}
var anims = dojo.map(sps, function(np, j){
// create animation
var sp = osps[j],
anim = new dojo._Animation({
duration: 1000,
easing: at,
curve: [sp.y, np.y]
});
var spl = spoly, sc = scircle[j];
dojo.connect(anim, "onAnimate", function(y){
//apply poly
var pshape = spl.getShape();
pshape.points[j].y = y;
spl.setShape(pshape);
//apply circle
var cshape = sc.getShape();
cshape.cy = y;
sc.setShape(cshape);
});
return anim;
});
var anims1 = dojo.map(sps, function(np, j){
// create animation
var sp = osps[j],
anim = new dojo._Animation({
duration: 1000,
easing: at,
curve: [sp.x, np.x]
});
var spl = spoly, sc = scircle[j];
dojo.connect(anim, "onAnimate", function(x){
//apply poly
var pshape = spl.getShape();
pshape.points[j].x = x;
spl.setShape(pshape);
//apply circle
var cshape = sc.getShape();
cshape.cx = x;
sc.setShape(cshape);
});
return anim;
});
var masterAnimation = dojo.fx.combine(anims.concat(anims1)); //dojo.fx.chain(anims);
masterAnimation.play();
return {group :ts, poly: spoly, circles: scircle};