5 Handles laying out data on to a virtual canvas square canvas between 0.0
6 and 1.0. If you want to add new chart/plot types such as point plots,
7 you need to add them here.
11 Copyright 2005,2006 (c) Alastair Tse <alastair^liquidx.net>
12 For use under the BSD license. <http://www.liquidx.net/plotkit>
17 if (typeof(PlotKit
.Base
) == 'undefined')
23 throw "PlotKit.Layout depends on MochiKit.{Base,Color,DOM,Format} and PlotKit.Base"
26 // --------------------------------------------------------------------
27 // Start of Layout definition
28 // --------------------------------------------------------------------
30 if (typeof(PlotKit
.Layout
) == 'undefined') {
34 PlotKit
.Layout
.NAME
= "PlotKit.Layout";
35 PlotKit
.Layout
.VERSION
= PlotKit
.VERSION
;
37 PlotKit
.Layout
.__repr__
= function() {
38 return "[" + this.NAME
+ " " + this.VERSION
+ "]";
41 PlotKit
.Layout
.toString
= function() {
42 return this.__repr__();
45 PlotKit
.Layout
.valid_styles
= ["bar", "line", "pie", "point"];
47 // --------------------------------------------------------------------
48 // Start of Layout definition
49 // --------------------------------------------------------------------
51 PlotKit
.Layout
= function(style
, options
) {
54 "barWidthFillFraction": 0.75,
55 "barOrientation": "vertical",
56 "xOriginIsZero": true,
57 "yOriginIsZero": true,
58 "xAxis": null, // [xmin, xmax]
59 "yAxis": null, // [ymin, ymax]
60 "xTicks": null, // [{label: "somelabel", v: value}, ..] (label opt.)
61 "yTicks": null, // [{label: "somelabel", v: value}, ..] (label opt.)
69 // valid external options : TODO: input verification
71 MochiKit
.Base
.update(this.options
, options
? options
: {});
73 // externally visible states
74 // overriden if xAxis and yAxis are set in options
75 if (!MochiKit
.Base
.isUndefinedOrNull(this.options
.xAxis
)) {
76 this.minxval
= this.options
.xAxis
[0];
77 this.maxxval
= this.options
.xAxis
[1];
78 this.xscale
= this.maxxval
- this.minxval
;
83 this.xscale
= null; // val -> pos factor (eg, xval * xscale = xpos)
86 if (!MochiKit
.Base
.isUndefinedOrNull(this.options
.yAxis
)) {
87 this.minyval
= this.options
.yAxis
[0];
88 this.maxyval
= this.options
.yAxis
[1];
89 this.yscale
= this.maxyval
- this.minyval
;
97 this.bars
= new Array(); // array of bars to plot for bar charts
98 this.points
= new Array(); // array of points to plot for line plots
99 this.slices
= new Array(); // array of slices to draw for pie charts
101 this.xticks
= new Array();
102 this.yticks
= new Array();
105 this.datasets
= new Array();
110 this.hitTestCache
= {x2maxy
: null};
114 // --------------------------------------------------------------------
115 // Dataset Manipulation
116 // --------------------------------------------------------------------
119 PlotKit
.Layout
.prototype.addDataset
= function(setname
, set_xy
) {
120 this.datasets
[setname
] = set_xy
;
123 PlotKit
.Layout
.prototype.removeDataset
= function(setname
, set_xy
) {
124 delete this.datasets
[setname
];
127 PlotKit
.Layout
.prototype.addDatasetFromTable
= function(name
, tableElement
, xcol
, ycol
, lcol
) {
128 var isNil
= MochiKit
.Base
.isUndefinedOrNull
;
129 var scrapeText
= MochiKit
.DOM
.scrapeText
;
130 var strip
= MochiKit
.Format
.strip
;
139 var rows
= tableElement
.tBodies
[0].rows
;
140 var data
= new Array();
141 var labels
= new Array();
144 for (var i
= 0; i
< rows
.length
; i
++) {
145 data
.push([parseFloat(strip(scrapeText(rows
[i
].cells
[xcol
]))),
146 parseFloat(strip(scrapeText(rows
[i
].cells
[ycol
])))]);
148 labels
.push({v
: parseFloat(strip(scrapeText(rows
[i
].cells
[xcol
]))),
149 label
: strip(scrapeText(rows
[i
].cells
[lcol
]))});
152 this.addDataset(name
, data
);
154 this.options
.xTicks
= labels
;
161 // --------------------------------------------------------------------
162 // Evaluates the layout for the current data and style.
163 // --------------------------------------------------------------------
165 PlotKit
.Layout
.prototype.evaluate
= function() {
166 this._evaluateLimits();
167 this._evaluateScales();
168 if (this.style
== "bar") {
169 if (this.options
.barOrientation
== "horizontal") {
170 this._evaluateHorizBarCharts();
173 this._evaluateBarCharts();
175 this._evaluateBarTicks();
177 else if (this.style
== "line") {
178 this._evaluateLineCharts();
179 this._evaluateLineTicks();
181 else if (this.style
== "pie") {
182 this._evaluatePieCharts();
183 this._evaluatePieTicks();
189 // Given the fractional x, y positions, report the corresponding
191 PlotKit
.Layout
.prototype.hitTest
= function(x
, y
) {
192 // TODO: make this more efficient with better datastructures
193 // for this.bars, this.points and this.slices
195 var f
= MochiKit
.Format
.twoDigitFloat
;
197 if ((this.style
== "bar") && this.bars
&& (this.bars
.length
> 0)) {
198 for (var i
= 0; i
< this.bars
.length
; i
++) {
199 var bar
= this.bars
[i
];
200 if ((x
>= bar
.x
) && (x
<= bar
.x
+ bar
.w
)
201 && (y
>= bar
.y
) && (y
- bar
.y
<= bar
.h
))
206 else if (this.style
== "line") {
207 if (this.hitTestCache
.x2maxy
== null) {
208 this._regenerateHitTestCache();
211 // 1. find the xvalues that equal or closest to the give x
212 var xval
= x
/ this.xscale
;
213 var xvalues
= this.hitTestCache
.xvalues
;
217 for (var i
= 1; i
< xvalues
.length
; i
++) {
218 if (xvalues
[i
] > xval
) {
219 xbefore
= xvalues
[i
-1];
225 if ((xbefore
!= null)) {
226 var ybefore
= this.hitTestCache
.x2maxy
[xbefore
];
227 var yafter
= this.hitTestCache
.x2maxy
[xafter
];
228 var yval
= (1.0 - y
)/this.yscale
;
230 // interpolate whether we will fall inside or outside
231 var gradient
= (yafter
- ybefore
) / (xafter
- xbefore
);
232 var projmaxy
= ybefore
+ gradient
* (xval
- xbefore
);
233 if (projmaxy
>= yval
) {
234 // inside the highest curve (roughly)
235 var obj
= {xval
: xval
, yval
: yval
,
236 xafter
: xafter
, yafter
: yafter
,
237 xbefore
: xbefore
, ybefore
: ybefore
,
245 else if (this.style
== "pie") {
246 var dist
= Math
.sqrt((y
-0.5)*(y
-0.5) + (x
-0.5)*(x
-0.5));
247 if (dist
> this.options
.pieRadius
)
250 // TODO: actually doesn't work if we don't know how the Canvas
251 // lays it out, need to fix!
252 var angle
= Math
.atan2(y
- 0.5, x
- 0.5) - Math
.PI
/2;
253 for (var i
= 0; i
< this.slices
.length
; i
++) {
254 var slice
= this.slices
[i
];
255 if (slice
.startAngle
< angle
&& slice
.endAngle
>= angle
)
263 // Reports valid position rectangle for X value (only valid for bar charts)
264 PlotKit
.Layout
.prototype.rectForX
= function(x
) {
268 // Reports valid angles through which X value encloses (only valid for pie charts)
269 PlotKit
.Layout
.prototype.angleRangeForX
= function(x
) {
273 // --------------------------------------------------------------------
274 // START Internal Functions
275 // --------------------------------------------------------------------
277 PlotKit
.Layout
.prototype._evaluateLimits
= function() {
278 // take all values from all datasets and find max and min
279 var map
= PlotKit
.Base
.map
;
280 var items
= PlotKit
.Base
.items
;
281 var itemgetter
= MochiKit
.Base
.itemgetter
;
282 var collapse
= PlotKit
.Base
.collapse
;
283 var listMin
= MochiKit
.Base
.listMin
;
284 var listMax
= MochiKit
.Base
.listMax
;
285 var isNil
= MochiKit
.Base
.isUndefinedOrNull
;
288 var all
= collapse(map(itemgetter(1), items(this.datasets
)));
289 if (isNil(this.options
.xAxis
)) {
290 if (this.options
.xOriginIsZero
)
293 this.minxval
= listMin(map(parseFloat
, map(itemgetter(0), all
)));
295 this.maxxval
= listMax(map(parseFloat
, map(itemgetter(0), all
)));
298 this.minxval
= this.options
.xAxis
[0];
299 this.maxxval
= this.options
.xAxis
[1];
300 this.xscale
= this.maxval
- this.minxval
;
303 if (isNil(this.options
.yAxis
)) {
304 if (this.options
.yOriginIsZero
)
307 this.minyval
= listMin(map(parseFloat
, map(itemgetter(1), all
)));
309 this.maxyval
= listMax(map(parseFloat
, map(itemgetter(1), all
)));
312 this.minyval
= this.options
.yAxis
[0];
313 this.maxyval
= this.options
.yAxis
[1];
314 this.yscale
= this.maxyval
- this.minyval
;
319 PlotKit
.Layout
.prototype._evaluateScales
= function() {
320 var isNil
= MochiKit
.Base
.isUndefinedOrNull
;
322 this.xrange
= this.maxxval
- this.minxval
;
323 if (this.xrange
== 0)
326 this.xscale
= 1/this.xrange
;
328 this.yrange
= this.maxyval
- this.minyval
;
329 if (this.yrange
== 0)
332 this.yscale
= 1/this.yrange
;
335 PlotKit
.Layout
.prototype._uniqueXValues
= function() {
336 var collapse
= PlotKit
.Base
.collapse
;
337 var map
= PlotKit
.Base
.map
;
338 var uniq
= PlotKit
.Base
.uniq
;
339 var getter
= MochiKit
.Base
.itemgetter
;
340 var items
= PlotKit
.Base
.items
;
342 var xvalues
= map(parseFloat
, map(getter(0), collapse(map(getter(1), items(this.datasets
)))));
343 xvalues
.sort(MochiKit
.Base
.compare
);
344 return uniq(xvalues
);
348 PlotKit
.Layout
.prototype._evaluateBarCharts
= function() {
349 var items
= PlotKit
.Base
.items
;
351 var setCount
= items(this.datasets
).length
;
353 // work out how far separated values are
354 var xdelta
= 10000000;
355 var xvalues
= this._uniqueXValues();
356 for (var i
= 1; i
< xvalues
.length
; i
++) {
357 xdelta
= Math
.min(Math
.abs(xvalues
[i
] - xvalues
[i
-1]), xdelta
);
361 var barWidthForSet
= 0;
363 if (xvalues
.length
== 1) {
364 // note we have to do something smarter if we only plot one value
367 this.minxval
= xvalues
[0];
368 barWidth
= 1.0 * this.options
.barWidthFillFraction
;
369 barWidthForSet
= barWidth
/setCount
;
370 barMargin
= (1.0 - this.options
.barWidthFillFraction
)/2;
373 // readjust xscale to fix with bar charts
374 if (this.xrange
== 1) {
377 else if (this.xrange
== 2) {
381 this.xscale
= (1.0 - xdelta
/this.xrange)/this.xrange
;
383 barWidth
= xdelta
* this.xscale
* this.options
.barWidthFillFraction
;
384 barWidthForSet
= barWidth
/ setCount
;
385 barMargin
= xdelta
* this.xscale
* (1.0 - this.options
.barWidthFillFraction
)/2;
388 this.minxdelta
= xdelta
; // need this for tick positions
391 this.bars
= new Array();
393 for (var setName
in this.datasets
) {
394 var dataset
= this.datasets
[setName
];
395 if (PlotKit
.Base
.isFuncLike(dataset
)) continue;
396 for (var j
= 0; j
< dataset
.length
; j
++) {
397 var item
= dataset
[j
];
399 x
: ((parseFloat(item
[0]) - this.minxval
) * this.xscale
) + (i
* barWidthForSet
) + barMargin
,
400 y
: 1.0 - ((parseFloat(item
[1]) - this.minyval
) * this.yscale
),
402 h
: ((parseFloat(item
[1]) - this.minyval
) * this.yscale
),
403 xval
: parseFloat(item
[0]),
404 yval
: parseFloat(item
[1]),
407 if ((rect
.x
>= 0.0) && (rect
.x
<= 1.0) &&
408 (rect
.y
>= 0.0) && (rect
.y
<= 1.0)) {
409 this.bars
.push(rect
);
416 // Create the horizontal bars
417 PlotKit
.Layout
.prototype._evaluateHorizBarCharts
= function() {
418 var items
= PlotKit
.Base
.items
;
420 var setCount
= items(this.datasets
).length
;
422 // work out how far separated values are
423 var xdelta
= 10000000;
424 var xvalues
= this._uniqueXValues();
425 for (var i
= 1; i
< xvalues
.length
; i
++) {
426 xdelta
= Math
.min(Math
.abs(xvalues
[i
] - xvalues
[i
-1]), xdelta
);
430 var barWidthForSet
= 0;
433 // work out how far each far each bar is separated
434 if (xvalues
.length
== 1) {
435 // do something smarter if we only plot one value
438 this.minxval
= xvalues
[0];
439 barWidth
= 1.0 * this.options
.barWidthFillFraction
;
440 barWidthForSet
= barWidth
/setCount
;
441 barMargin
= (1.0 - this.options
.barWidthFillFraction
)/2;
444 // readjust yscale to fix with bar charts
445 this.xscale
= (1.0 - xdelta
/this.xrange)/this.xrange
;
446 barWidth
= xdelta
* this.xscale
* this.options
.barWidthFillFraction
;
447 barWidthForSet
= barWidth
/ setCount
;
448 barMargin
= xdelta
* this.xscale
* (1.0 - this.options
.barWidthFillFraction
)/2;
451 this.minxdelta
= xdelta
; // need this for tick positions
454 this.bars
= new Array();
456 for (var setName
in this.datasets
) {
457 var dataset
= this.datasets
[setName
];
458 if (PlotKit
.Base
.isFuncLike(dataset
)) continue;
459 for (var j
= 0; j
< dataset
.length
; j
++) {
460 var item
= dataset
[j
];
462 y
: ((parseFloat(item
[0]) - this.minxval
) * this.xscale
) + (i
* barWidthForSet
) + barMargin
,
465 w
: ((parseFloat(item
[1]) - this.minyval
) * this.yscale
),
466 xval
: parseFloat(item
[0]),
467 yval
: parseFloat(item
[1]),
471 // limit the x, y values so they do not overdraw
478 if ((rect
.x
>= 0.0) && (rect
.x
<= 1.0)) {
479 this.bars
.push(rect
);
487 // Create the line charts
488 PlotKit
.Layout
.prototype._evaluateLineCharts
= function() {
489 var items
= PlotKit
.Base
.items
;
491 var setCount
= items(this.datasets
).length
;
494 this.points
= new Array();
496 for (var setName
in this.datasets
) {
497 var dataset
= this.datasets
[setName
];
498 if (PlotKit
.Base
.isFuncLike(dataset
)) continue;
499 dataset
.sort(function(a
, b
) { return compare(parseFloat(a
[0]), parseFloat(b
[0])); });
500 for (var j
= 0; j
< dataset
.length
; j
++) {
501 var item
= dataset
[j
];
503 x
: ((parseFloat(item
[0]) - this.minxval
) * this.xscale
),
504 y
: 1.0 - ((parseFloat(item
[1]) - this.minyval
) * this.yscale
),
505 xval
: parseFloat(item
[0]),
506 yval
: parseFloat(item
[1]),
510 // limit the x, y values so they do not overdraw
511 if (point
.y
<= 0.0) {
514 if (point
.y
>= 1.0) {
517 if ((point
.x
>= 0.0) && (point
.x
<= 1.0)) {
518 this.points
.push(point
);
525 // Create the pie charts
526 PlotKit
.Layout
.prototype._evaluatePieCharts
= function() {
527 var items
= PlotKit
.Base
.items
;
528 var sum
= MochiKit
.Iter
.sum
;
529 var getter
= MochiKit
.Base
.itemgetter
;
531 var setCount
= items(this.datasets
).length
;
533 // we plot the y values of the first dataset
534 var dataset
= items(this.datasets
)[0][1];
535 var total
= sum(map(getter(1), dataset
));
537 this.slices
= new Array();
538 var currentAngle
= 0.0;
539 for (var i
= 0; i
< dataset
.length
; i
++) {
540 var fraction
= dataset
[i
][1] / total
;
541 var startAngle
= currentAngle
* Math
.PI
* 2;
542 var endAngle
= (currentAngle
+ fraction
) * Math
.PI
* 2;
544 var slice
= {fraction
: fraction
,
547 startAngle
: startAngle
,
550 if (dataset
[i
][1] != 0) {
551 this.slices
.push(slice
);
553 currentAngle
+= fraction
;
557 PlotKit
.Layout
.prototype._evaluateLineTicksForXAxis
= function() {
558 var isNil
= MochiKit
.Base
.isUndefinedOrNull
;
560 if (this.options
.xTicks
) {
561 // we use use specified ticks with optional labels
563 this.xticks
= new Array();
564 var makeTicks
= function(tick
) {
565 var label
= tick
.label
;
567 label
= tick
.v
.toString();
568 var pos
= this.xscale
* (tick
.v
- this.minxval
);
569 if ((pos
>= 0.0) && (pos
<= 1.0)) {
570 this.xticks
.push([pos
, label
]);
573 MochiKit
.Iter
.forEach(this.options
.xTicks
, bind(makeTicks
, this));
575 else if (this.options
.xNumberOfTicks
) {
576 // we use defined number of ticks as hint to auto generate
577 var xvalues
= this._uniqueXValues();
578 var roughSeparation
= this.xrange
/ this.options
.xNumberOfTicks
;
581 this.xticks
= new Array();
582 for (var i
= 0; i
<= xvalues
.length
; i
++) {
583 if ((xvalues
[i
] - this.minxval
) >= (tickCount
* roughSeparation
)) {
584 var pos
= this.xscale
* (xvalues
[i
] - this.minxval
);
585 if ((pos
> 1.0) || (pos
< 0.0))
587 this.xticks
.push([pos
, xvalues
[i
]]);
590 if (tickCount
> this.options
.xNumberOfTicks
)
596 PlotKit
.Layout
.prototype._evaluateLineTicksForYAxis
= function() {
597 var isNil
= MochiKit
.Base
.isUndefinedOrNull
;
600 if (this.options
.yTicks
) {
601 this.yticks
= new Array();
602 var makeTicks
= function(tick
) {
603 var label
= tick
.label
;
605 label
= tick
.v
.toString();
606 var pos
= 1.0 - (this.yscale
* (tick
.v
- this.minyval
));
607 if ((pos
>= 0.0) && (pos
<= 1.0)) {
608 this.yticks
.push([pos
, label
]);
611 MochiKit
.Iter
.forEach(this.options
.yTicks
, bind(makeTicks
, this));
613 else if (this.options
.yNumberOfTicks
) {
614 // We use the optionally defined number of ticks as a guide
615 this.yticks
= new Array();
617 // if we get this separation right, we'll have good looking graphs
618 var roundInt
= PlotKit
.Base
.roundInterval
;
619 var prec
= this.options
.yTickPrecision
;
620 var roughSeparation
= roundInt(this.yrange
,
621 this.options
.yNumberOfTicks
, prec
);
623 // round off each value of the y-axis to the precision
624 // eg. 1.3333 at precision 1 -> 1.3
625 for (var i
= 0; i
<= this.options
.yNumberOfTicks
; i
++) {
626 var yval
= this.minyval
+ (i
* roughSeparation
);
627 var pos
= 1.0 - ((yval
- this.minyval
) * this.yscale
);
628 if ((pos
> 1.0) || (pos
< 0.0))
630 this.yticks
.push([pos
, MochiKit
.Format
.roundToFixed(yval
, prec
)]);
635 PlotKit
.Layout
.prototype._evaluateLineTicks
= function() {
636 this._evaluateLineTicksForXAxis();
637 this._evaluateLineTicksForYAxis();
640 PlotKit
.Layout
.prototype._evaluateBarTicks
= function() {
641 this._evaluateLineTicks();
642 var centerInBar
= function(tick
) {
643 return [tick
[0] + (this.minxdelta
* this.xscale
)/2, tick
[1]];
645 this.xticks
= MochiKit
.Base
.map(bind(centerInBar
, this), this.xticks
);
647 if (this.options
.barOrientation
== "horizontal") {
649 var tempticks
= this.xticks
;
650 this.xticks
= this.yticks
;
651 this.yticks
= tempticks
;
653 // we need to invert the "yaxis" (which is now the xaxis when drawn)
654 var invert
= function(tick
) {
655 return [1.0 - tick
[0], tick
[1]];
657 this.xticks
= MochiKit
.Base
.map(invert
, this.xticks
);
661 PlotKit
.Layout
.prototype._evaluatePieTicks
= function() {
662 var isNil
= MochiKit
.Base
.isUndefinedOrNull
;
663 var formatter
= MochiKit
.Format
.numberFormatter("#%");
665 this.xticks
= new Array();
666 if (this.options
.xTicks
) {
667 // make a lookup dict for x->slice values
668 var lookup
= new Array();
669 for (var i
= 0; i
< this.slices
.length
; i
++) {
670 lookup
[this.slices
[i
].xval
] = this.slices
[i
];
673 for (var i
=0; i
< this.options
.xTicks
.length
; i
++) {
674 var tick
= this.options
.xTicks
[i
];
675 var slice
= lookup
[tick
.v
];
676 var label
= tick
.label
;
679 label
= tick
.v
.toString();
680 label
+= " (" + formatter(slice
.fraction
) + ")";
681 this.xticks
.push([tick
.v
, label
]);
686 // we make our own labels from all the slices
687 for (var i
=0; i
< this.slices
.length
; i
++) {
688 var slice
= this.slices
[i
];
689 var label
= slice
.xval
+ " (" + formatter(slice
.fraction
) + ")";
690 this.xticks
.push([slice
.xval
, label
]);
695 PlotKit
.Layout
.prototype._regenerateHitTestCache
= function() {
696 this.hitTestCache
.xvalues
= this._uniqueXValues();
697 this.hitTestCache
.xlookup
= new Array();
698 this.hitTestCache
.x2maxy
= new Array();
700 var listMax
= MochiKit
.Base
.listMax
;
701 var itemgetter
= MochiKit
.Base
.itemgetter
;
702 var map
= MochiKit
.Base
.map
;
704 // generate a lookup table for x values to y values
705 var setNames
= keys(this.datasets
);
706 for (var i
= 0; i
< setNames
.length
; i
++) {
707 var dataset
= this.datasets
[setNames
[i
]];
708 for (var j
= 0; j
< dataset
.length
; j
++) {
709 var xval
= dataset
[j
][0];
710 var yval
= dataset
[j
][1];
711 if (this.hitTestCache
.xlookup
[xval
])
712 this.hitTestCache
.xlookup
[xval
].push([yval
, setNames
[i
]]);
714 this.hitTestCache
.xlookup
[xval
] = [[yval
, setNames
[i
]]];
718 for (var x
in this.hitTestCache
.xlookup
) {
719 var yvals
= this.hitTestCache
.xlookup
[x
];
720 this.hitTestCache
.x2maxy
[x
] = listMax(map(itemgetter(0), yvals
));
726 // --------------------------------------------------------------------
727 // END Internal Functions
728 // --------------------------------------------------------------------
731 // Namespace Iniitialisation
733 PlotKit
.LayoutModule
= {};
734 PlotKit
.LayoutModule
.Layout
= PlotKit
.Layout
;
736 PlotKit
.LayoutModule
.EXPORT
= [
740 PlotKit
.LayoutModule
.EXPORT_OK
= [];
742 PlotKit
.LayoutModule
.__new__
= function() {
743 var m
= MochiKit
.Base
;
745 m
.nameFunctions(this);
748 ":common": this.EXPORT
,
749 ":all": m
.concat(this.EXPORT
, this.EXPORT_OK
)
753 PlotKit
.LayoutModule
.__new__();
754 MochiKit
.Base
._exportSymbols(this, PlotKit
.LayoutModule
);