source: Dev/trunk/src/client/dojox/charting/plot2d/Pie.js @ 485

Last change on this file since 485 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 17.6 KB
Line 
1define(["dojo/_base/lang", "dojo/_base/array" ,"dojo/_base/declare",
2                "./Base", "./_PlotEvents", "./common",
3                "dojox/gfx", "dojox/gfx/matrix", "dojox/lang/functional", "dojox/lang/utils","dojo/has"],
4        function(lang, arr, declare, Base, PlotEvents, dc, g, m, df, du, has){
5
6        /*=====
7        declare("dojox.charting.plot2d.__PieCtorArgs", dojox.charting.plot2d.__DefaultCtorArgs, {
8                // summary:
9                //              Specialized keyword arguments object for use in defining parameters on a Pie chart.
10       
11                // labels: Boolean?
12                //              Whether or not to draw labels for each pie slice.  Default is true.
13                labels:                 true,
14       
15                // ticks: Boolean?
16                //              Whether or not to draw ticks to labels within each slice. Default is false.
17                ticks:                  false,
18       
19                // fixed: Boolean?
20                //              Whether a fixed precision must be applied to data values for display. Default is true.
21                fixed:                  true,
22       
23                // precision: Number?
24                //              The precision at which to round data values for display. Default is 0.
25                precision:              1,
26       
27                // labelOffset: Number?
28                //              The amount in pixels by which to offset labels.  Default is 20.
29                labelOffset:    20,
30       
31                // labelStyle: String?
32                //              Options as to where to draw labels.  Values include "default", and "columns".   Default is "default".
33                labelStyle:             "default",      // default/columns
34               
35                // omitLabels: Boolean?
36                //              Whether labels of slices small to the point of not being visible are omitted.   Default false.
37                omitLabels: false,
38               
39                // htmlLabels: Boolean?
40                //              Whether or not to use HTML to render slice labels. Default is true.
41                htmlLabels:             true,
42       
43                // radGrad: String?
44                //              The type of radial gradient to use in rendering.  Default is "native".
45                radGrad:        "native",
46       
47                // fanSize: Number?
48                //              The amount for a radial gradient.  Default is 5.
49                fanSize:                5,
50       
51                // startAngle: Number?
52                //              Where to being rendering gradients in slices, in degrees.  Default is 0.
53                startAngle:     0,
54       
55                // radius: Number?
56                //              The size of the radial gradient.  Default is 0.
57                radius:         0,
58
59                // shadow: dojox.gfx.Stroke?
60                //              An optional stroke to use to draw any shadows for a series on a plot.
61                shadow:         {},
62
63                // fill: dojox.gfx.Fill?
64                //              Any fill to be used for elements on the plot.
65                fill:           {},
66
67                // filter: dojox.gfx.Filter?
68                //              An SVG filter to be used for elements on the plot. gfx SVG renderer must be used and dojox/gfx/svgext must
69                //              be required for this to work.
70                filter:         {},
71
72                // styleFunc: Function?
73                //              A function that returns a styling object for the a given data item.
74                styleFunc:      null
75        });
76        =====*/
77
78        var FUDGE_FACTOR = 0.2; // use to overlap fans
79
80        return declare("dojox.charting.plot2d.Pie", [Base, PlotEvents], {
81                // summary:
82                //              The plot that represents a typical pie chart.
83                defaultParams: {
84                        labels:                 true,
85                        ticks:                  false,
86                        fixed:                  true,
87                        precision:              1,
88                        labelOffset:    20,
89                        labelStyle:             "default",      // default/columns
90                        htmlLabels:             true,           // use HTML to draw labels
91                        radGrad:        "native",       // or "linear", or "fan"
92                        fanSize:                5,                      // maximum fan size in degrees
93                        startAngle:     0                       // start angle for slices in degrees
94                },
95                optionalParams: {
96                        radius:         0,
97                        omitLabels: false,
98                        // theme components
99                        stroke:         {},
100                        outline:        {},
101                        shadow:         {},
102                        fill:           {},
103                        filter:     {},
104                        styleFunc:      null,
105                        font:           "",
106                        fontColor:      "",
107                        labelWiring: {}
108                },
109
110                constructor: function(chart, kwArgs){
111                        // summary:
112                        //              Create a pie plot.
113                        this.opt = lang.clone(this.defaultParams);
114                        du.updateWithObject(this.opt, kwArgs);
115                        du.updateWithPattern(this.opt, kwArgs, this.optionalParams);
116                        this.axes = [];
117                        this.run = null;
118                        this.dyn = [];
119                },
120                clear: function(){
121                        // summary:
122                        //              Clear out all of the information tied to this plot.
123                        // returns: dojox/charting/plot2d/Pie
124                        //              A reference to this plot for functional chaining.
125                        this.inherited(arguments);
126                        this.dyn = [];
127                        this.run = null;
128                        return this;    //      dojox/charting/plot2d/Pie
129                },
130                setAxis: function(axis){
131                        // summary:
132                        //              Dummy method, since axes are irrelevant with a Pie chart.
133                        // returns: dojox/charting/plot2d/Pie
134                        //              The reference to this plot for functional chaining.
135                        return this;    //      dojox/charting/plot2d/Pie
136                },
137                addSeries: function(run){
138                        // summary:
139                        //              Add a series of data to this plot.
140                        // returns: dojox/charting/plot2d/Pie
141                        //              The reference to this plot for functional chaining.
142                        this.run = run;
143                        return this;    //      dojox/charting/plot2d/Pie
144                },
145                getSeriesStats: function(){
146                        // summary:
147                        //              Returns default stats (irrelevant for this type of plot).
148                        // returns: Object
149                        //              {hmin, hmax, vmin, vmax} min/max in both directions.
150                        return lang.delegate(dc.defaultStats); // Object
151                },
152                getRequiredColors: function(){
153                        // summary:
154                        //              Return the number of colors needed to draw this plot.
155                        return this.run ? this.run.data.length : 0;
156                },
157                render: function(dim, offsets){
158                        // summary:
159                        //              Render the plot on the chart.
160                        // dim: Object
161                        //              An object of the form { width, height }.
162                        // offsets: Object
163                        //              An object of the form { l, r, t, b }.
164                        // returns: dojox/charting/plot2d/Pie
165                        //              A reference to this plot for functional chaining.
166                        if(!this.dirty){ return this; }
167                        this.resetEvents();
168                        this.dirty = false;
169                        this._eventSeries = {};
170                        this.cleanGroup();
171                        var s = this.group, t = this.chart.theme;
172
173                        if(!this.run || !this.run.data.length){
174                                return this;
175                        }
176
177                        // calculate the geometry
178                        var rx = (dim.width  - offsets.l - offsets.r) / 2,
179                                ry = (dim.height - offsets.t - offsets.b) / 2,
180                                r  = Math.min(rx, ry),
181                                labelFont = "font" in this.opt ? this.opt.font : t.series.font,
182                                size,
183                                startAngle = m._degToRad(this.opt.startAngle),
184                                start = startAngle, filteredRun, slices, labels, shift, labelR,
185                                run = this.run.data,
186                                events = this.events();
187
188                        this.dyn = [];
189
190                        if("radius" in this.opt){
191                                r = this.opt.radius;
192                                labelR = r - this.opt.labelOffset;
193                        }
194                        var     circle = {
195                                        cx: offsets.l + rx,
196                                        cy: offsets.t + ry,
197                                        r:  r
198                                };
199
200                        // draw shadow
201                        if(this.opt.shadow || t.shadow){
202                                var shadow = this.opt.shadow || t.shadow;
203                                var scircle = lang.clone(circle);
204                                scircle.cx += shadow.dx;
205                                scircle.cy += shadow.dy;
206                                s.createCircle(scircle).setFill(shadow.color).setStroke(shadow);
207                        }
208                        if(s.setFilter && (this.opt.filter || t.filter)){
209                                s.createCircle(circle).setFill(t.series.stroke).setFilter(this.opt.filter || t.filter);
210                        }
211
212                        if(typeof run[0] == "number"){
213                                filteredRun = df.map(run, "x ? Math.max(x, 0) : 0");
214                                if(df.every(filteredRun, "<= 0")){
215                                        s.createCircle(circle).setStroke(t.series.stroke);
216                                        this.dyn = arr.map(filteredRun, function(){
217                                                return {  };
218                                        });
219                                        return this;
220                                }else{
221                                        slices = df.map(filteredRun, "/this", df.foldl(filteredRun, "+", 0));
222                                        if(this.opt.labels){
223                                                labels = arr.map(slices, function(x){
224                                                        return x > 0 ? this._getLabel(x * 100) + "%" : "";
225                                                }, this);
226                                        }
227                                }
228                        }else{
229                                filteredRun = df.map(run, "x ? Math.max(x.y, 0) : 0");
230                                if(df.every(filteredRun, "<= 0")){
231                                        s.createCircle(circle).setStroke(t.series.stroke);
232                                        this.dyn = arr.map(filteredRun, function(){
233                                                return {  };
234                                        });
235                                        return this;
236                                }else{
237                                        slices = df.map(filteredRun, "/this", df.foldl(filteredRun, "+", 0));
238                                        if(this.opt.labels){
239                                                labels = arr.map(slices, function(x, i){
240                                                        if(x < 0){ return ""; }
241                                                        var v = run[i];
242                                                        return "text" in v ? v.text : this._getLabel(x * 100) + "%";
243                                                }, this);
244                                        }
245                                }
246                        }
247                        var themes = df.map(run, function(v, i){
248                                var tMixin = [this.opt, this.run];
249                                if(v !== null && typeof v != "number"){
250                                        tMixin.push(v);
251                                }
252                                if(this.opt.styleFunc){
253                                        tMixin.push(this.opt.styleFunc(v));
254                                }
255                                return t.next("slice", tMixin, true);
256                        }, this);
257
258                        if(this.opt.labels){
259                                size = labelFont ? g.normalizedLength(g.splitFontString(labelFont).size) : 0;
260                                shift = df.foldl1(df.map(labels, function(label, i){
261                                        var font = themes[i].series.font;
262                                        return g._base._getTextBox(label, {font: font}).w;
263                                }, this), "Math.max(a, b)") / 2;
264                                if(this.opt.labelOffset < 0){
265                                        r = Math.min(rx - 2 * shift, ry - size) + this.opt.labelOffset;
266                                }
267                                labelR = r - this.opt.labelOffset;
268                        }
269
270                        // draw slices
271                        var eventSeries = new Array(slices.length);
272                        arr.some(slices, function(slice, i){
273                                if(slice < 0){
274                                        // degenerated slice
275                                        return false;   // continue
276                                }
277                                if(slice == 0){
278                                  this.dyn.push({fill: null, stroke: null});
279                                  return false;
280                                }
281                                var v = run[i], theme = themes[i], specialFill, o;
282                                if(slice >= 1){
283                                        // whole pie
284                                        specialFill = this._plotFill(theme.series.fill, dim, offsets);
285                                        specialFill = this._shapeFill(specialFill,
286                                                {
287                                                        x: circle.cx - circle.r, y: circle.cy - circle.r,
288                                                        width: 2 * circle.r, height: 2 * circle.r
289                                                });
290                                        specialFill = this._pseudoRadialFill(specialFill, {x: circle.cx, y: circle.cy}, circle.r);
291                                        var shape = s.createCircle(circle).setFill(specialFill).setStroke(theme.series.stroke);
292                                        this.dyn.push({fill: specialFill, stroke: theme.series.stroke});
293
294                                        if(events){
295                                                o = {
296                                                        element: "slice",
297                                                        index:   i,
298                                                        run:     this.run,
299                                                        shape:   shape,
300                                                        x:       i,
301                                                        y:       typeof v == "number" ? v : v.y,
302                                                        cx:      circle.cx,
303                                                        cy:      circle.cy,
304                                                        cr:      r
305                                                };
306                                                this._connectEvents(o);
307                                                eventSeries[i] = o;
308                                        }
309
310                                        return false;   // we continue because we want to collect null data points for legend
311                                }
312                                // calculate the geometry of the slice
313                                var end = start + slice * 2 * Math.PI;
314                                if(i + 1 == slices.length){
315                                        end = startAngle + 2 * Math.PI;
316                                }
317                                var     step = end - start,
318                                        x1 = circle.cx + r * Math.cos(start),
319                                        y1 = circle.cy + r * Math.sin(start),
320                                        x2 = circle.cx + r * Math.cos(end),
321                                        y2 = circle.cy + r * Math.sin(end);
322                                // draw the slice
323                                var fanSize = m._degToRad(this.opt.fanSize);
324                                if(theme.series.fill && theme.series.fill.type === "radial" && this.opt.radGrad === "fan" && step > fanSize){
325                                        var group = s.createGroup(), nfans = Math.ceil(step / fanSize), delta = step / nfans;
326                                        specialFill = this._shapeFill(theme.series.fill,
327                                                {x: circle.cx - circle.r, y: circle.cy - circle.r, width: 2 * circle.r, height: 2 * circle.r});
328                                        for(var j = 0; j < nfans; ++j){
329                                                var fansx = j == 0 ? x1 : circle.cx + r * Math.cos(start + (j - FUDGE_FACTOR) * delta),
330                                                        fansy = j == 0 ? y1 : circle.cy + r * Math.sin(start + (j - FUDGE_FACTOR) * delta),
331                                                        fanex = j == nfans - 1 ? x2 : circle.cx + r * Math.cos(start + (j + 1 + FUDGE_FACTOR) * delta),
332                                                        faney = j == nfans - 1 ? y2 : circle.cy + r * Math.sin(start + (j + 1 + FUDGE_FACTOR) * delta);
333                                                group.createPath().
334                                                                moveTo(circle.cx, circle.cy).
335                                                                lineTo(fansx, fansy).
336                                                                arcTo(r, r, 0, delta > Math.PI, true, fanex, faney).
337                                                                lineTo(circle.cx, circle.cy).
338                                                                closePath().
339                                                                setFill(this._pseudoRadialFill(specialFill, {x: circle.cx, y: circle.cy}, r, start + (j + 0.5) * delta, start + (j + 0.5) * delta));
340                                        }
341                                        group.createPath().
342                                                moveTo(circle.cx, circle.cy).
343                                                lineTo(x1, y1).
344                                                arcTo(r, r, 0, step > Math.PI, true, x2, y2).
345                                                lineTo(circle.cx, circle.cy).
346                                                closePath().
347                                                setStroke(theme.series.stroke);
348                                        shape = group;
349                                }else{
350                                        shape = s.createPath().
351                                                moveTo(circle.cx, circle.cy).
352                                                lineTo(x1, y1).
353                                                arcTo(r, r, 0, step > Math.PI, true, x2, y2).
354                                                lineTo(circle.cx, circle.cy).
355                                                closePath().
356                                                setStroke(theme.series.stroke);
357                                        specialFill = theme.series.fill;
358                                        if(specialFill && specialFill.type === "radial"){
359                                                specialFill = this._shapeFill(specialFill, {x: circle.cx - circle.r, y: circle.cy - circle.r, width: 2 * circle.r, height: 2 * circle.r});
360                                                if(this.opt.radGrad === "linear"){
361                                                        specialFill = this._pseudoRadialFill(specialFill, {x: circle.cx, y: circle.cy}, r, start, end);
362                                                }
363                                        }else if(specialFill && specialFill.type === "linear"){
364                                                specialFill = this._plotFill(specialFill, dim, offsets);
365                                                specialFill = this._shapeFill(specialFill, shape.getBoundingBox());
366                                        }
367                                        shape.setFill(specialFill);
368                                }
369                                this.dyn.push({fill: specialFill, stroke: theme.series.stroke});
370
371                                if(events){
372                                        o = {
373                                                element: "slice",
374                                                index:   i,
375                                                run:     this.run,
376                                                shape:   shape,
377                                                x:       i,
378                                                y:       typeof v == "number" ? v : v.y,
379                                                cx:      circle.cx,
380                                                cy:      circle.cy,
381                                                cr:      r
382                                        };
383                                        this._connectEvents(o);
384                                        eventSeries[i] = o;
385                                }
386
387                                start = end;
388
389                                return false;   // continue
390                        }, this);
391                        // draw labels
392                        if(this.opt.labels){
393                                var isRtl = has("dojo-bidi") && this.chart.isRightToLeft();
394                                if(this.opt.labelStyle == "default"){ // inside or outside based on labelOffset
395                                        start = startAngle;
396                                        arr.some(slices, function(slice, i){
397                                                if(slice <= 0){
398                                                        // degenerated slice
399                                                        return false;   // continue
400                                                }
401                                                var theme = themes[i];
402                                                if(slice >= 1){
403                                                        // whole pie
404                                                        this.renderLabel(s, circle.cx, circle.cy + size / 2, labels[i], theme, this.opt.labelOffset > 0);
405                                                        return true;    // stop iteration
406                                                }
407                                                // calculate the geometry of the slice
408                                                var end = start + slice * 2 * Math.PI;
409                                                if(i + 1 == slices.length){
410                                                        end = startAngle + 2 * Math.PI;
411                                                }
412                                                if(this.opt.omitLabels && end-start < 0.001){
413                                                        return false;   // continue
414                                                }
415                                                var     labelAngle = (start + end) / 2,
416                                                        x = circle.cx + labelR * Math.cos(labelAngle),
417                                                        y = circle.cy + labelR * Math.sin(labelAngle) + size / 2;
418                                                // draw the label
419                                                this.renderLabel(s, isRtl ? dim.width - x : x, y, labels[i], theme, this.opt.labelOffset > 0);
420                                                start = end;
421                                                return false;   // continue
422                                        }, this);
423                                }else if(this.opt.labelStyle == "columns"){
424                                        start = startAngle;
425                                        var omitLabels = this.opt.omitLabels;
426                                        //calculate label angles
427                                        var labeledSlices = [];
428                                        arr.forEach(slices, function(slice, i){
429                                                var end = start + slice * 2 * Math.PI;
430                                                if(i + 1 == slices.length){
431                                                        end = startAngle + 2 * Math.PI;
432                                                }
433                                                var labelAngle = (start + end) / 2;
434                                                labeledSlices.push({
435                                                        angle: labelAngle,
436                                                        left: Math.cos(labelAngle) < 0,
437                                                        theme: themes[i],
438                                                        index: i,
439                                                        omit: omitLabels?end - start < 0.001:false
440                                                });
441                                                start = end;
442                                        });
443                                        //calculate label radius to each slice
444                                        var labelHeight = g._base._getTextBox("a",{ font: labelFont }).h;
445                                        this._getProperLabelRadius(labeledSlices, labelHeight, circle.r * 1.1);
446                                        //draw label and wiring
447                                        arr.forEach(labeledSlices, function(slice, i){
448                                                if(!slice.omit){
449                                                        var leftColumn = circle.cx - circle.r * 2,
450                                                                rightColumn = circle.cx + circle.r * 2,
451                                                                labelWidth = g._base._getTextBox(labels[i], {font: slice.theme.series.font}).w,
452                                                                x = circle.cx + slice.labelR * Math.cos(slice.angle),
453                                                                y = circle.cy + slice.labelR * Math.sin(slice.angle),
454                                                                jointX = (slice.left) ? (leftColumn + labelWidth) : (rightColumn - labelWidth),
455                                                                labelX = (slice.left) ? leftColumn : jointX;
456                                                        var wiring = s.createPath().moveTo(circle.cx + circle.r * Math.cos(slice.angle), circle.cy + circle.r * Math.sin(slice.angle));
457                                                        if(Math.abs(slice.labelR * Math.cos(slice.angle)) < circle.r * 2 - labelWidth){
458                                                                wiring.lineTo(x, y);
459                                                        }
460                                                        wiring.lineTo(jointX, y).setStroke(slice.theme.series.labelWiring);
461                                                        this.renderLabel(s, isRtl ? dim.width - labelWidth - labelX : labelX, y, labels[i], slice.theme, false, "left");
462                                                }
463                                        },this);
464                                }
465                        }
466                        // post-process events to restore the original indexing
467                        var esi = 0;
468                        this._eventSeries[this.run.name] = df.map(run, function(v){
469                                return v <= 0 ? null : eventSeries[esi++];
470                        });
471                        // chart mirroring starts
472                        if(has("dojo-bidi")){
473                                this._checkOrientation(this.group, dim, offsets);
474                        }
475                        // chart mirroring ends
476                        return this;    //      dojox/charting/plot2d/Pie
477                },
478                _getProperLabelRadius: function(slices, labelHeight, minRidius){
479                        var leftCenterSlice, rightCenterSlice,
480                                leftMinSIN = 1, rightMinSIN = 1;
481                        if(slices.length == 1){
482                                slices[0].labelR = minRidius;
483                                return;
484                        }
485                        for(var i = 0; i < slices.length; i++){
486                                var tempSIN = Math.abs(Math.sin(slices[i].angle));
487                                if(slices[i].left){
488                                        if(leftMinSIN >= tempSIN){
489                                                leftMinSIN = tempSIN;
490                                                leftCenterSlice = slices[i];
491                                        }
492                                }else{
493                                        if(rightMinSIN >= tempSIN){
494                                                rightMinSIN = tempSIN;
495                                                rightCenterSlice = slices[i];
496                                        }
497                                }
498                        }
499                        leftCenterSlice.labelR = rightCenterSlice.labelR = minRidius;
500                        this._calculateLabelR(leftCenterSlice, slices, labelHeight);
501                        this._calculateLabelR(rightCenterSlice, slices, labelHeight);
502                },
503                _calculateLabelR: function(firstSlice, slices, labelHeight){
504                        var i = firstSlice.index,length = slices.length,
505                                currentLabelR = firstSlice.labelR, nextLabelR;
506                        while(!(slices[i%length].left ^ slices[(i+1)%length].left)){
507                                if(!slices[(i + 1) % length].omit){
508                                        nextLabelR = (Math.sin(slices[i % length].angle) * currentLabelR + ((slices[i % length].left) ? (-labelHeight) : labelHeight)) /
509                                        Math.sin(slices[(i + 1) % length].angle);
510                                        currentLabelR = (nextLabelR < firstSlice.labelR) ? firstSlice.labelR : nextLabelR;
511                                        slices[(i + 1) % length].labelR = currentLabelR;
512                                }
513                                i++;
514                        }
515                        i = firstSlice.index;
516                        var j = (i == 0)?length-1 : i - 1;
517                        while(!(slices[i].left ^ slices[j].left)){
518                                if(!slices[j].omit){
519                                        nextLabelR = (Math.sin(slices[i].angle) * currentLabelR + ((slices[i].left) ? labelHeight : (-labelHeight))) /
520                                        Math.sin(slices[j].angle);
521                                        currentLabelR = (nextLabelR < firstSlice.labelR) ? firstSlice.labelR : nextLabelR;
522                                        slices[j].labelR = currentLabelR;
523                                }
524                                i--;j--;
525                                i = (i < 0)?i+slices.length:i;
526                                j = (j < 0)?j+slices.length:j;
527                        }
528                }
529        });
530});
Note: See TracBrowser for help on using the repository browser.