From 865688a15f14e2e466bf0bd424a0ba9f265c9cc9 Mon Sep 17 00:00:00 2001
From: "olau@iola.dk"
Date: Tue, 26 Feb 2008 20:16:10 +0000
Subject: [PATCH] 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
---
API.txt | 145 ++++++++++++++++++++----------
NEWS.txt | 24 ++++-
TODO | 2 +-
examples/time.html | 3 +-
examples/visitors.html | 2 +-
jquery.flot.js | 198 ++++++++++++++++++++---------------------
6 files changed, 219 insertions(+), 155 deletions(-)
diff --git a/API.txt b/API.txt
index 931f08c..60d7f20 100644
--- a/API.txt
+++ b/API.txt
@@ -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:
diff --git a/NEWS.txt b/NEWS.txt
index 2204380..e29fa94 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -1,13 +1,31 @@
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
diff --git a/TODO b/TODO
index 8089b73..db1a740 100644
--- a/TODO
+++ b/TODO
@@ -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
diff --git a/examples/time.html b/examples/time.html
index aed6e0a..754425d 100644
--- a/examples/time.html
+++ b/examples/time.html
@@ -23,7 +23,7 @@
-
The timestamps must be specified as Javascript
+
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!).
@@ -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()
} });
diff --git a/examples/visitors.html b/examples/visitors.html
index 2236991..c8ad737 100644
--- a/examples/visitors.html
+++ b/examples/visitors.html
@@ -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);
diff --git a/jquery.flot.js b/jquery.flot.js
index bfa298f..b468532 100644
--- a/jquery.flot.js
+++ b/jquery.flot.js
@@ -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)));
}