X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=plotkit_v091%2FPlotKit%2FLayout.js;h=13b2de0fc203014adbdf3517ce5bb7432469e79e;hb=5709f0d5571980cae539ebb94533dee5c6b405c1;hp=ff1c9db8acd7f2ea0784b89c932129c8a306bbe9;hpb=6a1aa64f6d22473e0357ad1cd7bd93259d899a69;p=dygraphs.git diff --git a/plotkit_v091/PlotKit/Layout.js b/plotkit_v091/PlotKit/Layout.js index ff1c9db..13b2de0 100644 --- a/plotkit_v091/PlotKit/Layout.js +++ b/plotkit_v091/PlotKit/Layout.js @@ -13,16 +13,6 @@ */ -try { - if (typeof(PlotKit.Base) == 'undefined') - { - throw "" - } -} -catch (e) { - throw "PlotKit.Layout depends on MochiKit.{Base,Color,DOM,Format} and PlotKit.Base" -} - // -------------------------------------------------------------------- // Start of Layout definition // -------------------------------------------------------------------- @@ -31,84 +21,18 @@ if (typeof(PlotKit.Layout) == 'undefined') { PlotKit.Layout = {}; } -PlotKit.Layout.NAME = "PlotKit.Layout"; -PlotKit.Layout.VERSION = PlotKit.VERSION; - -PlotKit.Layout.__repr__ = function() { - return "[" + this.NAME + " " + this.VERSION + "]"; -}; - -PlotKit.Layout.toString = function() { - return this.__repr__(); -} - -PlotKit.Layout.valid_styles = ["bar", "line", "pie", "point"]; - // -------------------------------------------------------------------- // Start of Layout definition // -------------------------------------------------------------------- PlotKit.Layout = function(style, options) { - - this.options = { - "barWidthFillFraction": 0.75, - "barOrientation": "vertical", - "xOriginIsZero": true, - "yOriginIsZero": true, - "xAxis": null, // [xmin, xmax] - "yAxis": null, // [ymin, ymax] - "xTicks": null, // [{label: "somelabel", v: value}, ..] (label opt.) - "yTicks": null, // [{label: "somelabel", v: value}, ..] (label opt.) - "xNumberOfTicks": 10, - "yNumberOfTicks": 5, - "xTickPrecision": 1, - "yTickPrecision": 1, - "pieRadius": 0.4 - }; + this.options = { }; // valid external options : TODO: input verification - this.style = style; MochiKit.Base.update(this.options, options ? options : {}); - // externally visible states - // overriden if xAxis and yAxis are set in options - if (!MochiKit.Base.isUndefinedOrNull(this.options.xAxis)) { - this.minxval = this.options.xAxis[0]; - this.maxxval = this.options.xAxis[1]; - this.xscale = this.maxxval - this.minxval; - } - else { - this.minxval = 0; - this.maxxval = null; - this.xscale = null; // val -> pos factor (eg, xval * xscale = xpos) - } - - if (!MochiKit.Base.isUndefinedOrNull(this.options.yAxis)) { - this.minyval = this.options.yAxis[0]; - this.maxyval = this.options.yAxis[1]; - this.yscale = this.maxyval - this.minyval; - } - else { - this.minyval = 0; - this.maxyval = null; - this.yscale = null; - } - - this.bars = new Array(); // array of bars to plot for bar charts - this.points = new Array(); // array of points to plot for line plots - this.slices = new Array(); // array of slices to draw for pie charts - - this.xticks = new Array(); - this.yticks = new Array(); - // internal states this.datasets = new Array(); - this.minxdelta = 0; - this.xrange = 1; - this.yrange = 1; - - this.hitTestCache = {x2maxy: null}; - }; // -------------------------------------------------------------------- @@ -120,383 +44,46 @@ PlotKit.Layout.prototype.addDataset = function(setname, set_xy) { this.datasets[setname] = set_xy; }; -PlotKit.Layout.prototype.removeDataset = function(setname, set_xy) { - delete this.datasets[setname]; -}; - -PlotKit.Layout.prototype.addDatasetFromTable = function(name, tableElement, xcol, ycol, lcol) { - var isNil = MochiKit.Base.isUndefinedOrNull; - var scrapeText = MochiKit.DOM.scrapeText; - var strip = MochiKit.Format.strip; - - if (isNil(xcol)) - xcol = 0; - if (isNil(ycol)) - ycol = 1; - if (isNil(lcol)) - lcol = -1; - - var rows = tableElement.tBodies[0].rows; - var data = new Array(); - var labels = new Array(); - - if (!isNil(rows)) { - for (var i = 0; i < rows.length; i++) { - data.push([parseFloat(strip(scrapeText(rows[i].cells[xcol]))), - parseFloat(strip(scrapeText(rows[i].cells[ycol])))]); - if (lcol >= 0){ - labels.push({v: parseFloat(strip(scrapeText(rows[i].cells[xcol]))), - label: strip(scrapeText(rows[i].cells[lcol]))}); - } - } - this.addDataset(name, data); - if (lcol >= 0) { - this.options.xTicks = labels; - } - return true; - } - return false; -}; - // -------------------------------------------------------------------- // Evaluates the layout for the current data and style. // -------------------------------------------------------------------- PlotKit.Layout.prototype.evaluate = function() { this._evaluateLimits(); - this._evaluateScales(); - if (this.style == "bar") { - if (this.options.barOrientation == "horizontal") { - this._evaluateHorizBarCharts(); - } - else { - this._evaluateBarCharts(); - } - this._evaluateBarTicks(); - } - else if (this.style == "line") { - this._evaluateLineCharts(); - this._evaluateLineTicks(); - } - else if (this.style == "pie") { - this._evaluatePieCharts(); - this._evaluatePieTicks(); - } -}; - - - -// Given the fractional x, y positions, report the corresponding -// x, y values. -PlotKit.Layout.prototype.hitTest = function(x, y) { - // TODO: make this more efficient with better datastructures - // for this.bars, this.points and this.slices - - var f = MochiKit.Format.twoDigitFloat; - - if ((this.style == "bar") && this.bars && (this.bars.length > 0)) { - for (var i = 0; i < this.bars.length; i++) { - var bar = this.bars[i]; - if ((x >= bar.x) && (x <= bar.x + bar.w) - && (y >= bar.y) && (y - bar.y <= bar.h)) - return bar; - } - } - - else if (this.style == "line") { - if (this.hitTestCache.x2maxy == null) { - this._regenerateHitTestCache(); - } - - // 1. find the xvalues that equal or closest to the give x - var xval = x / this.xscale; - var xvalues = this.hitTestCache.xvalues; - var xbefore = null; - var xafter = null; - - for (var i = 1; i < xvalues.length; i++) { - if (xvalues[i] > xval) { - xbefore = xvalues[i-1]; - xafter = xvalues[i]; - break; - } - } - - if ((xbefore != null)) { - var ybefore = this.hitTestCache.x2maxy[xbefore]; - var yafter = this.hitTestCache.x2maxy[xafter]; - var yval = (1.0 - y)/this.yscale; - - // interpolate whether we will fall inside or outside - var gradient = (yafter - ybefore) / (xafter - xbefore); - var projmaxy = ybefore + gradient * (xval - xbefore); - if (projmaxy >= yval) { - // inside the highest curve (roughly) - var obj = {xval: xval, yval: yval, - xafter: xafter, yafter: yafter, - xbefore: xbefore, ybefore: ybefore, - yprojected: projmaxy - }; - return obj; - } - } - } - - else if (this.style == "pie") { - var dist = Math.sqrt((y-0.5)*(y-0.5) + (x-0.5)*(x-0.5)); - if (dist > this.options.pieRadius) - return null; - - // TODO: actually doesn't work if we don't know how the Canvas - // lays it out, need to fix! - var angle = Math.atan2(y - 0.5, x - 0.5) - Math.PI/2; - for (var i = 0; i < this.slices.length; i++) { - var slice = this.slices[i]; - if (slice.startAngle < angle && slice.endAngle >= angle) - return slice; - } - } - - return null; -}; - -// Reports valid position rectangle for X value (only valid for bar charts) -PlotKit.Layout.prototype.rectForX = function(x) { - return null; + this._evaluateLineCharts(); + this._evaluateLineTicks(); }; -// Reports valid angles through which X value encloses (only valid for pie charts) -PlotKit.Layout.prototype.angleRangeForX = function(x) { - return null; -}; // -------------------------------------------------------------------- // START Internal Functions // -------------------------------------------------------------------- PlotKit.Layout.prototype._evaluateLimits = function() { - // take all values from all datasets and find max and min - var map = PlotKit.Base.map; - var items = PlotKit.Base.items; - var itemgetter = MochiKit.Base.itemgetter; - var collapse = PlotKit.Base.collapse; - var listMin = MochiKit.Base.listMin; - var listMax = MochiKit.Base.listMax; - var isNil = MochiKit.Base.isUndefinedOrNull; + this.minxval = this.maxxval = null; + for (var name in this.datasets) { + var series = this.datasets[name]; + var x1 = series[0][0]; + if (!this.minxval || x1 < this.minxval) this.minxval = x1; - - var all = collapse(map(itemgetter(1), items(this.datasets))); - if (isNil(this.options.xAxis)) { - if (this.options.xOriginIsZero) - this.minxval = 0; - else - this.minxval = listMin(map(parseFloat, map(itemgetter(0), all))); - - this.maxxval = listMax(map(parseFloat, map(itemgetter(0), all))); - } - else { - this.minxval = this.options.xAxis[0]; - this.maxxval = this.options.xAxis[1]; - this.xscale = this.maxval - this.minxval; - } - - if (isNil(this.options.yAxis)) { - if (this.options.yOriginIsZero) - this.minyval = 0; - else - this.minyval = listMin(map(parseFloat, map(itemgetter(1), all))); - - this.maxyval = listMax(map(parseFloat, map(itemgetter(1), all))); + var x2 = series[series.length - 1][0]; + if (!this.maxxval || x2 > this.maxxval) this.maxxval = x2; } - else { - this.minyval = this.options.yAxis[0]; - this.maxyval = this.options.yAxis[1]; - this.yscale = this.maxyval - this.minyval; - } - -}; - -PlotKit.Layout.prototype._evaluateScales = function() { - var isNil = MochiKit.Base.isUndefinedOrNull; - this.xrange = this.maxxval - this.minxval; - if (this.xrange == 0) - this.xscale = 1.0; - else - this.xscale = 1/this.xrange; + this.xscale = (this.xrange != 0 ? 1/this.xrange : 1.0); + this.minyval = this.options.yAxis[0]; + this.maxyval = this.options.yAxis[1]; this.yrange = this.maxyval - this.minyval; - if (this.yrange == 0) - this.yscale = 1.0; - else - this.yscale = 1/this.yrange; -}; - -PlotKit.Layout.prototype._uniqueXValues = function() { - var collapse = PlotKit.Base.collapse; - var map = PlotKit.Base.map; - var uniq = PlotKit.Base.uniq; - var getter = MochiKit.Base.itemgetter; - var items = PlotKit.Base.items; - - var xvalues = map(parseFloat, map(getter(0), collapse(map(getter(1), items(this.datasets))))); - xvalues.sort(MochiKit.Base.compare); - return uniq(xvalues); + this.yscale = (this.yrange != 0 ? 1/this.yrange : 1.0); }; -// Create the bars -PlotKit.Layout.prototype._evaluateBarCharts = function() { - var items = PlotKit.Base.items; - - var setCount = items(this.datasets).length; - - // work out how far separated values are - var xdelta = 10000000; - var xvalues = this._uniqueXValues(); - for (var i = 1; i < xvalues.length; i++) { - xdelta = Math.min(Math.abs(xvalues[i] - xvalues[i-1]), xdelta); - } - - var barWidth = 0; - var barWidthForSet = 0; - var barMargin = 0; - if (xvalues.length == 1) { - // note we have to do something smarter if we only plot one value - xdelta = 1.0; - this.xscale = 1.0; - this.minxval = xvalues[0]; - barWidth = 1.0 * this.options.barWidthFillFraction; - barWidthForSet = barWidth/setCount; - barMargin = (1.0 - this.options.barWidthFillFraction)/2; - } - else { - // readjust xscale to fix with bar charts - if (this.xrange == 1) { - this.xscale = 0.5; - } - else if (this.xrange == 2) { - this.xscale = 1/3.0; - } - else { - this.xscale = (1.0 - xdelta/this.xrange)/this.xrange; - } - barWidth = xdelta * this.xscale * this.options.barWidthFillFraction; - barWidthForSet = barWidth / setCount; - barMargin = xdelta * this.xscale * (1.0 - this.options.barWidthFillFraction)/2; - } - - this.minxdelta = xdelta; // need this for tick positions - - // add all the rects - this.bars = new Array(); - var i = 0; - for (var setName in this.datasets) { - var dataset = this.datasets[setName]; - if (PlotKit.Base.isFuncLike(dataset)) continue; - for (var j = 0; j < dataset.length; j++) { - var item = dataset[j]; - var rect = { - x: ((parseFloat(item[0]) - this.minxval) * this.xscale) + (i * barWidthForSet) + barMargin, - y: 1.0 - ((parseFloat(item[1]) - this.minyval) * this.yscale), - w: barWidthForSet, - h: ((parseFloat(item[1]) - this.minyval) * this.yscale), - xval: parseFloat(item[0]), - yval: parseFloat(item[1]), - name: setName - }; - if ((rect.x >= 0.0) && (rect.x <= 1.0) && - (rect.y >= 0.0) && (rect.y <= 1.0)) { - this.bars.push(rect); - } - } - i++; - } -}; - -// Create the horizontal bars -PlotKit.Layout.prototype._evaluateHorizBarCharts = function() { - var items = PlotKit.Base.items; - - var setCount = items(this.datasets).length; - - // work out how far separated values are - var xdelta = 10000000; - var xvalues = this._uniqueXValues(); - for (var i = 1; i < xvalues.length; i++) { - xdelta = Math.min(Math.abs(xvalues[i] - xvalues[i-1]), xdelta); - } - - var barWidth = 0; - var barWidthForSet = 0; - var barMargin = 0; - - // work out how far each far each bar is separated - if (xvalues.length == 1) { - // do something smarter if we only plot one value - xdelta = 1.0; - this.xscale = 1.0; - this.minxval = xvalues[0]; - barWidth = 1.0 * this.options.barWidthFillFraction; - barWidthForSet = barWidth/setCount; - barMargin = (1.0 - this.options.barWidthFillFraction)/2; - } - else { - // readjust yscale to fix with bar charts - this.xscale = (1.0 - xdelta/this.xrange)/this.xrange; - barWidth = xdelta * this.xscale * this.options.barWidthFillFraction; - barWidthForSet = barWidth / setCount; - barMargin = xdelta * this.xscale * (1.0 - this.options.barWidthFillFraction)/2; - } - - this.minxdelta = xdelta; // need this for tick positions - - // add all the rects - this.bars = new Array(); - var i = 0; - for (var setName in this.datasets) { - var dataset = this.datasets[setName]; - if (PlotKit.Base.isFuncLike(dataset)) continue; - for (var j = 0; j < dataset.length; j++) { - var item = dataset[j]; - var rect = { - y: ((parseFloat(item[0]) - this.minxval) * this.xscale) + (i * barWidthForSet) + barMargin, - x: 0.0, - h: barWidthForSet, - w: ((parseFloat(item[1]) - this.minyval) * this.yscale), - xval: parseFloat(item[0]), - yval: parseFloat(item[1]), - name: setName - }; - - // limit the x, y values so they do not overdraw - if (rect.y <= 0.0) { - rect.y = 0.0; - } - if (rect.y >= 1.0) { - rect.y = 1.0; - } - if ((rect.x >= 0.0) && (rect.x <= 1.0)) { - this.bars.push(rect); - } - } - i++; - } -}; - - // Create the line charts PlotKit.Layout.prototype._evaluateLineCharts = function() { - var items = PlotKit.Base.items; - - var setCount = items(this.datasets).length; - // add all the rects this.points = new Array(); - var i = 0; for (var setName in this.datasets) { var dataset = this.datasets[setName]; - if (PlotKit.Base.isFuncLike(dataset)) continue; - dataset.sort(function(a, b) { return compare(parseFloat(a[0]), parseFloat(b[0])); }); for (var j = 0; j < dataset.length; j++) { var item = dataset[j]; var point = { @@ -518,239 +105,28 @@ PlotKit.Layout.prototype._evaluateLineCharts = function() { this.points.push(point); } } - i++; - } -}; - -// Create the pie charts -PlotKit.Layout.prototype._evaluatePieCharts = function() { - var items = PlotKit.Base.items; - var sum = MochiKit.Iter.sum; - var getter = MochiKit.Base.itemgetter; - - var setCount = items(this.datasets).length; - - // we plot the y values of the first dataset - var dataset = items(this.datasets)[0][1]; - var total = sum(map(getter(1), dataset)); - - this.slices = new Array(); - var currentAngle = 0.0; - for (var i = 0; i < dataset.length; i++) { - var fraction = dataset[i][1] / total; - var startAngle = currentAngle * Math.PI * 2; - var endAngle = (currentAngle + fraction) * Math.PI * 2; - - var slice = {fraction: fraction, - xval: dataset[i][0], - yval: dataset[i][1], - startAngle: startAngle, - endAngle: endAngle - }; - if (dataset[i][1] != 0) { - this.slices.push(slice); - } - currentAngle += fraction; - } -}; - -PlotKit.Layout.prototype._evaluateLineTicksForXAxis = function() { - var isNil = MochiKit.Base.isUndefinedOrNull; - - if (this.options.xTicks) { - // we use use specified ticks with optional labels - - this.xticks = new Array(); - var makeTicks = function(tick) { - var label = tick.label; - if (isNil(label)) - label = tick.v.toString(); - var pos = this.xscale * (tick.v - this.minxval); - if ((pos >= 0.0) && (pos <= 1.0)) { - this.xticks.push([pos, label]); - } - }; - MochiKit.Iter.forEach(this.options.xTicks, bind(makeTicks, this)); - } - else if (this.options.xNumberOfTicks) { - // we use defined number of ticks as hint to auto generate - var xvalues = this._uniqueXValues(); - var roughSeparation = this.xrange / this.options.xNumberOfTicks; - var tickCount = 0; - - this.xticks = new Array(); - for (var i = 0; i <= xvalues.length; i++) { - if ((xvalues[i] - this.minxval) >= (tickCount * roughSeparation)) { - var pos = this.xscale * (xvalues[i] - this.minxval); - if ((pos > 1.0) || (pos < 0.0)) - continue; - this.xticks.push([pos, xvalues[i]]); - tickCount++; - } - if (tickCount > this.options.xNumberOfTicks) - break; - } - } -}; - -PlotKit.Layout.prototype._evaluateLineTicksForYAxis = function() { - var isNil = MochiKit.Base.isUndefinedOrNull; - - - if (this.options.yTicks) { - this.yticks = new Array(); - var makeTicks = function(tick) { - var label = tick.label; - if (isNil(label)) - label = tick.v.toString(); - var pos = 1.0 - (this.yscale * (tick.v - this.minyval)); - if ((pos >= 0.0) && (pos <= 1.0)) { - this.yticks.push([pos, label]); - } - }; - MochiKit.Iter.forEach(this.options.yTicks, bind(makeTicks, this)); - } - else if (this.options.yNumberOfTicks) { - // We use the optionally defined number of ticks as a guide - this.yticks = new Array(); - - // if we get this separation right, we'll have good looking graphs - var roundInt = PlotKit.Base.roundInterval; - var prec = this.options.yTickPrecision; - var roughSeparation = roundInt(this.yrange, - this.options.yNumberOfTicks, prec); - - // round off each value of the y-axis to the precision - // eg. 1.3333 at precision 1 -> 1.3 - for (var i = 0; i <= this.options.yNumberOfTicks; i++) { - var yval = this.minyval + (i * roughSeparation); - var pos = 1.0 - ((yval - this.minyval) * this.yscale); - if ((pos > 1.0) || (pos < 0.0)) - continue; - this.yticks.push([pos, MochiKit.Format.roundToFixed(yval, prec)]); - } } }; PlotKit.Layout.prototype._evaluateLineTicks = function() { - this._evaluateLineTicksForXAxis(); - this._evaluateLineTicksForYAxis(); -}; - -PlotKit.Layout.prototype._evaluateBarTicks = function() { - this._evaluateLineTicks(); - var centerInBar = function(tick) { - return [tick[0] + (this.minxdelta * this.xscale)/2, tick[1]]; - }; - this.xticks = MochiKit.Base.map(bind(centerInBar, this), this.xticks); - - if (this.options.barOrientation == "horizontal") { - // swap scales - var tempticks = this.xticks; - this.xticks = this.yticks; - this.yticks = tempticks; - - // we need to invert the "yaxis" (which is now the xaxis when drawn) - var invert = function(tick) { - return [1.0 - tick[0], tick[1]]; - } - this.xticks = MochiKit.Base.map(invert, this.xticks); - } -}; - -PlotKit.Layout.prototype._evaluatePieTicks = function() { - var isNil = MochiKit.Base.isUndefinedOrNull; - var formatter = MochiKit.Format.numberFormatter("#%"); - - this.xticks = new Array(); - if (this.options.xTicks) { - // make a lookup dict for x->slice values - var lookup = new Array(); - for (var i = 0; i < this.slices.length; i++) { - lookup[this.slices[i].xval] = this.slices[i]; - } - - for (var i =0; i < this.options.xTicks.length; i++) { - var tick = this.options.xTicks[i]; - var slice = lookup[tick.v]; - var label = tick.label; - if (slice) { - if (isNil(label)) - label = tick.v.toString(); - label += " (" + formatter(slice.fraction) + ")"; - this.xticks.push([tick.v, label]); - } - } - } - else { - // we make our own labels from all the slices - for (var i =0; i < this.slices.length; i++) { - var slice = this.slices[i]; - var label = slice.xval + " (" + formatter(slice.fraction) + ")"; - this.xticks.push([slice.xval, label]); - } - } -}; - -PlotKit.Layout.prototype._regenerateHitTestCache = function() { - this.hitTestCache.xvalues = this._uniqueXValues(); - this.hitTestCache.xlookup = new Array(); - this.hitTestCache.x2maxy = new Array(); - - var listMax = MochiKit.Base.listMax; - var itemgetter = MochiKit.Base.itemgetter; - var map = MochiKit.Base.map; - - // generate a lookup table for x values to y values - var setNames = keys(this.datasets); - for (var i = 0; i < setNames.length; i++) { - var dataset = this.datasets[setNames[i]]; - for (var j = 0; j < dataset.length; j++) { - var xval = dataset[j][0]; - var yval = dataset[j][1]; - if (this.hitTestCache.xlookup[xval]) - this.hitTestCache.xlookup[xval].push([yval, setNames[i]]); - else - this.hitTestCache.xlookup[xval] = [[yval, setNames[i]]]; - } - } - - for (var x in this.hitTestCache.xlookup) { - var yvals = this.hitTestCache.xlookup[x]; - this.hitTestCache.x2maxy[x] = listMax(map(itemgetter(0), yvals)); - } - - -}; - -// -------------------------------------------------------------------- -// END Internal Functions -// -------------------------------------------------------------------- - - -// Namespace Iniitialisation - -PlotKit.LayoutModule = {}; -PlotKit.LayoutModule.Layout = PlotKit.Layout; - -PlotKit.LayoutModule.EXPORT = [ - "Layout" -]; - -PlotKit.LayoutModule.EXPORT_OK = []; - -PlotKit.LayoutModule.__new__ = function() { - var m = MochiKit.Base; - - m.nameFunctions(this); - - this.EXPORT_TAGS = { - ":common": this.EXPORT, - ":all": m.concat(this.EXPORT, this.EXPORT_OK) - }; + this.xticks = new Array(); + for (var i = 0; i < this.options.xTicks.length; i++) { + var tick = this.options.xTicks[i]; + var label = tick.label; + var pos = this.xscale * (tick.v - this.minxval); + if ((pos >= 0.0) && (pos <= 1.0)) { + this.xticks.push([pos, label]); + } + } + + this.yticks = new Array(); + for (var i = 0; i < this.options.yTicks.length; i++) { + var tick = this.options.yTicks[i]; + var label = tick.label; + var pos = 1.0 - (this.yscale * (tick.v - this.minyval)); + if ((pos >= 0.0) && (pos <= 1.0)) { + this.yticks.push([pos, label]); + } + } }; -PlotKit.LayoutModule.__new__(); -MochiKit.Base._exportSymbols(this, PlotKit.LayoutModule); - -