Commit 8cf42618 authored by olau@iola.dk's avatar olau@iola.dk

Cleaned up the scaling algorithm and some of the tick stuff

git-svn-id: https://flot.googlecode.com/svn/trunk@39 1e0a6537-2640-0410-bfb7-f154510ff394
parent 3b80148d
......@@ -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:
......
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
--------
......
......@@ -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)
// 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;
var margin = axisOptions.autoscaleMargin;
if (margin != null) {
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
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;
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 = '<div style="font-size:smaller;color:' + options.grid.color + '">';
// 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 += '<div style="position:absolute;top:' + (plotOffset.top + plotHeight + options.grid.labelMargin) + 'px;left:' + (plotOffset.left + tHoz(tick.v) - xBoxWidth/2) + 'px;width:' + xBoxWidth + 'px;text-align:center" class="gridLabel">' + tick.label + "</div>";
}
......@@ -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 += '<div style="position:absolute;top:' + (plotOffset.top + tVert(tick.v) - labelMaxHeight/2) + 'px;left:0;width:' + labelMaxWidth + 'px;text-align:right" class="gridLabel">' + tick.label + "</div>";
}
......@@ -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) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment