diff --git a/API.txt b/API.txt index db00766..dd376f7 100644 --- a/API.txt +++ b/API.txt @@ -133,38 +133,62 @@ Customizing the axes xaxis, yaxis: { ticks: null or ticks array or (fn: range -> ticks array), noTicks: number, - tickFormatter: fn: number -> string, + tickFormatter: fn: number, object -> string, tickDecimals: null or number, min: null or number, max: null or number, - autoscaleMargin: number + autoscaleMargin: null or number } The two axes have the same kind of options. The most import are -min/max that specifies the precise minimum/maximum value on the scale. -If you don't specify a value, it will automatically be chosen by a -scaling algorithm that is based on perceived reasonable tick values. -The "autoscaleMargin" is the fraction of margin that the scaling -algorithm will add to avoid that the outermost points ends up on the -grid outline. The default value is 0 for the x axis and 0.02 for the y -axis. +"min"/"max" that specifies the precise minimum/maximum value on the +scale. If you don't specify a value, it will automatically be chosen +by a scaling algorithm based on the minimum/maximum data values. + +The "autoscaleMargin" is a bit esoteric: it's the fraction of margin +that the scaling algorithm will add to avoid that the outermost points +ends up on the grid outline. Note that this margin is only applied +when a min or max value is not explicitly set. If a margin is +specified, the plot will furthermore extend the axis end-point to the +nearest whole tick. The default value is "null" for the x axis and +0.02 for the y axis which seems appropriate for most cases. The rest of the options deal with the ticks. If you don't specify any ticks, a tick generator algorithm will make some for you based on the -"noTicks" setting. The algorithm always tries to generate reasonably -round tick values so even if you ask for 3 ticks, you might get 5 if -that fits better with the rounding. +number of ticks setting, "noTicks". The algorithm always tries to +generate reasonably round tick values so even if you ask for 3 ticks, +you might get 5 if that fits better with the rounding. Never set +"noTicks" to 0, that will just break the auto-detection stuff. If you +don't want ticks, provide an empty "ticks" array as described below. You can control how the ticks look like with "tickDecimals", the number of decimals to display (default is auto-detected), or by -providing a function to "tickFormatter". The function gets one -argument, the tick value, and should return a string. The default -formatter looks like this: +providing a function to "tickFormatter". - function defaultTickFormatter(val) { - return "" + val; +The tick formatter function gets two argument, the tick value and an +optional "axis" object with information, and should return a string. +The default formatter looks like this: + + function defaultTickFormatter(val, axis) { + return val.toFixed(axis.tickDecimals); + } + +The axis object has "min" and "max" with the range of the axis, +"tickDecimals" with the number of decimals to round the value to and +"tickSize" with the size of the interval between ticks as calculated +by the automatic axis scaling algorithm. Here's an example of a +custom formatter: + + function suffixFormatter(val, axis) { + if (val > 1000000) + return (val / 1000000).toFixed(axis.tickDecimals) + " MB"; + else if (val > 1000) + return (val / 1000).toFixed(axis.tickDecimals) + " kB"; + else + return val.toFixed(axis.tickDecimals) + " B"; } + If you want to override the tick algorithm, you can manually specify "ticks" which should be an array of tick values, either like this: diff --git a/NEWS.txt b/NEWS.txt index 82d4398..2204380 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,3 +1,15 @@ +Flot x.x +-------- + +Cleaned up the automatic axis scaling algorithm and fixed how it +interacts with ticks. Also fixed a couple of tick-related corner case +bugs. + +"tickFormatter" now takes a function with two parameters, the second +parameter is an optional object with information about the axis. It +has min, max, tickDecimals, tickSize. + + Flot 0.3 -------- diff --git a/jquery.flot.js b/jquery.flot.js index 16ce8fd..140dc58 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -33,7 +33,7 @@ tickDecimals: null, // no. of decimals, null means auto min: null, // min. value to show, null means set automatically max: null, // max. value to show, null means set automatically - autoscaleMargin: 0 // margin in % to add if auto-setting min/max + autoscaleMargin: null // margin in % to add if auto-setting min/max }, yaxis: { noTicks: 5, @@ -101,12 +101,14 @@ constructCanvas(); bindEvents(); findDataRanges(); - calculateRange(xaxis, options.xaxis); + setRange(xaxis, options.xaxis); + setTickSize(xaxis, options.xaxis); + setTicks(xaxis, options.xaxis); extendXRangeIfNeededByBar(); - calculateRange(yaxis, options.yaxis); - calculateTicks(xaxis, options.xaxis); - calculateTicks(yaxis, options.yaxis); - calculateSpacing(); + setRange(yaxis, options.yaxis); + setTickSize(yaxis, options.yaxis); + setTicks(yaxis, options.yaxis); + setSpacing(); draw(); insertLegend(); @@ -209,31 +211,36 @@ } } - function getTickSize(noTicks, min, max, decimals) { - var delta = (max - min) / noTicks; - var magn = getMagnitude(delta); + function setTickSize(axis, axisOptions) { + var delta = (axis.max - axis.min) / axisOptions.noTicks; + var maxDec = axisOptions.tickDecimals; + var dec = -Math.floor(Math.log(delta) / Math.LN10); + if (maxDec != null && dec > maxDec) + dec = maxDec; + var magn = Math.pow(10, -dec); var norm = delta / magn; // norm is between 1.0 and 10.0 var tickSize = 1; if (norm < 1.5) tickSize = 1; - else if (norm < 2.25) + else if (norm < 3) { tickSize = 2; - else if (norm < 3) - tickSize = 2.5; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + tickSize = 2.5; + ++dec; + } + } else if (norm < 7.5) tickSize = 5; else tickSize = 10; - if (tickSize == 2.5 && decimals == 0) - tickSize = 2; - - tickSize *= magn; - return tickSize; + axis.tickSize = tickSize * magn; + axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec); } - function calculateRange(axis, axisOptions) { + function setRange(axis, axisOptions) { var min = axisOptions.min != null ? axisOptions.min : axis.datamin; var max = axisOptions.max != null ? axisOptions.max : axis.datamax; @@ -249,34 +256,22 @@ max += widen; } - axis.tickSize = getTickSize(axisOptions.noTicks, min, max, axisOptions.tickDecimals); - // consider autoscaling - var margin; - if (axisOptions.min == null) { - // first add in a little margin - margin = axisOptions.autoscaleMargin; - if (margin != 0) { - min -= axis.tickSize * margin; - // make sure we don't go below zero if all - // values are positive + var margin = axisOptions.autoscaleMargin; + if (margin != null) { + if (axisOptions.min == null) { + min -= (max - min) * margin; + // make sure we don't go below zero if all values + // are positive if (min < 0 && axis.datamin >= 0) min = 0; - - min = axis.tickSize * Math.floor(min / axis.tickSize); } - } - if (axisOptions.max == null) { - margin = axisOptions.autoscaleMargin; - if (margin != 0) { - max += axis.tickSize * margin; + if (axisOptions.max == null) { + max += (max - min) * margin; if (max > 0 && axis.datamax <= 0) max = 0; - - max = axis.tickSize * Math.ceil(max / axis.tickSize); } } - axis.min = min; axis.max = max; } @@ -288,17 +283,17 @@ var newmax = xaxis.max; for (var i = 0; i < series.length; ++i) if (series[i].bars.show && series[i].bars.barWidth + xaxis.datamax > newmax) - newmax = xaxis.max + series[i].bars.barWidth; + newmax = xaxis.datamax + series[i].bars.barWidth; xaxis.max = newmax; } } - function defaultTickFormatter(val) { - return "" + val; + function defaultTickFormatter(val, axis) { + return val.toFixed(axis.tickDecimals); } - function calculateTicks(axis, axisOptions) { - var i; + function setTicks(axis, axisOptions) { + var i, v; axis.ticks = []; if (axisOptions.ticks) { @@ -310,43 +305,41 @@ // clean up the user-supplied ticks, copy them over for (i = 0; i < ticks.length; ++i) { - var v, label; + var label = null; var t = ticks[i]; if (typeof(t) == "object") { v = t[0]; if (t.length > 1) label = t[1]; - else - label = axisOptions.tickFormatter(v); } - else { + else v = t; - label = axisOptions.tickFormatter(v); - } + if (label == null) + label = "" + axisOptions.tickFormatter(v, axis); axis.ticks[i] = { v: v, label: label }; } } else { // round to nearest multiple of tick size - var start = axis.tickSize * Math.ceil(axis.min / axis.tickSize); + var start = axis.tickSize * Math.floor(axis.min / axis.tickSize); // then spew out all possible ticks - for (i = 0; start + i * axis.tickSize <= axis.max; ++i) { + i = 0; + do { v = start + i * axis.tickSize; - - // round (this is always needed to fix numerical instability) - var decimals = axisOptions.tickDecimals; - if (decimals == null) - decimals = 1 - Math.floor(Math.log(axis.tickSize) / Math.LN10); - if (decimals < 0) - decimals = 0; - - v = v.toFixed(decimals); - axis.ticks.push({ v: v, label: axisOptions.tickFormatter(v) }); - } + axis.ticks.push({ v: v, label: "" + axisOptions.tickFormatter(v, axis) }); + ++i; + } while (v < axis.max); + } + + if (axisOptions.autoscaleMargin != null) { + if (axisOptions.min == null) + axis.min = axis.ticks[0].v; + if (axisOptions.max == null && axis.ticks.length > 1) + axis.max = axis.ticks[axis.ticks.length - 1].v; } } - function calculateSpacing() { + function setSpacing() { // calculate spacing for labels, using the heuristic // that the longest string is probably the one that takes // up the most space @@ -415,7 +408,7 @@ var i, v; for (i = 0; i < xaxis.ticks.length; ++i) { v = xaxis.ticks[i].v; - if (v == xaxis.min || v == xaxis.max) + if (v <= xaxis.min || v >= xaxis.max) continue; // skip those lying on the axes ctx.moveTo(Math.floor(tHoz(v)) + ctx.lineWidth/2, 0); @@ -424,7 +417,7 @@ for (i = 0; i < yaxis.ticks.length; ++i) { v = yaxis.ticks[i].v; - if (v == yaxis.min || v == yaxis.max) + if (v <= yaxis.min || v >= yaxis.max) continue; ctx.moveTo(0, Math.floor(tVert(v)) + ctx.lineWidth/2); @@ -441,25 +434,18 @@ } function drawLabels() { - var i; - var tick; + var i, tick; var html = '
'; - // calculate width for labels; to avoid measuring the - // widths of the labels, we construct fixed-size boxes and - // put the labels inside them, the fixed-size boxes are - // easy to mid-align - var noLabels = 0; - for (i = 0; i < xaxis.ticks.length; ++i) { - if (xaxis.ticks[i].label) { - ++noLabels; - } - } - var xBoxWidth = plotWidth / noLabels; + // set width for labels; to avoid measuring the widths of + // the labels, we construct fixed-size boxes and put the + // labels inside them, the fixed-size boxes are easy to + // mid-align + var xBoxWidth = plotWidth / 6; // do the x-axis for (i = 0; i < xaxis.ticks.length; ++i) { tick = xaxis.ticks[i]; - if (!tick.label) + if (!tick.label || tick.v < xaxis.min || tick.v > xaxis.max) continue; html += '
' + tick.label + "
"; } @@ -467,7 +453,7 @@ // do the y-axis for (i = 0; i < yaxis.ticks.length; ++i) { tick = yaxis.ticks[i]; - if (!tick.label || tick.label.length == 0) + if (!tick.label || tick.v < yaxis.min || tick.v > yaxis.max) continue; html += '
' + tick.label + "
"; } @@ -1223,11 +1209,6 @@ return plot; }; - function getMagnitude(x) { - return Math.pow(10, Math.floor(Math.log(x) / Math.LN10)); - } - - // color helpers, inspiration from the jquery color animation // plugin by John Resig function Color (r, g, b, a) {