Commit | Line | Data |
---|---|---|
6a1aa64f DV |
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 == "bar") { | |
169 | if (this.options.barOrientation == "horizontal") { | |
170 | this._evaluateHorizBarCharts(); | |
171 | } | |
172 | else { | |
173 | this._evaluateBarCharts(); | |
174 | } | |
175 | this._evaluateBarTicks(); | |
176 | } | |
177 | else if (this.style == "line") { | |
178 | this._evaluateLineCharts(); | |
179 | this._evaluateLineTicks(); | |
180 | } | |
181 | else if (this.style == "pie") { | |
182 | this._evaluatePieCharts(); | |
183 | this._evaluatePieTicks(); | |
184 | } | |
185 | }; | |
186 | ||
187 | ||
188 | ||
189 | // Given the fractional x, y positions, report the corresponding | |
190 | // x, y values. | |
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 | |
194 | ||
195 | var f = MochiKit.Format.twoDigitFloat; | |
196 | ||
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)) | |
202 | return bar; | |
203 | } | |
204 | } | |
205 | ||
206 | else if (this.style == "line") { | |
207 | if (this.hitTestCache.x2maxy == null) { | |
208 | this._regenerateHitTestCache(); | |
209 | } | |
210 | ||
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; | |
214 | var xbefore = null; | |
215 | var xafter = null; | |
216 | ||
217 | for (var i = 1; i < xvalues.length; i++) { | |
218 | if (xvalues[i] > xval) { | |
219 | xbefore = xvalues[i-1]; | |
220 | xafter = xvalues[i]; | |
221 | break; | |
222 | } | |
223 | } | |
224 | ||
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; | |
229 | ||
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, | |
238 | yprojected: projmaxy | |
239 | }; | |
240 | return obj; | |
241 | } | |
242 | } | |
243 | } | |
244 | ||
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) | |
248 | return null; | |
249 | ||
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) | |
256 | return slice; | |
257 | } | |
258 | } | |
259 | ||
260 | return null; | |
261 | }; | |
262 | ||
263 | // Reports valid position rectangle for X value (only valid for bar charts) | |
264 | PlotKit.Layout.prototype.rectForX = function(x) { | |
265 | return null; | |
266 | }; | |
267 | ||
268 | // Reports valid angles through which X value encloses (only valid for pie charts) | |
269 | PlotKit.Layout.prototype.angleRangeForX = function(x) { | |
270 | return null; | |
271 | }; | |
272 | ||
273 | // -------------------------------------------------------------------- | |
274 | // START Internal Functions | |
275 | // -------------------------------------------------------------------- | |
276 | ||
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; | |
286 | ||
287 | ||
288 | var all = collapse(map(itemgetter(1), items(this.datasets))); | |
289 | if (isNil(this.options.xAxis)) { | |
290 | if (this.options.xOriginIsZero) | |
291 | this.minxval = 0; | |
292 | else | |
293 | this.minxval = listMin(map(parseFloat, map(itemgetter(0), all))); | |
294 | ||
295 | this.maxxval = listMax(map(parseFloat, map(itemgetter(0), all))); | |
296 | } | |
297 | else { | |
298 | this.minxval = this.options.xAxis[0]; | |
299 | this.maxxval = this.options.xAxis[1]; | |
300 | this.xscale = this.maxval - this.minxval; | |
301 | } | |
302 | ||
303 | if (isNil(this.options.yAxis)) { | |
304 | if (this.options.yOriginIsZero) | |
305 | this.minyval = 0; | |
306 | else | |
307 | this.minyval = listMin(map(parseFloat, map(itemgetter(1), all))); | |
308 | ||
309 | this.maxyval = listMax(map(parseFloat, map(itemgetter(1), all))); | |
310 | } | |
311 | else { | |
312 | this.minyval = this.options.yAxis[0]; | |
313 | this.maxyval = this.options.yAxis[1]; | |
314 | this.yscale = this.maxyval - this.minyval; | |
315 | } | |
316 | ||
317 | }; | |
318 | ||
319 | PlotKit.Layout.prototype._evaluateScales = function() { | |
320 | var isNil = MochiKit.Base.isUndefinedOrNull; | |
321 | ||
322 | this.xrange = this.maxxval - this.minxval; | |
323 | if (this.xrange == 0) | |
324 | this.xscale = 1.0; | |
325 | else | |
326 | this.xscale = 1/this.xrange; | |
327 | ||
328 | this.yrange = this.maxyval - this.minyval; | |
329 | if (this.yrange == 0) | |
330 | this.yscale = 1.0; | |
331 | else | |
332 | this.yscale = 1/this.yrange; | |
333 | }; | |
334 | ||
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; | |
341 | ||
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); | |
345 | }; | |
346 | ||
347 | // Create the bars | |
348 | PlotKit.Layout.prototype._evaluateBarCharts = function() { | |
349 | var items = PlotKit.Base.items; | |
350 | ||
351 | var setCount = items(this.datasets).length; | |
352 | ||
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); | |
358 | } | |
359 | ||
360 | var barWidth = 0; | |
361 | var barWidthForSet = 0; | |
362 | var barMargin = 0; | |
363 | if (xvalues.length == 1) { | |
364 | // note we have to do something smarter if we only plot one value | |
365 | xdelta = 1.0; | |
366 | this.xscale = 1.0; | |
367 | this.minxval = xvalues[0]; | |
368 | barWidth = 1.0 * this.options.barWidthFillFraction; | |
369 | barWidthForSet = barWidth/setCount; | |
370 | barMargin = (1.0 - this.options.barWidthFillFraction)/2; | |
371 | } | |
372 | else { | |
373 | // readjust xscale to fix with bar charts | |
374 | if (this.xrange == 1) { | |
375 | this.xscale = 0.5; | |
376 | } | |
377 | else if (this.xrange == 2) { | |
378 | this.xscale = 1/3.0; | |
379 | } | |
380 | else { | |
381 | this.xscale = (1.0 - xdelta/this.xrange)/this.xrange; | |
382 | } | |
383 | barWidth = xdelta * this.xscale * this.options.barWidthFillFraction; | |
384 | barWidthForSet = barWidth / setCount; | |
385 | barMargin = xdelta * this.xscale * (1.0 - this.options.barWidthFillFraction)/2; | |
386 | } | |
387 | ||
388 | this.minxdelta = xdelta; // need this for tick positions | |
389 | ||
390 | // add all the rects | |
391 | this.bars = new Array(); | |
392 | var i = 0; | |
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]; | |
398 | var rect = { | |
399 | x: ((parseFloat(item[0]) - this.minxval) * this.xscale) + (i * barWidthForSet) + barMargin, | |
400 | y: 1.0 - ((parseFloat(item[1]) - this.minyval) * this.yscale), | |
401 | w: barWidthForSet, | |
402 | h: ((parseFloat(item[1]) - this.minyval) * this.yscale), | |
403 | xval: parseFloat(item[0]), | |
404 | yval: parseFloat(item[1]), | |
405 | name: setName | |
406 | }; | |
407 | if ((rect.x >= 0.0) && (rect.x <= 1.0) && | |
408 | (rect.y >= 0.0) && (rect.y <= 1.0)) { | |
409 | this.bars.push(rect); | |
410 | } | |
411 | } | |
412 | i++; | |
413 | } | |
414 | }; | |
415 | ||
416 | // Create the horizontal bars | |
417 | PlotKit.Layout.prototype._evaluateHorizBarCharts = function() { | |
418 | var items = PlotKit.Base.items; | |
419 | ||
420 | var setCount = items(this.datasets).length; | |
421 | ||
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); | |
427 | } | |
428 | ||
429 | var barWidth = 0; | |
430 | var barWidthForSet = 0; | |
431 | var barMargin = 0; | |
432 | ||
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 | |
436 | xdelta = 1.0; | |
437 | this.xscale = 1.0; | |
438 | this.minxval = xvalues[0]; | |
439 | barWidth = 1.0 * this.options.barWidthFillFraction; | |
440 | barWidthForSet = barWidth/setCount; | |
441 | barMargin = (1.0 - this.options.barWidthFillFraction)/2; | |
442 | } | |
443 | else { | |
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; | |
449 | } | |
450 | ||
451 | this.minxdelta = xdelta; // need this for tick positions | |
452 | ||
453 | // add all the rects | |
454 | this.bars = new Array(); | |
455 | var i = 0; | |
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]; | |
461 | var rect = { | |
462 | y: ((parseFloat(item[0]) - this.minxval) * this.xscale) + (i * barWidthForSet) + barMargin, | |
463 | x: 0.0, | |
464 | h: barWidthForSet, | |
465 | w: ((parseFloat(item[1]) - this.minyval) * this.yscale), | |
466 | xval: parseFloat(item[0]), | |
467 | yval: parseFloat(item[1]), | |
468 | name: setName | |
469 | }; | |
470 | ||
471 | // limit the x, y values so they do not overdraw | |
472 | if (rect.y <= 0.0) { | |
473 | rect.y = 0.0; | |
474 | } | |
475 | if (rect.y >= 1.0) { | |
476 | rect.y = 1.0; | |
477 | } | |
478 | if ((rect.x >= 0.0) && (rect.x <= 1.0)) { | |
479 | this.bars.push(rect); | |
480 | } | |
481 | } | |
482 | i++; | |
483 | } | |
484 | }; | |
485 | ||
486 | ||
487 | // Create the line charts | |
488 | PlotKit.Layout.prototype._evaluateLineCharts = function() { | |
489 | var items = PlotKit.Base.items; | |
490 | ||
491 | var setCount = items(this.datasets).length; | |
492 | ||
493 | // add all the rects | |
494 | this.points = new Array(); | |
495 | var i = 0; | |
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]; | |
502 | var point = { | |
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]), | |
507 | name: setName | |
508 | }; | |
509 | ||
510 | // limit the x, y values so they do not overdraw | |
511 | if (point.y <= 0.0) { | |
512 | point.y = 0.0; | |
513 | } | |
514 | if (point.y >= 1.0) { | |
515 | point.y = 1.0; | |
516 | } | |
517 | if ((point.x >= 0.0) && (point.x <= 1.0)) { | |
518 | this.points.push(point); | |
519 | } | |
520 | } | |
521 | i++; | |
522 | } | |
523 | }; | |
524 | ||
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; | |
530 | ||
531 | var setCount = items(this.datasets).length; | |
532 | ||
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)); | |
536 | ||
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; | |
543 | ||
544 | var slice = {fraction: fraction, | |
545 | xval: dataset[i][0], | |
546 | yval: dataset[i][1], | |
547 | startAngle: startAngle, | |
548 | endAngle: endAngle | |
549 | }; | |
550 | if (dataset[i][1] != 0) { | |
551 | this.slices.push(slice); | |
552 | } | |
553 | currentAngle += fraction; | |
554 | } | |
555 | }; | |
556 | ||
557 | PlotKit.Layout.prototype._evaluateLineTicksForXAxis = function() { | |
558 | var isNil = MochiKit.Base.isUndefinedOrNull; | |
559 | ||
560 | if (this.options.xTicks) { | |
561 | // we use use specified ticks with optional labels | |
562 | ||
563 | this.xticks = new Array(); | |
564 | var makeTicks = function(tick) { | |
565 | var label = tick.label; | |
566 | if (isNil(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]); | |
571 | } | |
572 | }; | |
573 | MochiKit.Iter.forEach(this.options.xTicks, bind(makeTicks, this)); | |
574 | } | |
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; | |
579 | var tickCount = 0; | |
580 | ||
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)) | |
586 | continue; | |
587 | this.xticks.push([pos, xvalues[i]]); | |
588 | tickCount++; | |
589 | } | |
590 | if (tickCount > this.options.xNumberOfTicks) | |
591 | break; | |
592 | } | |
593 | } | |
594 | }; | |
595 | ||
596 | PlotKit.Layout.prototype._evaluateLineTicksForYAxis = function() { | |
597 | var isNil = MochiKit.Base.isUndefinedOrNull; | |
598 | ||
599 | ||
600 | if (this.options.yTicks) { | |
601 | this.yticks = new Array(); | |
602 | var makeTicks = function(tick) { | |
603 | var label = tick.label; | |
604 | if (isNil(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]); | |
609 | } | |
610 | }; | |
611 | MochiKit.Iter.forEach(this.options.yTicks, bind(makeTicks, this)); | |
612 | } | |
613 | else if (this.options.yNumberOfTicks) { | |
614 | // We use the optionally defined number of ticks as a guide | |
615 | this.yticks = new Array(); | |
616 | ||
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); | |
622 | ||
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)) | |
629 | continue; | |
630 | this.yticks.push([pos, MochiKit.Format.roundToFixed(yval, prec)]); | |
631 | } | |
632 | } | |
633 | }; | |
634 | ||
635 | PlotKit.Layout.prototype._evaluateLineTicks = function() { | |
636 | this._evaluateLineTicksForXAxis(); | |
637 | this._evaluateLineTicksForYAxis(); | |
638 | }; | |
639 | ||
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]]; | |
644 | }; | |
645 | this.xticks = MochiKit.Base.map(bind(centerInBar, this), this.xticks); | |
646 | ||
647 | if (this.options.barOrientation == "horizontal") { | |
648 | // swap scales | |
649 | var tempticks = this.xticks; | |
650 | this.xticks = this.yticks; | |
651 | this.yticks = tempticks; | |
652 | ||
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]]; | |
656 | } | |
657 | this.xticks = MochiKit.Base.map(invert, this.xticks); | |
658 | } | |
659 | }; | |
660 | ||
661 | PlotKit.Layout.prototype._evaluatePieTicks = function() { | |
662 | var isNil = MochiKit.Base.isUndefinedOrNull; | |
663 | var formatter = MochiKit.Format.numberFormatter("#%"); | |
664 | ||
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]; | |
671 | } | |
672 | ||
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; | |
677 | if (slice) { | |
678 | if (isNil(label)) | |
679 | label = tick.v.toString(); | |
680 | label += " (" + formatter(slice.fraction) + ")"; | |
681 | this.xticks.push([tick.v, label]); | |
682 | } | |
683 | } | |
684 | } | |
685 | else { | |
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]); | |
691 | } | |
692 | } | |
693 | }; | |
694 | ||
695 | PlotKit.Layout.prototype._regenerateHitTestCache = function() { | |
696 | this.hitTestCache.xvalues = this._uniqueXValues(); | |
697 | this.hitTestCache.xlookup = new Array(); | |
698 | this.hitTestCache.x2maxy = new Array(); | |
699 | ||
700 | var listMax = MochiKit.Base.listMax; | |
701 | var itemgetter = MochiKit.Base.itemgetter; | |
702 | var map = MochiKit.Base.map; | |
703 | ||
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]]); | |
713 | else | |
714 | this.hitTestCache.xlookup[xval] = [[yval, setNames[i]]]; | |
715 | } | |
716 | } | |
717 | ||
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)); | |
721 | } | |
722 | ||
723 | ||
724 | }; | |
725 | ||
726 | // -------------------------------------------------------------------- | |
727 | // END Internal Functions | |
728 | // -------------------------------------------------------------------- | |
729 | ||
730 | ||
731 | // Namespace Iniitialisation | |
732 | ||
733 | PlotKit.LayoutModule = {}; | |
734 | PlotKit.LayoutModule.Layout = PlotKit.Layout; | |
735 | ||
736 | PlotKit.LayoutModule.EXPORT = [ | |
737 | "Layout" | |
738 | ]; | |
739 | ||
740 | PlotKit.LayoutModule.EXPORT_OK = []; | |
741 | ||
742 | PlotKit.LayoutModule.__new__ = function() { | |
743 | var m = MochiKit.Base; | |
744 | ||
745 | m.nameFunctions(this); | |
746 | ||
747 | this.EXPORT_TAGS = { | |
748 | ":common": this.EXPORT, | |
749 | ":all": m.concat(this.EXPORT, this.EXPORT_OK) | |
750 | }; | |
751 | }; | |
752 | ||
753 | PlotKit.LayoutModule.__new__(); | |
754 | MochiKit.Base._exportSymbols(this, PlotKit.LayoutModule); | |
755 | ||
756 |