Commit 865688a1 authored by olau@iola.dk's avatar olau@iola.dk

Some further tweaks to time series handling, implemented support for tickSize...

Some further tweaks to time series handling, implemented support for tickSize and minTickSize, automatic detection of no. ticks

git-svn-id: https://flot.googlecode.com/svn/trunk@45 1e0a6537-2640-0410-bfb7-f154510ff394
parent dfba9f38
......@@ -148,6 +148,8 @@ Customizing the axes
max: null or number
autoscaleMargin: null or number
ticks: null or number or ticks array or (fn: range -> ticks array)
tickSize: number or array
minTickSize: number or array
tickFormatter: (fn: number, object -> string) or string
tickDecimals: null or number
}
......@@ -171,41 +173,25 @@ nearest whole tick. The default value is "null" for the x axis and
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. You can tweak how many it tries to generate by setting
"ticks" to a number. 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. If you don't want ticks, set
"ticks" to 0 or an empty array.
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 as "tickFormatter". 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 formatter(val, axis) {
return val.toFixed(axis.tickDecimals);
}
some for you. The algorithm has two passes. It first estimates how
many ticks would be reasonable and uses this number to compute a nice
round tick interval size. Then it generates the ticks.
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";
}
You can specify how many ticks the algorithm aims for by setting
"ticks" to a number. The algorithm always tries to generate reasonably
round tick values so even if you ask for three ticks, you might get
five if that fits better with the rounding. If you don't want ticks,
set "ticks" to 0 or an empty array.
Another option is to skip the rounding part and directly set the tick
interval size with "tickSize". If you set it to 2, you'll get ticks at
2, 4, 6, etc. Alternatively, you can specify that you just don't want
ticks at a size less than a specific tick size with "minTickSize".
Note that for time series, the format is an array like [2, "month"],
see the next section.
If you want to override the tick algorithm, you can specify an array
to "ticks", either like this:
If you want to completely override the tick algorithm, you can specify
an array for "ticks", either like this:
ticks: [0, 1.2, 2.4]
......@@ -220,24 +206,50 @@ generator that spits out intervals of pi, suitable for use on the x
axis for trigonometric functions:
function piTickGenerator(axis) {
var res = [], i = Math.ceil(axis.min / Math.PI);
while (true) {
var res = [], i = Math.floor(axis.min / Math.PI);
do {
var v = i * Math.PI;
if (v > axis.max)
break;
res.push([v, i + "\u03c0"]);
++i;
}
} while (v < axis.max);
return res;
}
You can control how the ticks look like with "tickDecimals", the
number of decimals to display (default is auto-detected).
Alternatively, for ultimate control you can provide a function to
"tickFormatter". The function is passed two parameters, the tick value
and an "axis" object with information, and should return a string. The
default formatter looks like this:
function formatter(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 (or specified by you). 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";
}
Time series data
================
The time series support in Flot is based on Javascript timestamps,
i.e. everywhere a time value is expected or passed over, a Javascript
i.e. everywhere a time value is expected or handed over, a Javascript
timestamp number is used. This is not the same as a Date object. A
Javascript timestamp is the number of milliseconds since January 1,
1970 00:00:00. This is almost the same as Unix timestamps, except it's
......@@ -264,10 +276,11 @@ the axis mode, Flot will automatically generate relevant ticks and
format them. As always, you can tweak the ticks via the "ticks"
option. Again the values should be timestamps, not Date objects!
Formatting is controlled separately through the following axis
options:
Tick generation and formatting is controlled separately through the
following axis options:
xaxis, yaxis: {
minTickSize
timeformat: null or format string
monthNames: null or array of size 12 of strings
}
......@@ -306,10 +319,17 @@ which will format December 24 as 24/12:
return d.getDate() + "/" + (d.getMonth() + 1);
}
For the time mode the axis object contains an additional
"tickSizeUnit" which is one of "second", "minute", "hour", "day",
"month" and "year". So if axis.tickSize is 2 and axis.tickSizeUnit is
"day", the ticks have been produced with two days in-between.
Note that for the time mode "tickSize" and "minTickSize" are a bit
special in that they are arrays on the form "[value, unit]" where unit
is one of "second", "minute", "hour", "day", "month" and "year". So
you can specify
minTickSize: [1, "month"]
to get a tick interval size of at least 1 month and correspondingly,
if axis.tickSize is [2, "day"] in the tick formatter, the ticks have
been produced with two days in-between.
Customizing the data series
......@@ -370,10 +390,12 @@ Customizing the grid
====================
grid: {
color: color,
backgroundColor: color or null,
tickColor: color,
labelMargin: number,
color: color
backgroundColor: color or null
tickColor: color
labelMargin: number
coloredAreas: array of areas or (fn: plot area -> array of areas)
coloredAreasColor: color
clickable: boolean
}
......@@ -389,8 +411,33 @@ of the page with CSS.
between tick labels and the grid.
"coloredAreas" is an array of areas that will be drawn on top of the
background. You can either specify an array of objects with { x1, y1,
x2, y2 } or a function that returns such an array given the plot area
as { xmin, xmax, ymin, ymax }. The default color of the areas are
"coloredAreasColor". You can override the color of individual areas by
specifying "color" in the area object.
Here's an example array:
coloredAreas: [ { x1: 0, y1: 10, x2: 2, y2: 15, color: "#bb0000" }, ... ]
If you leave out one of the values, that value is assumed to go to the
border of the plot. So for example { x1: 0, x2: 2 } means an area that
extends from the top to the bottom of the plot in the x range 0-2.
An example function might look like this:
coloredAreas: function (plotarea) {
var areas = [];
for (var x = Math.floor(plotarea.xmin); x < plotarea.xmax; x += 2)
areas.push({ x1: x, x2: x + 1 });
return areas;
}
If you set "clickable" to true, the plot will listen for click events
on the plot are and fire a "plotclick" event on the placeholder with
on the plot area and fire a "plotclick" event on the placeholder with
an object { x: number, y: number } as parameter when one occurs. The
returned coordinates will be in the unit of the plot (not in pixels).
You can use it like this:
......
Flot x.x
--------
Time series support. Specify axis.mode: "time", put in Javascript
timestamps as data, and Flot will automatically spit out sensible
ticks. Take a look at the two new examples. The format can be
customized with axis.timeformat and axis.monthNames, or if that fails
with axis.tickFormatter.
Support for colored background areas via grid.coloredAreas. Specify an
array of { x1, y1, x2, y2 } objects or a function that returns these
given { xmin, xmax, ymin, ymax }.
The default number of ticks to aim for is now dependent on the size of
the plot in pixels. Support for customizing tick interval sizes
directly with axis.minTickSize and axis.tickSize.
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.
The option axis.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.
API changes: deprecated axis.noTicks in favor of just specifying the
number as axis.ticks.
Flot 0.3
......
......@@ -4,10 +4,10 @@ say why or come up with a patch. :-)
pending
- split out autoscaleMargin into a snapToTicks
- autodetect a sensible ticks setting
grid configuration
- how ticks look like
- consider setting default grid colors from each other?
selection
- user should be able to cancel selection with escape
......
......@@ -23,7 +23,7 @@
<button id="nineties">1990-2000</button>
<button id="ninetynine">1999</button></p>
<p>The timestamps <em>must</em> be specified as Javascript
<p>The timestamps must be specified as Javascript
timestamps, as milliseconds since January 1, 1970 00:00. This is
like Unix timestamps, but in milliseconds instead of seconds
(remember to multiply with 1000!).</p>
......@@ -49,6 +49,7 @@ $(function () {
$("#ninetynine").click(function () {
$.plot($("#placeholder"), [d], { xaxis: {
mode: "time",
minTickSize: [1, "month"],
min: (new Date("1999/01/01")).getTime(),
max: (new Date("2000/01/01")).getTime()
} });
......
......@@ -22,7 +22,7 @@
$(function () {
var d = [[1196463600000, 0], [1196550000000, 0], [1196636400000, 0], [1196722800000, 77], [1196809200000, 3636], [1196895600000, 3575], [1196982000000, 2736], [1197068400000, 1086], [1197154800000, 676], [1197241200000, 1205], [1197327600000, 906], [1197414000000, 710], [1197500400000, 639], [1197586800000, 540], [1197673200000, 435], [1197759600000, 301], [1197846000000, 575], [1197932400000, 481], [1198018800000, 591], [1198105200000, 608], [1198191600000, 459], [1198278000000, 234], [1198364400000, 1352], [1198450800000, 686], [1198537200000, 279], [1198623600000, 449], [1198710000000, 468], [1198796400000, 392], [1198882800000, 282], [1198969200000, 208], [1199055600000, 229], [1199142000000, 177], [1199228400000, 374], [1199314800000, 436], [1199401200000, 404], [1199487600000, 253], [1199574000000, 218], [1199660400000, 476], [1199746800000, 462], [1199833200000, 448], [1199919600000, 442], [1200006000000, 403], [1200092400000, 204], [1200178800000, 194], [1200265200000, 327], [1200351600000, 374], [1200438000000, 507], [1200524400000, 546], [1200610800000, 482], [1200697200000, 283], [1200783600000, 221], [1200870000000, 483], [1200956400000, 523], [1201042800000, 528], [1201129200000, 483], [1201215600000, 452], [1201302000000, 270], [1201388400000, 222], [1201474800000, 439], [1201561200000, 559], [1201647600000, 521], [1201734000000, 477], [1201820400000, 442], [1201906800000, 252], [1201993200000, 236], [1202079600000, 525], [1202166000000, 477], [1202252400000, 386], [1202338800000, 409], [1202425200000, 408], [1202511600000, 237], [1202598000000, 193], [1202684400000, 357], [1202770800000, 414], [1202857200000, 393], [1202943600000, 353], [1203030000000, 364], [1203116400000, 215], [1203202800000, 214], [1203289200000, 356], [1203375600000, 399], [1203462000000, 334], [1203548400000, 348], [1203634800000, 243], [1203721200000, 126], [1203807600000, 157], [1203894000000, 288]];
// helper for returning the week-ends in a period
// helper for returning the weekends in a period
function weekendAreas(plotarea) {
var areas = [];
var d = new Date(plotarea.xmin);
......
......@@ -36,12 +36,12 @@
// mode specific options
tickDecimals: null, // no. of decimals, null means auto
tickSize: null, // number or [number, "unit"]
minTickSize: null, // number or [number, "unit"]
monthNames: null, // list of names of months
timeformat: null, // format string to use
timeformat: null // format string to use
},
yaxis: {
ticks: null,
autoscaleMargin: 0.02
},
points: {
......@@ -257,12 +257,17 @@
}
function prepareTickGeneration(axis, axisOptions) {
var noTicks = 5;
// estimate number of ticks
var noTicks;
if (typeof axisOptions.ticks == "number" && axisOptions.ticks > 0)
noTicks = axisOptions.ticks;
else if (axis == xaxis)
noTicks = canvasWidth / 100;
else
noTicks = canvasHeight / 60;
var delta = (axis.max - axis.min) / noTicks;
var size, generator, unit = "", formatter, i;
var size, generator, unit, formatter, i;
if (axisOptions.mode == "time") {
// pretty handling of time
......@@ -332,103 +337,21 @@
[1, "year"]
];
// a generic tick generator for the well-behaved cases
// where it's simply matter of adding a fixed no. of seconds
var genericTimeGenerator = function(axis) {
var ticks = [];
var step = axis.tickSize * timeUnitSize[axis.tickSizeUnit];
var d = new Date(axis.min);
d.setMilliseconds(0);
if (axis.tickSizeUnit == "second")
d.setSeconds(floorInBase(d.getSeconds(), axis.tickSize));
else if (step >= timeUnitSize.minute)
d.setSeconds(0);
if (axis.tickSizeUnit == "minute")
d.setMinutes(floorInBase(d.getMinutes(), axis.tickSize));
else if (step >= timeUnitSize.hour)
d.setMinutes(0);
if (axis.tickSizeUnit == "hour")
d.setHours(floorInBase(d.getHours(), axis.tickSize));
else if (step >= timeUnitSize.day)
d.setHours(0);
do {
var v = d.getTime();
ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
//console.log(d, "generic", axis.tickSize, axis.tickSizeUnit)
d.setTime(v + step);
} while (v < axis.max);
return ticks;
};
var unitGenerator = {
"second": genericTimeGenerator,
"minute": genericTimeGenerator,
"hour": genericTimeGenerator,
"day": genericTimeGenerator,
"month": function(axis) {
var ticks = [];
var d = new Date(axis.min);
d.setMilliseconds(0);
d.setSeconds(0);
d.setMinutes(0);
d.setHours(0);
d.setDate(1);
d.setMonth(floorInBase(d.getMonth(), axis.tickSize));
var carry = 0;
do {
var v = d.getTime();
ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
//console.log(d, "month", axis.tickSize)
if (axis.tickSize < 1) {
// a bit complicated - we'll divide the month
// up but we need to take care of fractions
// so we don't end up in the middle of a day
d.setDate(1);
var start = d.getTime();
d.setMonth(d.getMonth() + 1);
var end = d.getTime();
d.setTime(v + carry * timeUnitSize.hour + (end - start) * axis.tickSize);
carry = d.getHours();
d.setHours(0);
}
else
d.setMonth(d.getMonth() + axis.tickSize);
} while (v < axis.max);
return ticks;
},
"year": function(axis) {
var ticks = [];
var d = new Date(axis.min);
d.setMilliseconds(0);
d.setSeconds(0);
d.setMinutes(0);
d.setHours(0);
d.setDate(1);
d.setMonth(0);
d.setFullYear(floorInBase(d.getFullYear(), axis.tickSize));
do {
var v = d.getTime();
ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
//console.log(d, "year", axis.tickSize);
d.setFullYear(d.getFullYear() + axis.tickSize);
} while (v < axis.max);
return ticks;
}
var minSize = 0;
if (axisOptions.minTickSize != null) {
if (typeof axisOptions.tickSize == "number")
minSize = axisOptions.tickSize;
else
minSize = axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]];
}
for (i = 0; i < spec.length - 1; ++i)
if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
+ spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2)
+ spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
&& spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
break;
size = spec[i][0];
unit = spec[i][1];
generator = unitGenerator[unit];
// special-case the possibility of several years
if (unit == "year") {
......@@ -446,6 +369,73 @@
size *= magn;
}
if (axisOptions.tickSize) {
size = axisOptions.tickSize[0];
unit = axisOptions.tickSize[1];
}
generator = function(axis) {
var ticks = [],
tickSize = axis.tickSize[0], unit = axis.tickSize[1],
d = new Date(axis.min);
var step = tickSize * timeUnitSize[unit];
if (unit == "second")
d.setSeconds(floorInBase(d.getSeconds(), tickSize));
if (unit == "minute")
d.setMinutes(floorInBase(d.getMinutes(), tickSize));
if (unit == "hour")
d.setHours(floorInBase(d.getHours(), tickSize));
if (unit == "month")
d.setMonth(floorInBase(d.getMonth(), tickSize));
if (unit == "year")
d.setFullYear(floorInBase(d.getFullYear(), tickSize));
// reset smaller components
d.setMilliseconds(0);
if (step >= timeUnitSize.minute)
d.setSeconds(0);
if (step >= timeUnitSize.hour)
d.setMinutes(0);
if (step >= timeUnitSize.day)
d.setHours(0);
if (step >= timeUnitSize.day * 4)
d.setDate(1);
if (step >= timeUnitSize.year)
d.setMonth(0);
var carry = 0;
do {
var v = d.getTime();
ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
if (unit == "month") {
if (tickSize < 1) {
// a bit complicated - we'll divide the month
// up but we need to take care of fractions
// so we don't end up in the middle of a day
d.setDate(1);
var start = d.getTime();
d.setMonth(d.getMonth() + 1);
var end = d.getTime();
d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
carry = d.getHours();
d.setHours(0);
}
else
d.setMonth(d.getMonth() + tickSize);
}
else if (unit == "year") {
d.setFullYear(d.getFullYear() + tickSize);
}
else
d.setTime(v + step);
} while (v < axis.max);
return ticks;
};
formatter = function (v, axis) {
var d = new Date(v);
......@@ -453,7 +443,7 @@
if (axisOptions.timeformat != null)
return formatDate(d, axisOptions.timeformat, axisOptions.monthNames);
var t = axis.tickSize * timeUnitSize[axis.tickSizeUnit];
var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
var span = axis.max - axis.min;
if (t < timeUnitSize.minute)
......@@ -504,7 +494,15 @@
size = 10;
size *= magn;
if (axisOptions.minTickSize != null && size < axisOptions.minTickSize)
size = axisOptions.minTickSize;
if (axisOptions.tickSize != null)
size = axisOptions.tickSize;
axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec);
generator = function (axis) {
var ticks = [];
var start = floorInBase(axis.min, axis.tickSize);
......@@ -523,9 +521,8 @@
};
}
axis.tickSize = size;
axis.tickSize = unit ? [size, unit] : size;
axis.tickGenerator = generator;
axis.tickSizeUnit = unit;
if ($.isFunction(axisOptions.tickFormatter))
axis.tickFormatter = function (v, axis) { return "" + axisOptions.tickFormatter(v, axis); };
else
......@@ -656,7 +653,6 @@
if ($.isFunction(areas))
areas = areas({ xmin: xaxis.min, xmax: xaxis.max, ymin: yaxis.min, ymax: yaxis.max });
ctx.fillStyle = options.grid.coloredAreasColor;
for (i = 0; i < areas.length; ++i) {
var a = areas[i];
......@@ -685,6 +681,8 @@
if (a.x1 >= xaxis.max || a.x2 <= xaxis.min || a.x1 == a.x2
|| a.y1 >= yaxis.max || a.y2 <= yaxis.min || a.y1 == a.y2)
continue;
ctx.fillStyle = a.color || options.grid.coloredAreasColor;
ctx.fillRect(Math.floor(tHoz(a.x1)), Math.floor(tVert(a.y2)),
Math.floor(tHoz(a.x2) - tHoz(a.x1)), Math.floor(tVert(a.y1) - tVert(a.y2)));
}
......
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