mercilessly rip out irrelevant PlotKit functions. This may be ill-advised, but PlotKi...
[dygraphs.git] / plotkit_v091 / PlotKit / Layout.js
1 /*
2 PlotKit Layout
3 ==============
4
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.
8
9 Copyright
10 ---------
11 Copyright 2005,2006 (c) Alastair Tse <alastair^liquidx.net>
12 For use under the BSD license. <http://www.liquidx.net/plotkit>
13
14 */
15
16 try {
17 if (typeof(PlotKit.Base) == 'undefined')
18 {
19 throw ""
20 }
21 }
22 catch (e) {
23 throw "PlotKit.Layout depends on MochiKit.{Base,Color,DOM,Format} and PlotKit.Base"
24 }
25
26 // --------------------------------------------------------------------
27 // Start of Layout definition
28 // --------------------------------------------------------------------
29
30 if (typeof(PlotKit.Layout) == 'undefined') {
31 PlotKit.Layout = {};
32 }
33
34 PlotKit.Layout.NAME = "PlotKit.Layout";
35 PlotKit.Layout.VERSION = PlotKit.VERSION;
36
37 PlotKit.Layout.__repr__ = function() {
38 return "[" + this.NAME + " " + this.VERSION + "]";
39 };
40
41 PlotKit.Layout.toString = function() {
42 return this.__repr__();
43 }
44
45 PlotKit.Layout.valid_styles = ["bar", "line", "pie", "point"];
46
47 // --------------------------------------------------------------------
48 // Start of Layout definition
49 // --------------------------------------------------------------------
50
51 PlotKit.Layout = function(style, options) {
52
53 this.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.)
62 "xNumberOfTicks": 10,
63 "yNumberOfTicks": 5,
64 "xTickPrecision": 1,
65 "yTickPrecision": 1,
66 "pieRadius": 0.4
67 };
68
69 // valid external options : TODO: input verification
70 this.style = style;
71 MochiKit.Base.update(this.options, options ? options : {});
72
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;
79 }
80 else {
81 this.minxval = 0;
82 this.maxxval = null;
83 this.xscale = null; // val -> pos factor (eg, xval * xscale = xpos)
84 }
85
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;
90 }
91 else {
92 this.minyval = 0;
93 this.maxyval = null;
94 this.yscale = null;
95 }
96
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
100
101 this.xticks = new Array();
102 this.yticks = new Array();
103
104 // internal states
105 this.datasets = new Array();
106 this.minxdelta = 0;
107 this.xrange = 1;
108 this.yrange = 1;
109
110 this.hitTestCache = {x2maxy: null};
111
112 };
113
114 // --------------------------------------------------------------------
115 // Dataset Manipulation
116 // --------------------------------------------------------------------
117
118
119 PlotKit.Layout.prototype.addDataset = function(setname, set_xy) {
120 this.datasets[setname] = set_xy;
121 };
122
123 PlotKit.Layout.prototype.removeDataset = function(setname, set_xy) {
124 delete this.datasets[setname];
125 };
126
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;
131
132 if (isNil(xcol))
133 xcol = 0;
134 if (isNil(ycol))
135 ycol = 1;
136 if (isNil(lcol))
137 lcol = -1;
138
139 var rows = tableElement.tBodies[0].rows;
140 var data = new Array();
141 var labels = new Array();
142
143 if (!isNil(rows)) {
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])))]);
147 if (lcol >= 0){
148 labels.push({v: parseFloat(strip(scrapeText(rows[i].cells[xcol]))),
149 label: strip(scrapeText(rows[i].cells[lcol]))});
150 }
151 }
152 this.addDataset(name, data);
153 if (lcol >= 0) {
154 this.options.xTicks = labels;
155 }
156 return true;
157 }
158 return false;
159 };
160
161 // --------------------------------------------------------------------
162 // Evaluates the layout for the current data and style.
163 // --------------------------------------------------------------------
164
165 PlotKit.Layout.prototype.evaluate = function() {
166 this._evaluateLimits();
167 this._evaluateScales();
168 if (this.style == "line") {
169 this._evaluateLineCharts();
170 this._evaluateLineTicks();
171 }
172 };
173
174
175
176
177 // --------------------------------------------------------------------
178 // START Internal Functions
179 // --------------------------------------------------------------------
180
181 PlotKit.Layout.prototype._evaluateLimits = function() {
182 // take all values from all datasets and find max and min
183 var map = PlotKit.Base.map;
184 var items = PlotKit.Base.items;
185 var itemgetter = MochiKit.Base.itemgetter;
186 var collapse = PlotKit.Base.collapse;
187 var listMin = MochiKit.Base.listMin;
188 var listMax = MochiKit.Base.listMax;
189 var isNil = MochiKit.Base.isUndefinedOrNull;
190
191
192 var all = collapse(map(itemgetter(1), items(this.datasets)));
193 if (isNil(this.options.xAxis)) {
194 if (this.options.xOriginIsZero)
195 this.minxval = 0;
196 else
197 this.minxval = listMin(map(parseFloat, map(itemgetter(0), all)));
198
199 this.maxxval = listMax(map(parseFloat, map(itemgetter(0), all)));
200 }
201 else {
202 this.minxval = this.options.xAxis[0];
203 this.maxxval = this.options.xAxis[1];
204 this.xscale = this.maxval - this.minxval;
205 }
206
207 if (isNil(this.options.yAxis)) {
208 if (this.options.yOriginIsZero)
209 this.minyval = 0;
210 else
211 this.minyval = listMin(map(parseFloat, map(itemgetter(1), all)));
212
213 this.maxyval = listMax(map(parseFloat, map(itemgetter(1), all)));
214 }
215 else {
216 this.minyval = this.options.yAxis[0];
217 this.maxyval = this.options.yAxis[1];
218 this.yscale = this.maxyval - this.minyval;
219 }
220
221 };
222
223 PlotKit.Layout.prototype._evaluateScales = function() {
224 var isNil = MochiKit.Base.isUndefinedOrNull;
225
226 this.xrange = this.maxxval - this.minxval;
227 if (this.xrange == 0)
228 this.xscale = 1.0;
229 else
230 this.xscale = 1/this.xrange;
231
232 this.yrange = this.maxyval - this.minyval;
233 if (this.yrange == 0)
234 this.yscale = 1.0;
235 else
236 this.yscale = 1/this.yrange;
237 };
238
239 PlotKit.Layout.prototype._uniqueXValues = function() {
240 var collapse = PlotKit.Base.collapse;
241 var map = PlotKit.Base.map;
242 var uniq = PlotKit.Base.uniq;
243 var getter = MochiKit.Base.itemgetter;
244 var items = PlotKit.Base.items;
245
246 var xvalues = map(parseFloat, map(getter(0), collapse(map(getter(1), items(this.datasets)))));
247 xvalues.sort(MochiKit.Base.compare);
248 return uniq(xvalues);
249 };
250
251
252 // Create the line charts
253 PlotKit.Layout.prototype._evaluateLineCharts = function() {
254 var items = PlotKit.Base.items;
255
256 var setCount = items(this.datasets).length;
257
258 // add all the rects
259 this.points = new Array();
260 var i = 0;
261 for (var setName in this.datasets) {
262 var dataset = this.datasets[setName];
263 if (PlotKit.Base.isFuncLike(dataset)) continue;
264 dataset.sort(function(a, b) { return compare(parseFloat(a[0]), parseFloat(b[0])); });
265 for (var j = 0; j < dataset.length; j++) {
266 var item = dataset[j];
267 var point = {
268 x: ((parseFloat(item[0]) - this.minxval) * this.xscale),
269 y: 1.0 - ((parseFloat(item[1]) - this.minyval) * this.yscale),
270 xval: parseFloat(item[0]),
271 yval: parseFloat(item[1]),
272 name: setName
273 };
274
275 // limit the x, y values so they do not overdraw
276 if (point.y <= 0.0) {
277 point.y = 0.0;
278 }
279 if (point.y >= 1.0) {
280 point.y = 1.0;
281 }
282 if ((point.x >= 0.0) && (point.x <= 1.0)) {
283 this.points.push(point);
284 }
285 }
286 i++;
287 }
288 };
289
290
291 PlotKit.Layout.prototype._evaluateLineTicksForXAxis = function() {
292 var isNil = MochiKit.Base.isUndefinedOrNull;
293
294 if (this.options.xTicks) {
295 // we use use specified ticks with optional labels
296
297 this.xticks = new Array();
298 var makeTicks = function(tick) {
299 var label = tick.label;
300 if (isNil(label))
301 label = tick.v.toString();
302 var pos = this.xscale * (tick.v - this.minxval);
303 if ((pos >= 0.0) && (pos <= 1.0)) {
304 this.xticks.push([pos, label]);
305 }
306 };
307 MochiKit.Iter.forEach(this.options.xTicks, bind(makeTicks, this));
308 }
309 else if (this.options.xNumberOfTicks) {
310 // we use defined number of ticks as hint to auto generate
311 var xvalues = this._uniqueXValues();
312 var roughSeparation = this.xrange / this.options.xNumberOfTicks;
313 var tickCount = 0;
314
315 this.xticks = new Array();
316 for (var i = 0; i <= xvalues.length; i++) {
317 if ((xvalues[i] - this.minxval) >= (tickCount * roughSeparation)) {
318 var pos = this.xscale * (xvalues[i] - this.minxval);
319 if ((pos > 1.0) || (pos < 0.0))
320 continue;
321 this.xticks.push([pos, xvalues[i]]);
322 tickCount++;
323 }
324 if (tickCount > this.options.xNumberOfTicks)
325 break;
326 }
327 }
328 };
329
330 PlotKit.Layout.prototype._evaluateLineTicksForYAxis = function() {
331 var isNil = MochiKit.Base.isUndefinedOrNull;
332
333
334 if (this.options.yTicks) {
335 this.yticks = new Array();
336 var makeTicks = function(tick) {
337 var label = tick.label;
338 if (isNil(label))
339 label = tick.v.toString();
340 var pos = 1.0 - (this.yscale * (tick.v - this.minyval));
341 if ((pos >= 0.0) && (pos <= 1.0)) {
342 this.yticks.push([pos, label]);
343 }
344 };
345 MochiKit.Iter.forEach(this.options.yTicks, bind(makeTicks, this));
346 }
347 else if (this.options.yNumberOfTicks) {
348 // We use the optionally defined number of ticks as a guide
349 this.yticks = new Array();
350
351 // if we get this separation right, we'll have good looking graphs
352 var roundInt = PlotKit.Base.roundInterval;
353 var prec = this.options.yTickPrecision;
354 var roughSeparation = roundInt(this.yrange,
355 this.options.yNumberOfTicks, prec);
356
357 // round off each value of the y-axis to the precision
358 // eg. 1.3333 at precision 1 -> 1.3
359 for (var i = 0; i <= this.options.yNumberOfTicks; i++) {
360 var yval = this.minyval + (i * roughSeparation);
361 var pos = 1.0 - ((yval - this.minyval) * this.yscale);
362 if ((pos > 1.0) || (pos < 0.0))
363 continue;
364 this.yticks.push([pos, MochiKit.Format.roundToFixed(yval, prec)]);
365 }
366 }
367 };
368
369 PlotKit.Layout.prototype._evaluateLineTicks = function() {
370 this._evaluateLineTicksForXAxis();
371 this._evaluateLineTicksForYAxis();
372 };
373
374
375 // --------------------------------------------------------------------
376 // END Internal Functions
377 // --------------------------------------------------------------------
378
379
380 // Namespace Iniitialisation
381
382 PlotKit.LayoutModule = {};
383 PlotKit.LayoutModule.Layout = PlotKit.Layout;
384
385 PlotKit.LayoutModule.EXPORT = [
386 "Layout"
387 ];
388
389 PlotKit.LayoutModule.EXPORT_OK = [];
390
391 PlotKit.LayoutModule.__new__ = function() {
392 var m = MochiKit.Base;
393
394 m.nameFunctions(this);
395
396 this.EXPORT_TAGS = {
397 ":common": this.EXPORT,
398 ":all": m.concat(this.EXPORT, this.EXPORT_OK)
399 };
400 };
401
402 PlotKit.LayoutModule.__new__();
403 MochiKit.Base._exportSymbols(this, PlotKit.LayoutModule);
404
405