From 7c9ff67b34eb1001eaf143f8c2bd2163a8625a9a Mon Sep 17 00:00:00 2001 From: "olau@iola.dk" Date: Wed, 10 Sep 2008 16:05:30 +0000 Subject: [PATCH] Landed dual axis/hover event/finding nearby datapoints patch git-svn-id: https://flot.googlecode.com/svn/trunk@73 1e0a6537-2640-0410-bfb7-f154510ff394 --- API.txt | 84 +- NEWS.txt | 22 +- TODO | 7 +- examples/dual-axis.html | 38 + examples/index.html | 12 +- examples/interacting.html | 46 +- examples/selection.html | 8 +- .../{real-data.html => turning-series.html} | 0 examples/visitors.html | 10 +- examples/zooming.html | 22 +- jquery.flot.js | 805 +++++++++++------- 11 files changed, 686 insertions(+), 368 deletions(-) create mode 100644 examples/dual-axis.html rename examples/{real-data.html => turning-series.html} (100%) diff --git a/API.txt b/API.txt index e8cfb99..6f72aec 100644 --- a/API.txt +++ b/API.txt @@ -57,6 +57,8 @@ The format of a single series object is as follows: lines: specific lines options, bars: specific bars options, points: specific points options, + xaxis: 1 or 2, + yaxis: 1 or 2, shadowSize: number } @@ -81,6 +83,11 @@ The latter is mostly useful if you let the user add and remove series, in which case you can hard-code the color index to prevent the colors from jumping around between the series. +The "xaxis" and "yaxis" options specify which axis to use, specify 2 +to get the secondary axis (x axis at top or y axis to the right). +E.g., you can use this to make a dual axis plot by specifying +{ yaxis: 2 } for one data series. + The rest of the options are all documented below as they are the same as the default options passed in via the options parameter in the plot commmand. When you specify them for a specific data series, they will @@ -148,7 +155,7 @@ that it will overwrite the contents of the container. Customizing the axes ==================== - xaxis, yaxis: { + xaxis, yaxis, x2axis, y2axis: { mode: null or "time" min: null or number max: null or number @@ -163,7 +170,7 @@ Customizing the axes tickDecimals: null or number } -The two axes have the same kind of options. The "mode" option +The axes have the same kind of options. The "mode" option determines how the data is interpreted, the default of null means as decimal numbers. Use "time" for time series data, see the next section. @@ -447,11 +454,11 @@ Customizing the grid clickable: boolean } -The grid is the thing with the two axes and a number of ticks. "color" +The grid is the thing with the axes and a number of ticks. "color" is the color of the grid itself whereas "backgroundColor" specifies the background color inside the grid area. The default value of null means that the background is transparent. You should only need to set -backgroundColor if want the grid area to be a different color from the +backgroundColor if you want the grid area to be a different color from the page color. Otherwise you might as well just set the background color of the page with CSS. @@ -488,18 +495,42 @@ An example function might look like this: If you set "clickable" to true, the plot will listen for click events 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: +a position and a nearby data item object as parameters. The returned +coordinates are in the unit of the axes (not in pixels). + +If you set "hoverable" to true, the plot will listen for mouse move +events on the plot area and fire a "plothover" event with the same +parameters as the "plotclick" event. + +You can use "plotclick" and "plothover" events like this: $.plot($("#placeholder"), [ d ], { grid: { clickable: true } }); - $("#placeholder").bind("plotclick", function (e, pos) { - // the values are in pos.x and pos.y + $("#placeholder").bind("plotclick", function (event, pos, item) { + alert("You clicked at " + pos.x + ", " + pos.y); + // secondary axis coordinates if present are in pos.x2, pos.y2 }); -Support for hover indications or for associating the clicks with any -specific data is still forthcoming. +The item object in this example is either null or a nearby object on the form: + + item: { + datapoint: the point as you specified it in the data, e.g. [0, 2] + dataIndex: the index of the point in the data array + series: the series object + seriesIndex: the index of the series + } + +For instance, if you have specified the data like this + + $.plot($("#placeholder"), [ { label: "Foo", data: [[0, 10], [7, 3]] } ], ...); + +and the mouse is near the point (7, 3), "datapoint" is the [7, 3] we +specified, "dataIndex" will be 1, "series" is a normalized series +object with among other things the "Foo" label in series.label and the +color in series.color, and "seriesIndex" is 0. + +Note that the detection of nearby points is still limited to points, +and support for highlighting the point or graph is still forthcoming. Customizing the selection @@ -515,12 +546,15 @@ You enable selection support by setting the mode to one of "x", "y" or similarly for "y" mode. For "xy", the selection becomes a rectangle where both ranges can be specified. "color" is color of the selection. -When selection support is enabled, a "selected" event will be emitted +When selection support is enabled, a "plotselected" event will be emitted on the DOM element you passed into the plot function. The event -handler gets one extra parameter with the area selected, like this: +handler gets one extra parameter with the ranges selected on the axes, +like this: - placeholder.bind("selected", function(event, area) { - // area selected is area.x1 to area.x2 and area.y1 to area.y2 + placeholder.bind("plotselected", function(event, ranges) { + alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to) + // similar for yaxis, secondary axes are in x2axis + // and y2axis if present }); @@ -534,18 +568,20 @@ members: Clear the selection rectangle. - - setSelection(area) + - setSelection(ranges) - Set the selection rectangle. The passed in area should have the - members x1 and x2 if the selection mode is "x" and y1 and y2 if - the selection mode is "y" and both x1, x2 and y1, y2 if the - selection mode is "xy", like this: + Set the selection rectangle. The passed in ranges is on the same + form as returned in the "plotselected" event. If the selection + mode is "x", you should put in either an xaxis (or x2axis) object, + if the mode is "y" you need to put in an yaxis (or y2axis) object + and both xaxis/x2axis and yaxis/y2axis if the selection mode is + "xy", like this: - setSelection({ x1: 0, x2: 10, y1: 40, y2: 60}); + setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } }); - setSelection will trigger the "selected" event when called so you + setSelection will trigger the "plotselected" event when called so you may have to do a bit of shortcircuiting to prevent an eternal loop - if you invoke the method inside the "selected" handler. + if you invoke setSelection inside a "plotselected" handler. - getCanvas() @@ -580,7 +616,7 @@ members: var series = plot.getData(); for (var i = 0; i < series.length; ++i) - alert([i].color); + alert(series[i].color); - setupGrid() diff --git a/NEWS.txt b/NEWS.txt index 4bab393..b0fff73 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,7 +1,16 @@ Flot x.x -------- -API changes: timestamps in time mode are now displayed according to +Backwards API change summary: Timestamps are now in UTC. Also +"selected" event -> becomes "plotselected" with new data, the +parameters for setSelection are now different (but backwards +compatibility hooks are in place). + +Interactivity: added a new "plothover" event and this and the +"plotclick" event now returns the closest data item (based on patch by +/david). + +Timestamps in time mode are now displayed according to UTC instead of the time zone of the visitor. This affects the way the timestamps should be input; you'll probably have to offset the timestamps according to your local time zone. It also affects any @@ -9,6 +18,14 @@ custom date handling code (which basically now should use the equivalent UTC date mehods, e.g. .setUTCMonth() instead of .setMonth(). +Support for dual axis has been added (based on patch by someone who's +annoyed and /david). For each data series you can specify which axes +it belongs to, and there are two more axes, x2axis and y2axis, to +customize. This affects the "selected" event which has been renamed to +"plotselected" and spews out { xaxis: { from: -10, to: 20 } ... } and +setSelection in which the parameters are on a new form (backwards +compatible hooks are in place so old code shouldn't break). + Added support for specifying the size of tick labels (axis.labelWidth, axis.labelHeight). Useful for specifying a max label size to keep multiple plots aligned. @@ -26,7 +43,8 @@ sets. Prevent the possibility of eternal looping in tick calculations. Fixed a bug when borderWidth is set to 0 (reported by Rob/sanchothefat). Fixed a bug with drawing bars extending below 0 (reported by James Hewitt, convenient patch by Ryan Funduk). Fixed a -bug with line widths of bars (reported by MikeM). +bug with line widths of bars (reported by MikeM). Fixed a bug with +'nw' and 'sw' legend positions. Flot 0.4 diff --git a/TODO b/TODO index 870b5f2..1add33e 100644 --- a/TODO +++ b/TODO @@ -24,16 +24,13 @@ support for highlighting stuff legend - interactive auto-highlight of graph? + - ability to specify noRows instead of just noColumns labels - labels on bars, data points - - plan "all points" option + - plain "all points" option - interactive "label this point" command -interactive hover over - - fire event with value for points - - fire event with graph id for lines - error margin indicators - for scientific/statistical purposes diff --git a/examples/dual-axis.html b/examples/dual-axis.html new file mode 100644 index 0000000..d97fa8a --- /dev/null +++ b/examples/dual-axis.html @@ -0,0 +1,38 @@ + + + + + Flot Examples + + + + + + +

Flot Examples

+ +
+ +

Dual axis support showing the raw oil price in US $/barrel of + crude oil (left axis) vs. the exchange rate from US $ to € (right + axis).

+ +

As illustrated, you can put in secondary y and x axes if you + need to. For each data series, simply specify the axis number.

+ + + + diff --git a/examples/index.html b/examples/index.html index 88c6fb5..36ae0a1 100644 --- a/examples/index.html +++ b/examples/index.html @@ -15,13 +15,11 @@ diff --git a/examples/interacting.html b/examples/interacting.html index 71acd66..029b97a 100644 --- a/examples/interacting.html +++ b/examples/interacting.html @@ -13,23 +13,47 @@
-

Flot supports user interactions. It's currently still a bit - primitive, but you can enable the user to click on the plot and - get the corresponding x and y values back.

+

One of the goals of Flot is to support user interactions intelligently. + Try hovering over the graph above and clicking on the points (note that support for highlighting the points is still missing).

-

Try clicking on the plot above.

+

+ + diff --git a/examples/selection.html b/examples/selection.html index f4dd98d..4a745d7 100644 --- a/examples/selection.html +++ b/examples/selection.html @@ -31,7 +31,7 @@

Selections are really useful for zooming. Just replot the chart with min and max values for the axes set to the values - in the "selected" event triggered. Try enabling the checkbox + in the "plotselected" event triggered. Try enabling the checkbox below and select a region again.

Zoom to selection.

@@ -80,14 +80,14 @@ $(function () { var placeholder = $("#placeholder"); - placeholder.bind("selected", function (event, area) { - $("#selection").text(area.x1.toFixed(1) + " to " + area.x2.toFixed(1)); + placeholder.bind("plotselected", function (event, ranges) { + $("#selection").text(ranges.xaxis.from.toFixed(1) + " to " + ranges.xaxis.to.toFixed(1)); var zoom = $("#zoom").attr("checked"); if (zoom) plot = $.plot(placeholder, data, $.extend(true, {}, options, { - xaxis: { min: area.x1, max: area.x2 } + xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to } })); }); diff --git a/examples/real-data.html b/examples/turning-series.html similarity index 100% rename from examples/real-data.html rename to examples/turning-series.html diff --git a/examples/visitors.html b/examples/visitors.html index f3a5b7a..7197b68 100644 --- a/examples/visitors.html +++ b/examples/visitors.html @@ -67,25 +67,25 @@ $(function () { // now connect the two var internalSelection = false; - $("#placeholder").bind("selected", function (event, area) { + $("#placeholder").bind("plotselected", function (event, ranges) { // do the zooming plot = $.plot($("#placeholder"), [d], $.extend(true, {}, options, { - xaxis: { min: area.x1, max: area.x2 } + xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to } })); if (internalSelection) return; // prevent eternal loop internalSelection = true; - overview.setSelection(area); + overview.setSelection(ranges); internalSelection = false; }); - $("#overview").bind("selected", function (event, area) { + $("#overview").bind("plotselected", function (event, ranges) { if (internalSelection) return; internalSelection = true; - plot.setSelection(area); + plot.setSelection(ranges); internalSelection = false; }); }); diff --git a/examples/zooming.html b/examples/zooming.html index 8ed5605..6040c02 100644 --- a/examples/zooming.html +++ b/examples/zooming.html @@ -65,31 +65,31 @@ $(function () { // now connect the two var internalSelection = false; - $("#placeholder").bind("selected", function (event, area) { + $("#placeholder").bind("plotselected", function (event, ranges) { // clamp the zooming to prevent eternal zoom - if (area.x2 - area.x1 < 0.00001) - area.x2 = area.x1 + 0.00001; - if (area.y2 - area.y1 < 0.00001) - area.y2 = area.y1 + 0.00001; + if (ranges.xaxis.to - ranges.xaxis.from < 0.00001) + ranges.xaxis.to = ranges.xaxis.from + 0.00001; + if (ranges.yaxis.to - ranges.yaxis.from < 0.00001) + ranges.yaxis.to = ranges.yaxis.from + 0.00001; // do the zooming - plot = $.plot($("#placeholder"), getData(area.x1, area.x2), + plot = $.plot($("#placeholder"), getData(ranges.xaxis.from, ranges.xaxis.to), $.extend(true, {}, options, { - xaxis: { min: area.x1, max: area.x2 }, - yaxis: { min: area.y1, max: area.y2 } + xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to }, + yaxis: { min: ranges.yaxis.from, max: ranges.yaxis.to } })); if (internalSelection) return; // prevent eternal loop internalSelection = true; - overview.setSelection(area); + overview.setSelection(ranges); internalSelection = false; }); - $("#overview").bind("selected", function (event, area) { + $("#overview").bind("plotselected", function (event, ranges) { if (internalSelection) return; internalSelection = true; - plot.setSelection(area); + plot.setSelection(ranges); internalSelection = false; }); }); diff --git a/jquery.flot.js b/jquery.flot.js index a793513..3894cd0 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -46,6 +46,12 @@ yaxis: { autoscaleMargin: 0.02 }, + x2axis: { + autoscaleMargin: null + }, + y2axis: { + autoscaleMargin: 0.02 + }, points: { show: false, radius: 3, @@ -72,7 +78,9 @@ tickColor: "#dddddd", // color used for the ticks labelMargin: 3, // in pixels borderWidth: 2, - clickable: null, + clickable: false, + hoverable: false, + mouseCatchingArea: 30, coloredAreas: null, // array of { x1, y1, x2, y2 } or fn: plot area -> areas coloredAreasColor: "#f4f4f4" }, @@ -86,10 +94,10 @@ ctx = null, octx = null, target = target_, xaxis = {}, yaxis = {}, + x2axis = {}, y2axis = {}, plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, canvasWidth = 0, canvasHeight = 0, plotWidth = 0, plotHeight = 0, - hozScale = 0, vertScale = 0, // dedicated to storing data for buggy standard compliance cases workarounds = {}; @@ -101,7 +109,7 @@ this.getCanvas = function() { return canvas; }; this.getPlotOffset = function() { return plotOffset; }; this.getData = function() { return series; }; - this.getAxes = function() { return { xaxis: xaxis, yaxis: yaxis }; }; + this.getAxes = function() { return { xaxis: xaxis, yaxis: yaxis, x2axis: x2axis, y2axis: y2axis }; }; // initialize parseOptions(options_); @@ -216,6 +224,14 @@ s.bars = $.extend(true, {}, options.bars, s.bars); if (s.shadowSize == null) s.shadowSize = options.shadowSize; + if (s.xaxis && s.xaxis == 2) + s.xaxis = x2axis; + else + s.xaxis = xaxis; + if (s.yaxis && s.yaxis == 2) + s.yaxis = y2axis; + else + s.yaxis = yaxis; } } @@ -223,11 +239,15 @@ var top_sentry = Number.POSITIVE_INFINITY, bottom_sentry = Number.NEGATIVE_INFINITY; - xaxis.datamin = yaxis.datamin = top_sentry; - xaxis.datamax = yaxis.datamax = bottom_sentry; + xaxis.datamin = yaxis.datamin = x2axis.datamin = y2axis.datamin = top_sentry; + xaxis.datamax = yaxis.datamax = x2axis.datamax = y2axis.datamax = bottom_sentry; + xaxis.used = yaxis.used = x2axis.used = y2axis.used = false; for (var i = 0; i < series.length; ++i) { - var data = series[i].data; + var data = series[i].data, + axisx = series[i].xaxis, + axisy = series[i].yaxis; + for (var j = 0; j < data.length; ++j) { if (data[j] == null) continue; @@ -240,25 +260,29 @@ continue; } - if (x < xaxis.datamin) - xaxis.datamin = x; - if (x > xaxis.datamax) - xaxis.datamax = x; - if (y < yaxis.datamin) - yaxis.datamin = y; - if (y > yaxis.datamax) - yaxis.datamax = y; + if (x < axisx.datamin) + axisx.datamin = x; + if (x > axisx.datamax) + axisx.datamax = x; + if (y < axisy.datamin) + axisy.datamin = y; + if (y > axisy.datamax) + axisy.datamax = y; + axisx.used = axisy.used = true; } } + + function setDefaultMinMax(axis) { + if (axis.datamin == top_sentry) + axis.datamin = 0; + if (axis.datamax == bottom_sentry) + axis.datamax = 1; + } - if (xaxis.datamin == top_sentry) - xaxis.datamin = 0; - if (yaxis.datamin == top_sentry) - yaxis.datamin = 0; - if (xaxis.datamax == bottom_sentry) - xaxis.datamax = 1; - if (yaxis.datamax == bottom_sentry) - yaxis.datamax = 1; + setDefaultMinMax(xaxis); + setDefaultMinMax(yaxis); + setDefaultMinMax(x2axis); + setDefaultMinMax(y2axis); } function constructCanvas() { @@ -286,15 +310,15 @@ // sometimes has trouble with the stacking order eventHolder = $([overlay, canvas]); - // bind events - if (options.selection.mode != null) { - eventHolder.mousedown(onMouseDown); - + if (options.selection.mode != null || options.grid.hoverable) { // FIXME: temp. work-around until jQuery bug 1871 is fixed eventHolder.each(function () { this.onmousemove = onMouseMove; }); + + if (options.selection.mode != null) + eventHolder.mousedown(onMouseDown); } if (options.grid.clickable) @@ -302,16 +326,42 @@ } function setupGrid() { - // x axis - setRange(xaxis, options.xaxis); - prepareTickGeneration(xaxis, options.xaxis); - setTicks(xaxis, options.xaxis); - extendXRangeIfNeededByBar(); + function setupAxis(axis, options) { + setRange(axis, options); + prepareTickGeneration(axis, options); + setTicks(axis, options); + // add transformation helpers + if (axis == xaxis || axis == x2axis) { + // data point to canvas coordinate + axis.p2c = function (p) { return (p - axis.min) * axis.scale; }; + // canvas coordinate to data point + axis.c2p = function (c) { return axis.min + c / axis.scale; }; + } + else { + axis.p2c = function (p) { return (axis.max - p) * axis.scale; }; + axis.c2p = function (p) { return axis.max - p / axis.scale; }; + } + } - // y axis - setRange(yaxis, options.yaxis); - prepareTickGeneration(yaxis, options.yaxis); - setTicks(yaxis, options.yaxis); + function extendXRangeIfNeededByBar(axis, options) { + // extend x range so end bar graph won't be drawn on the chart border + if (options.max == null) { + // great, we're autoscaling, check if we might need a bump + + var newmax = axis.max; + for (var i = 0; i < series.length; ++i) + if (series[i].bars.show && series[i].bars.barWidth + axis.datamax > newmax) + newmax = axis.datamax + series[i].bars.barWidth; + axis.max = newmax; + } + } + + setupAxis(xaxis, options.xaxis); + extendXRangeIfNeededByBar(xaxis,options.xaxis); + setupAxis(yaxis, options.yaxis); + setupAxis(x2axis, options.x2axis); + extendXRangeIfNeededByBar(x2axis, options.x2axis); + setupAxis(y2axis, options.y2axis); setSpacing(); insertLabels(); @@ -360,7 +410,7 @@ var noTicks; if (typeof axisOptions.ticks == "number" && axisOptions.ticks > 0) noTicks = axisOptions.ticks; - else if (axis == xaxis) + else if (axis == xaxis || axis == x2axis) noTicks = canvasWidth / 100; else noTicks = canvasHeight / 60; @@ -604,9 +654,13 @@ generator = function (axis) { var ticks = []; - var start = floorInBase(axis.min, axis.tickSize); - // then spew out all possible ticks - var i = 0, v = Number.NaN, prev; + + if (axis.min == null) // FIXME + return ticks; + + // spew out all possible ticks + var start = floorInBase(axis.min, axis.tickSize), + i = 0, v = Number.NaN, prev; do { prev = v; v = start + i * axis.tickSize; @@ -633,20 +687,11 @@ axis.labelHeight = axisOptions.labelHeight; } - function extendXRangeIfNeededByBar() { - if (options.xaxis.max == null) { - // great, we're autoscaling, check if we might need a bump - - 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.datamax + series[i].bars.barWidth; - xaxis.max = newmax; - } - } - function setTicks(axis, axisOptions) { axis.ticks = []; + + if (!axis.used) + return; if (axisOptions.ticks == null) axis.ticks = axis.tickGenerator(axis); @@ -689,31 +734,67 @@ } function setSpacing() { - var i, labels = [], l; - if (yaxis.labelWidth == null || yaxis.labelHeight == null) { - // calculate y label dimensions - for (i = 0; i < yaxis.ticks.length; ++i) { - l = yaxis.ticks[i].label; - if (l) - labels.push('
' + l + '
'); + function measureXLabels(axis) { + // to avoid measuring the widths of the labels, we + // construct fixed-size boxes and put the labels inside + // them, we don't need the exact figures and the + // fixed-size box content is easy to center + if (axis.labelWidth == null) + axis.labelWidth = canvasWidth / 6; + + // measure x label heights + if (axis.labelHeight == null) { + labels = []; + for (i = 0; i < axis.ticks.length; ++i) { + l = axis.ticks[i].label; + if (l) + labels.push('' + l + ''); + } + + axis.labelHeight = 0; + if (labels.length > 0) { + var dummyDiv = $('
' + + labels.join("") + '
').appendTo(target); + axis.labelHeight = dummyDiv.height(); + dummyDiv.remove(); + } } - - if (labels.length > 0) { - var dummyDiv = $('
' - + labels.join("") + '
').appendTo(target); - if (yaxis.labelWidth == null) - yaxis.labelWidth = dummyDiv.width(); - if (yaxis.labelHeight == null) - yaxis.labelHeight = dummyDiv.find("div").height(); - dummyDiv.remove(); + } + + function measureYLabels(axis) { + if (axis.labelWidth == null || axis.labelHeight == null) { + var i, labels = [], l; + // calculate y label dimensions + for (i = 0; i < axis.ticks.length; ++i) { + l = axis.ticks[i].label; + if (l) + labels.push('
' + l + '
'); + } + + if (labels.length > 0) { + var dummyDiv = $('
' + + labels.join("") + '
').appendTo(target); + if (axis.labelWidth == null) + axis.labelWidth = dummyDiv.width(); + if (axis.labelHeight == null) + axis.labelHeight = dummyDiv.find("div").height(); + dummyDiv.remove(); + } + + if (axis.labelWidth == null) + axis.labelWidth = 0; + if (axis.labelHeight == null) + axis.labelHeight = 0; } - - if (yaxis.labelWidth == null) - yaxis.labelWidth = 0; - if (yaxis.labelHeight == null) - yaxis.labelHeight = 0; } - + + measureXLabels(xaxis); + measureYLabels(yaxis); + measureXLabels(x2axis); + measureYLabels(y2axis); + + // get the most space needed around the grid for things + // that may stick out var maxOutset = options.grid.borderWidth / 2; if (options.points.show) maxOutset = Math.max(maxOutset, options.points.radius + options.points.lineWidth/2); @@ -724,41 +805,25 @@ plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = maxOutset; + if (xaxis.labelHeight > 0) + plotOffset.bottom += xaxis.labelHeight + options.grid.labelMargin; if (yaxis.labelWidth > 0) plotOffset.left += yaxis.labelWidth + options.grid.labelMargin; - plotWidth = canvasWidth - plotOffset.left - plotOffset.right; - - // 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 - if (xaxis.labelWidth == null) - xaxis.labelWidth = plotWidth / 6; - if (xaxis.labelHeight == null) { - // measure x label heights - labels = []; - for (i = 0; i < xaxis.ticks.length; ++i) { - l = xaxis.ticks[i].label; - if (l) - labels.push('' + l + ''); - } - - xaxis.labelHeight = 0; - if (labels.length > 0) { - var dummyDiv = $('
' - + labels.join("") + '
').appendTo(target); - xaxis.labelHeight = dummyDiv.height(); - dummyDiv.remove(); - } - } - - if (xaxis.labelHeight > 0) - plotOffset.bottom += xaxis.labelHeight + options.grid.labelMargin; + if (x2axis.labelHeight > 0) + plotOffset.top += x2axis.labelHeight + options.grid.labelMargin; + if (y2axis.labelWidth > 0) + plotOffset.right += y2axis.labelWidth + options.grid.labelMargin; + + plotWidth = canvasWidth - plotOffset.left - plotOffset.right; plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; - hozScale = plotWidth / (xaxis.max - xaxis.min); - vertScale = plotHeight / (yaxis.max - yaxis.min); + + // precompute how much the axis is scaling a point in canvas space + xaxis.scale = plotWidth / (xaxis.max - xaxis.min); + yaxis.scale = plotHeight / (yaxis.max - yaxis.min); + x2axis.scale = plotWidth / (x2axis.max - x2axis.min); + y2axis.scale = plotHeight / (y2axis.max - y2axis.min); } function draw() { @@ -768,14 +833,6 @@ } } - function tHoz(x) { - return (x - xaxis.min) * hozScale; - } - - function tVert(y) { - return plotHeight - (y - yaxis.min) * vertScale; - } - function drawGrid() { var i; @@ -825,8 +882,8 @@ 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))); + ctx.fillRect(Math.floor(xaxis.p2c(a.x1)), Math.floor(yaxis.p2c(a.y2)), + Math.floor(xaxis.p2c(a.x2) - xaxis.p2c(a.x1)), Math.floor(yaxis.p2c(a.y1) - yaxis.p2c(a.y2))); } } @@ -840,8 +897,8 @@ if (v <= xaxis.min || v >= xaxis.max) continue; // skip those lying on the axes - ctx.moveTo(Math.floor(tHoz(v)) + ctx.lineWidth/2, 0); - ctx.lineTo(Math.floor(tHoz(v)) + ctx.lineWidth/2, plotHeight); + ctx.moveTo(Math.floor(xaxis.p2c(v)) + ctx.lineWidth/2, 0); + ctx.lineTo(Math.floor(xaxis.p2c(v)) + ctx.lineWidth/2, plotHeight); } for (i = 0; i < yaxis.ticks.length; ++i) { @@ -849,9 +906,28 @@ if (v <= yaxis.min || v >= yaxis.max) continue; - ctx.moveTo(0, Math.floor(tVert(v)) + ctx.lineWidth/2); - ctx.lineTo(plotWidth, Math.floor(tVert(v)) + ctx.lineWidth/2); + ctx.moveTo(0, Math.floor(yaxis.p2c(v)) + ctx.lineWidth/2); + ctx.lineTo(plotWidth, Math.floor(yaxis.p2c(v)) + ctx.lineWidth/2); + } + + for (i = 0; i < x2axis.ticks.length; ++i) { + v = x2axis.ticks[i].v; + if (v <= x2axis.min || v >= x2axis.max) + continue; + + ctx.moveTo(Math.floor(x2axis.p2c(v)) + ctx.lineWidth/2, -5); + ctx.lineTo(Math.floor(x2axis.p2c(v)) + ctx.lineWidth/2, 5); + } + + for (i = 0; i < y2axis.ticks.length; ++i) { + v = y2axis.ticks[i].v; + if (v <= y2axis.min || v >= y2axis.max) + continue; + + ctx.moveTo(plotWidth-5, Math.floor(y2axis.p2c(v)) + ctx.lineWidth/2); + ctx.lineTo(plotWidth+5, Math.floor(y2axis.p2c(v)) + ctx.lineWidth/2); } + ctx.stroke(); if (options.grid.borderWidth) { @@ -876,7 +952,7 @@ tick = xaxis.ticks[i]; if (!tick.label || tick.v < xaxis.min || tick.v > xaxis.max) continue; - html += '
' + tick.label + "
"; + html += '
' + tick.label + "
"; } // do the y-axis @@ -884,7 +960,23 @@ tick = yaxis.ticks[i]; if (!tick.label || tick.v < yaxis.min || tick.v > yaxis.max) continue; - html += '
' + tick.label + "
"; + html += '
' + tick.label + "
"; + } + + // do the x2-axis + for (i = 0; i < x2axis.ticks.length; ++i) { + tick = x2axis.ticks[i]; + if (!tick.label || tick.v < x2axis.min || tick.v > x2axis.max) + continue; + html += '
' + tick.label + "
"; + } + + // do the y2-axis + for (i = 0; i < y2axis.ticks.length; ++i) { + tick = y2axis.ticks[i]; + if (!tick.label || tick.v < y2axis.min || tick.v > y2axis.max) + continue; + html += '
' + tick.label + "
"; } html += ''; @@ -902,7 +994,7 @@ } function drawSeriesLines(series) { - function plotLine(data, offset) { + function plotLine(data, offset, axisx, axisy) { var prev, cur = null, drawx = null, drawy = null; ctx.beginPath(); @@ -917,76 +1009,76 @@ x2 = cur[0], y2 = cur[1]; // clip with ymin - if (y1 <= y2 && y1 < yaxis.min) { - if (y2 < yaxis.min) + if (y1 <= y2 && y1 < axisy.min) { + if (y2 < axisy.min) continue; // line segment is outside // compute new intersection point - x1 = (yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = yaxis.min; + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; } - else if (y2 <= y1 && y2 < yaxis.min) { - if (y1 < yaxis.min) + else if (y2 <= y1 && y2 < axisy.min) { + if (y1 < axisy.min) continue; - x2 = (yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = yaxis.min; + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; } // clip with ymax - if (y1 >= y2 && y1 > yaxis.max) { - if (y2 > yaxis.max) + if (y1 >= y2 && y1 > axisy.max) { + if (y2 > axisy.max) continue; - x1 = (yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = yaxis.max; + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; } - else if (y2 >= y1 && y2 > yaxis.max) { - if (y1 > yaxis.max) + else if (y2 >= y1 && y2 > axisy.max) { + if (y1 > axisy.max) continue; - x2 = (yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = yaxis.max; + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; } // clip with xmin - if (x1 <= x2 && x1 < xaxis.min) { - if (x2 < xaxis.min) + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) continue; - y1 = (xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = xaxis.min; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; } - else if (x2 <= x1 && x2 < xaxis.min) { - if (x1 < xaxis.min) + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) continue; - y2 = (xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = xaxis.min; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; } // clip with xmax - if (x1 >= x2 && x1 > xaxis.max) { - if (x2 > xaxis.max) + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) continue; - y1 = (xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = xaxis.max; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; } - else if (x2 >= x1 && x2 > xaxis.max) { - if (x1 > xaxis.max) + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) continue; - y2 = (xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = xaxis.max; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; } - if (drawx != tHoz(x1) || drawy != tVert(y1) + offset) - ctx.moveTo(tHoz(x1), tVert(y1) + offset); + if (drawx != axisx.p2c(x1) || drawy != axisy.p2c(y1) + offset) + ctx.moveTo(axisx.p2c(x1), axisy.p2c(y1) + offset); - drawx = tHoz(x2); - drawy = tVert(y2) + offset; + drawx = axisx.p2c(x2); + drawy = axisy.p2c(y2) + offset; ctx.lineTo(drawx, drawy); } ctx.stroke(); } - function plotLineArea(data) { + function plotLineArea(data, axisx, axisy) { var prev, cur = null; - var bottom = Math.min(Math.max(0, yaxis.min), yaxis.max); + var bottom = Math.min(Math.max(0, axisy.min), axisy.max); var top, lastX = 0; var areaOpen = false; @@ -997,7 +1089,7 @@ if (areaOpen && prev != null && cur == null) { // close area - ctx.lineTo(tHoz(lastX), tVert(bottom)); + ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom)); ctx.fill(); areaOpen = false; continue; @@ -1012,49 +1104,49 @@ // clip x values // clip with xmin - if (x1 <= x2 && x1 < xaxis.min) { - if (x2 < xaxis.min) + if (x1 <= x2 && x1 < axisx.min) { + if (x2 < axisx.min) continue; - y1 = (xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = xaxis.min; + y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.min; } - else if (x2 <= x1 && x2 < xaxis.min) { - if (x1 < xaxis.min) + else if (x2 <= x1 && x2 < axisx.min) { + if (x1 < axisx.min) continue; - y2 = (xaxis.min - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = xaxis.min; + y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.min; } // clip with xmax - if (x1 >= x2 && x1 > xaxis.max) { - if (x2 > xaxis.max) + if (x1 >= x2 && x1 > axisx.max) { + if (x2 > axisx.max) continue; - y1 = (xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x1 = xaxis.max; + y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x1 = axisx.max; } - else if (x2 >= x1 && x2 > xaxis.max) { - if (x1 > xaxis.max) + else if (x2 >= x1 && x2 > axisx.max) { + if (x1 > axisx.max) continue; - y2 = (xaxis.max - x1) / (x2 - x1) * (y2 - y1) + y1; - x2 = xaxis.max; + y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1; + x2 = axisx.max; } if (!areaOpen) { // open area ctx.beginPath(); - ctx.moveTo(tHoz(x1), tVert(bottom)); + ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom)); areaOpen = true; } // now first check the case where both is outside - if (y1 >= yaxis.max && y2 >= yaxis.max) { - ctx.lineTo(tHoz(x1), tVert(yaxis.max)); - ctx.lineTo(tHoz(x2), tVert(yaxis.max)); + if (y1 >= axisy.max && y2 >= axisy.max) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max)); continue; } - else if (y1 <= yaxis.min && y2 <= yaxis.min) { - ctx.lineTo(tHoz(x1), tVert(yaxis.min)); - ctx.lineTo(tHoz(x2), tVert(yaxis.min)); + else if (y1 <= axisy.min && y2 <= axisy.min) { + ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min)); continue; } @@ -1066,58 +1158,58 @@ // and clip the y values, without shortcutting // clip with ymin - if (y1 <= y2 && y1 < yaxis.min && y2 >= yaxis.min) { - x1 = (yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = yaxis.min; + if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) { + x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.min; } - else if (y2 <= y1 && y2 < yaxis.min && y1 >= yaxis.min) { - x2 = (yaxis.min - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = yaxis.min; + else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) { + x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.min; } // clip with ymax - if (y1 >= y2 && y1 > yaxis.max && y2 <= yaxis.max) { - x1 = (yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y1 = yaxis.max; + if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) { + x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y1 = axisy.max; } - else if (y2 >= y1 && y2 > yaxis.max && y1 <= yaxis.max) { - x2 = (yaxis.max - y1) / (y2 - y1) * (x2 - x1) + x1; - y2 = yaxis.max; + else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) { + x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1; + y2 = axisy.max; } // if the x value was changed we got a rectangle // to fill if (x1 != x1old) { - if (y1 <= yaxis.min) - top = yaxis.min; + if (y1 <= axisy.min) + top = axisy.min; else - top = yaxis.max; + top = axisy.max; - ctx.lineTo(tHoz(x1old), tVert(top)); - ctx.lineTo(tHoz(x1), tVert(top)); + ctx.lineTo(axisx.p2c(x1old), axisy.p2c(top)); + ctx.lineTo(axisx.p2c(x1), axisy.p2c(top)); } // fill the triangles - ctx.lineTo(tHoz(x1), tVert(y1)); - ctx.lineTo(tHoz(x2), tVert(y2)); + ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2)); // fill the other rectangle if it's there if (x2 != x2old) { - if (y2 <= yaxis.min) - top = yaxis.min; + if (y2 <= axisy.min) + top = axisy.min; else - top = yaxis.max; + top = axisy.max; - ctx.lineTo(tHoz(x2old), tVert(top)); - ctx.lineTo(tHoz(x2), tVert(top)); + ctx.lineTo(axisx.p2c(x2old), axisy.p2c(top)); + ctx.lineTo(axisx.p2c(x2), axisy.p2c(top)); } lastX = Math.max(x2, x2old); } if (areaOpen) { - ctx.lineTo(tHoz(lastX), tVert(bottom)); + ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom)); ctx.fill(); } } @@ -1133,50 +1225,50 @@ // draw shadow in two steps ctx.lineWidth = sw / 2; ctx.strokeStyle = "rgba(0,0,0,0.1)"; - plotLine(series.data, lw/2 + sw/2 + ctx.lineWidth/2); + plotLine(series.data, lw/2 + sw/2 + ctx.lineWidth/2, series.xaxis, series.yaxis); ctx.lineWidth = sw / 2; ctx.strokeStyle = "rgba(0,0,0,0.2)"; - plotLine(series.data, lw/2 + ctx.lineWidth/2); + plotLine(series.data, lw/2 + ctx.lineWidth/2, series.xaxis, series.yaxis); } ctx.lineWidth = lw; ctx.strokeStyle = series.color; setFillStyle(series.lines, series.color); if (series.lines.fill) - plotLineArea(series.data, 0); - plotLine(series.data, 0); + plotLineArea(series.data, series.xaxis, series.yaxis); + plotLine(series.data, 0, series.xaxis, series.yaxis); ctx.restore(); } function drawSeriesPoints(series) { - function plotPoints(data, radius, fill) { + function plotPoints(data, radius, fill, axisx, axisy) { for (var i = 0; i < data.length; ++i) { if (data[i] == null) continue; var x = data[i][0], y = data[i][1]; - if (x < xaxis.min || x > xaxis.max || y < yaxis.min || y > yaxis.max) + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) continue; ctx.beginPath(); - ctx.arc(tHoz(x), tVert(y), radius, 0, 2 * Math.PI, true); + ctx.arc(axisx.p2c(x), axisy.p2c(y), radius, 0, 2 * Math.PI, true); if (fill) ctx.fill(); ctx.stroke(); } } - function plotPointShadows(data, offset, radius) { + function plotPointShadows(data, offset, radius, axisx, axisy) { for (var i = 0; i < data.length; ++i) { if (data[i] == null) continue; var x = data[i][0], y = data[i][1]; - if (x < xaxis.min || x > xaxis.max || y < yaxis.min || y > yaxis.max) + if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) continue; ctx.beginPath(); - ctx.arc(tHoz(x), tVert(y) + offset, radius, 0, Math.PI, false); + ctx.arc(axisx.p2c(x), axisy.p2c(y) + offset, radius, 0, Math.PI, false); ctx.stroke(); } } @@ -1190,22 +1282,25 @@ // draw shadow in two steps ctx.lineWidth = sw / 2; ctx.strokeStyle = "rgba(0,0,0,0.1)"; - plotPointShadows(series.data, sw/2 + ctx.lineWidth/2, series.points.radius); + plotPointShadows(series.data, sw/2 + ctx.lineWidth/2, + series.points.radius, series.xaxis, series.yaxis); ctx.lineWidth = sw / 2; ctx.strokeStyle = "rgba(0,0,0,0.2)"; - plotPointShadows(series.data, ctx.lineWidth/2, series.points.radius); + plotPointShadows(series.data, ctx.lineWidth/2, + series.points.radius, series.xaxis, series.yaxis); } ctx.lineWidth = series.points.lineWidth; ctx.strokeStyle = series.color; setFillStyle(series.points, series.color); - plotPoints(series.data, series.points.radius, series.points.fill); + plotPoints(series.data, series.points.radius, series.points.fill, + series.xaxis, series.yaxis); ctx.restore(); } function drawSeriesBars(series) { - function plotBars(data, barWidth, offset, fill) { + function plotBars(data, barWidth, offset, fill, axisx, axisy) { for (var i = 0; i < data.length; i++) { if (data[i] == null) continue; @@ -1216,55 +1311,55 @@ // determine the co-ordinates of the bar, account for negative bars having // flipped top/bottom and draw/don't draw accordingly var left = x, right = x + barWidth, bottom = (y < 0 ? y : 0), top = (y < 0 ? 0 : y); - if (right < xaxis.min || left > xaxis.max || top < yaxis.min || bottom > yaxis.max) + if (right < axisx.min || left > axisx.max || top < axisy.min || bottom > axisy.max) continue; // clip - if (left < xaxis.min) { - left = xaxis.min; + if (left < axisx.min) { + left = axisx.min; drawLeft = false; } - if (right > xaxis.max) { - right = xaxis.max; + if (right > axisx.max) { + right = axisx.max; drawRight = false; } - if (bottom < yaxis.min) - bottom = yaxis.min; + if (bottom < axisy.min) + bottom = axisy.min; - if (top > yaxis.max) { - top = yaxis.max; + if (top > axisy.max) { + top = axisy.max; drawTop = false; } // fill the bar if (fill) { ctx.beginPath(); - ctx.moveTo(tHoz(left), tVert(bottom) + offset); - ctx.lineTo(tHoz(left), tVert(top) + offset); - ctx.lineTo(tHoz(right), tVert(top) + offset); - ctx.lineTo(tHoz(right), tVert(bottom) + offset); + ctx.moveTo(axisx.p2c(left), axisy.p2c(bottom) + offset); + ctx.lineTo(axisx.p2c(left), axisy.p2c(top) + offset); + ctx.lineTo(axisx.p2c(right), axisy.p2c(top) + offset); + ctx.lineTo(axisx.p2c(right), axisy.p2c(bottom) + offset); ctx.fill(); } // draw outline if (drawLeft || drawRight || drawTop) { ctx.beginPath(); - ctx.moveTo(tHoz(left), tVert(bottom) + offset); + ctx.moveTo(axisx.p2c(left), axisy.p2c(bottom) + offset); if (drawLeft) - ctx.lineTo(tHoz(left), tVert(top) + offset); + ctx.lineTo(axisx.p2c(left), axisy.p2c(top) + offset); else - ctx.moveTo(tHoz(left), tVert(top) + offset); + ctx.moveTo(axisx.p2c(left), axisy.p2c(top) + offset); if (drawTop) - ctx.lineTo(tHoz(right), tVert(top) + offset); + ctx.lineTo(axisx.p2c(right), axisy.p2c(top) + offset); else - ctx.moveTo(tHoz(right), tVert(top) + offset); + ctx.moveTo(axisx.p2c(right), axisy.p2c(top) + offset); if (drawRight) - ctx.lineTo(tHoz(right), tVert(bottom) + offset); + ctx.lineTo(axisx.p2c(right), axisy.p2c(bottom) + offset); else - ctx.moveTo(tHoz(right), tVert(bottom) + offset); + ctx.moveTo(axisx.p2c(right), axisy.p2c(bottom) + offset); ctx.stroke(); } } @@ -1293,7 +1388,7 @@ ctx.lineWidth = series.bars.lineWidth; ctx.strokeStyle = series.color; setFillStyle(series.bars, series.color); - plotBars(series.data, series.bars.barWidth, 0, series.bars.fill); + plotBars(series.data, series.bars.barWidth, 0, series.bars.fill, series.xaxis, series.yaxis); ctx.restore(); } @@ -1342,39 +1437,40 @@ if (rowStarted) fragments.push(''); - if (fragments.length > 0) { - var table = '' + fragments.join("") + '
'; - if (options.legend.container != null) - options.legend.container.html(table); - else { - var pos = ""; - var p = options.legend.position, m = options.legend.margin; - if (p.charAt(0) == "n") - pos += 'top:' + (m + plotOffset.top) + 'px;'; - else if (p.charAt(0) == "s") - pos += 'bottom:' + (m + plotOffset.bottom) + 'px;'; - if (p.charAt(1) == "e") - pos += 'right:' + (m + plotOffset.right) + 'px;'; - else if (p.charAt(1) == "w") - pos += 'left:' + (m + plotOffset.bottom) + 'px;'; - var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(target); - if (options.legend.backgroundOpacity != 0.0) { - // put in the transparent background - // separately to avoid blended labels and - // label boxes - var c = options.legend.backgroundColor; - if (c == null) { - var tmp; - if (options.grid.backgroundColor) - tmp = options.grid.backgroundColor; - else - tmp = extractColor(legend); - c = parseColor(tmp).adjust(null, null, null, 1).toString(); - } - var div = legend.children(); - $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); - + if (fragments.length == 0) + return; + + var table = '' + fragments.join("") + '
'; + if (options.legend.container != null) + options.legend.container.html(table); + else { + var pos = ""; + var p = options.legend.position, m = options.legend.margin; + if (p.charAt(0) == "n") + pos += 'top:' + (m + plotOffset.top) + 'px;'; + else if (p.charAt(0) == "s") + pos += 'bottom:' + (m + plotOffset.bottom) + 'px;'; + if (p.charAt(1) == "e") + pos += 'right:' + (m + plotOffset.right) + 'px;'; + else if (p.charAt(1) == "w") + pos += 'left:' + (m + plotOffset.left) + 'px;'; + var legend = $('
' + table.replace('style="', 'style="position:absolute;' + pos +';') + '
').appendTo(target); + if (options.legend.backgroundOpacity != 0.0) { + // put in the transparent background + // separately to avoid blended labels and + // label boxes + var c = options.legend.backgroundColor; + if (c == null) { + var tmp; + if (options.grid.backgroundColor) + tmp = options.grid.backgroundColor; + else + tmp = extractColor(legend); + c = parseColor(tmp).adjust(null, null, null, 1).toString(); } + var div = legend.children(); + $('
').prependTo(legend).css('opacity', options.legend.backgroundOpacity); + } } } @@ -1385,6 +1481,50 @@ var selectionInterval = null; var ignoreClick = false; + // Returns the data item the mouse is over, or null if none is found + function findNearbyItem(mouseX, mouseY) { + var maxDistance = options.grid.mouseCatchingArea; + var lowestDistance = maxDistance * maxDistance + 0.1, + item = null; + + for (var i = 0; i < series.length; ++i) { + var data = series[i].data, + axisx = series[i].xaxis, + axisy = series[i].yaxis; + + var mx = axisx.c2p(mouseX), my = axisy.c2p(mouseY), + maxx = maxDistance / axisx.scale, + maxy = maxDistance / axisy.scale; + for (var j = 0; j < data.length; ++j) { + if (data[j] == null) + continue; + + // We have to calculate distances in pixels, not in + // data units, because the scale of the axes may be different + var x = data[j][0], y = data[j][1]; + if (x - mx > maxx || x - mx < -maxx) + continue; // Don't bother, we're too far + if (y - my > maxy || y - my < -maxy) + continue; + + var dx = Math.abs(xaxis.p2c(x) - mouseX), + dy = Math.abs(yaxis.p2c(y) - mouseY); + var dist = dx * dx + dy * dy; + if (dist < lowestDistance) { + lowestDistance = dist; + item = { datapoint: data[j], + dataIndex: j, + series: series[i], + seriesIndex: i }; + } + } + } + + return item; + } + + var hoverTimeout = null; + function onMouseMove(ev) { // FIXME: temp. work-around until jQuery bug 1871 is fixed var e = ev || window.event; @@ -1397,6 +1537,9 @@ lastMousePos.pageX = e.pageX; lastMousePos.pageY = e.pageY; } + + if (options.grid.hoverable && !hoverTimeout) + hoverTimeout = setTimeout(emitHoverEvent, 100); } function onMouseDown(e) { @@ -1430,19 +1573,38 @@ ignoreClick = false; return; } + + triggerClickHoverEvent("plotclick", e); + } + + function emitHoverEvent() { + triggerClickHoverEvent("plothover", lastMousePos); + hoverTimeout = null; + } + + function triggerClickHoverEvent(eventname, event) { + var offset = eventHolder.offset(), + pos = {}, + canvasX = event.pageX - offset.left - plotOffset.left, + canvasY = event.pageY - offset.top - plotOffset.top; - var offset = eventHolder.offset(); - var pos = {}; - pos.x = e.pageX - offset.left - plotOffset.left; - pos.x = xaxis.min + pos.x / hozScale; - pos.y = e.pageY - offset.top - plotOffset.top; - pos.y = yaxis.max - pos.y / vertScale; - - target.trigger("plotclick", [ pos ]); + if (xaxis.used) + pos.x = xaxis.c2p(canvasX); + if (yaxis.used) + pos.y = yaxis.c2p(canvasY); + if (x2axis.used) + pos.x2 = x2axis.c2p(canvasX); + if (y2axis.used) + pos.y2 = y2axis.c2p(canvasY); + + item = findNearbyItem(canvasX, canvasY); + + target.trigger(eventname, [ pos, item ]); } function triggerSelectedEvent() { var x1, x2, y1, y2; + if (selection.first.x <= selection.second.x) { x1 = selection.first.x; x2 = selection.second.x; @@ -1460,14 +1622,22 @@ y1 = selection.second.y; y2 = selection.first.y; } - - x1 = xaxis.min + x1 / hozScale; - x2 = xaxis.min + x2 / hozScale; - y1 = yaxis.max - y1 / vertScale; - y2 = yaxis.max - y2 / vertScale; + var r = {}; + if (xaxis.used) + r.xaxis = { from: xaxis.c2p(x1), to: xaxis.c2p(x2) }; + if (x2axis.used) + r.x2axis = { from: x2axis.c2p(x1), to: x2axis.c2p(x2) }; + if (yaxis.used) + r.yaxis = { from: yaxis.c2p(y1), to: yaxis.c2p(y2) }; + if (y2axis.used) + r.yaxis = { from: y2axis.c2p(y1), to: y2axis.c2p(y2) }; + + target.trigger("plotselected", [ r ]); - target.trigger("selected", [ { x1: x1, y1: y1, x2: x2, y2: y2 } ]); + // backwards-compat stuff, to be removed in future + if (xaxis.used && yaxis.used) + target.trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]); } function onSelectionMouseUp(e) { @@ -1545,24 +1715,61 @@ prevSelection = null; } - function setSelection(area) { + function setSelection(ranges) { clearSelection(); - if (options.selection.mode == "x") { - selection.first.y = 0; - selection.second.y = plotHeight; - } - else { - selection.first.y = (yaxis.max - area.y1) * vertScale; - selection.second.y = (yaxis.max - area.y2) * vertScale; - } + var axis, from, to; + if (options.selection.mode == "y") { selection.first.x = 0; selection.second.x = plotWidth; } else { - selection.first.x = (area.x1 - xaxis.min) * hozScale; - selection.second.x = (area.x2 - xaxis.min) * hozScale; + if (ranges.yaxis) { + axis = xaxis; + from = ranges.xaxis.from; + to = ranges.xaxis.to; + } + else if (ranges.x2axis) { + axis = x2axis; + from = ranges.x2axis.from; + to = ranges.x2axis.to; + } + else { + // backwards-compat stuff - to be removed in future + axis = xaxis; + from = ranges.x1; + to = ranges.x2; + } + + selection.first.x = axis.p2c(from); + selection.second.x = axis.p2c(to); + } + + if (options.selection.mode == "x") { + selection.first.y = 0; + selection.second.y = plotHeight; + } + else { + if (ranges.yaxis) { + axis = yaxis; + from = ranges.yaxis.from; + to = ranges.yaxis.to; + } + else if (ranges.y2axis) { + axis = y2axis; + from = ranges.y2axis.from; + to = ranges.y2axis.to; + } + else { + // backwards-compat stuff - to be removed in future + axis = yaxis; + from = ranges.y1; + to = ranges.y2; + } + + selection.first.y = axis.p2c(from); + selection.second.y = axis.p2c(to); } drawSelection();