diff --git a/API.txt b/API.txt index 8a8dbc2..671b557 100644 --- a/API.txt +++ b/API.txt @@ -178,7 +178,8 @@ Customizing the axes color: null or color spec tickColor: null or color spec - + font: null or font spec object + min: null or number max: null or number autoscaleMargin: null or number @@ -222,6 +223,20 @@ you can also set the color of the ticks separately with "tickColor" (otherwise it's autogenerated as the base color with some transparency). +You can customize the font used to draw the labels with CSS or +directly with "font". The default value of null means that the font is +read from the font style on the placeholder element (80% the size of +that to be precise). If you set it directly with "font: { ... }", the +format is like this: + + { + size: 11, + style: "italic", + weight: "bold", + family: "sans-serif", + variant: "small-caps" + } + The options "min"/"max" are the precise minimum/maximum value on the scale. If you don't specify either of them, a value will automatically be chosen based on the minimum/maximum data values. Note that Flot @@ -677,8 +692,7 @@ above the data or below (below is default). "labelMargin" is the space in pixels between tick labels and axis line, and "axisMargin" is the space in pixels between axes when there -are two next to each other. Note that you can style the tick labels -with CSS, e.g. to change the color. They have class "tickLabel". +are two next to each other. "borderWidth" is the width of the border around the plot. Set it to 0 to disable the border. You can also set "borderColor" if you want the diff --git a/FAQ.txt b/FAQ.txt index e02b761..2bde48a 100644 --- a/FAQ.txt +++ b/FAQ.txt @@ -6,7 +6,8 @@ Q: How much data can Flot cope with? A: Flot will happily draw everything you send to it so the answer depends on the browser. The excanvas emulation used for IE (built with VML) makes IE by far the slowest browser so be sure to test with that -if IE users are in your target group. +if IE users are in your target group (for large plots in IE, you can +also check out Flashcanvas which may be faster). 1000 points is not a problem, but as soon as you start having more points than the pixel width, you should probably start thinking about @@ -25,10 +26,11 @@ conversion automatically. Q: Can I export the graph? -A: This is a limitation of the canvas technology. There's a hook in -the canvas object for getting an image out, but you won't get the tick -labels. And it's not likely to be supported by IE. At this point, your -best bet is probably taking a screenshot, e.g. with PrtScn. +A: You can grab the image rendered by the canvas element used by Flot +as a PNG or JPEG (remember to set a background). Note that it won't +include anything not drawn in the canvas (such as the legend). And it +doesn't work with excanvas which uses VML, but you could try +Flashcanvas. Q: The bars are all tiny in time mode? @@ -56,11 +58,9 @@ libraries") for details. Q: Flot doesn't work with [insert name of Javascript UI framework]! -A: The only non-standard thing used by Flot is the canvas tag; -otherwise it is simply a series of absolute positioned divs within the -placeholder tag you put in. If this is not working, it's probably -because the framework you're using is doing something weird with the -DOM, or you're using it the wrong way. +A: Flot is using standard HTML to make charts. If this is not working, +it's probably because the framework you're using is doing something +weird with the DOM or with the CSS that is interfering with Flot. A common problem is that there's display:none on a container until the user does something. Many tab widgets work this way, and there's diff --git a/NEWS.txt b/NEWS.txt index 529a26f..3f57858 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,13 +1,31 @@ Flot x.x -------- +API changes: + +Axis labels are now drawn with canvas text with some parsing to +support newlines. This solves various issues but also means that they +no longer support HTML markup, can be accessed as DOM elements or +styled directly with CSS. Some older browsers lack this function of +the canvas API (this doesn't affect IE); if this is a problem, either +continue using an older version of Flot or try an emulation helper +such as canvas-text or Flashcanvas. + + + +Changes: + +- Canvas text support for labels (sponsored by YCharts.com). Bug fixes - Fix problem with null values and pie plugin (patch by gcruxifix, issue 500). -- Fix problem with threshold plugin and bars (based on patch by kaarlenkaski) - +- Fix problem with threshold plugin and bars (based on patch by + kaarlenkaski, issue 348). +- Fix axis box calculations so the boxes include the outermost part of + the labels too. + Flot 0.7 -------- diff --git a/examples/interacting-axes.html b/examples/interacting-axes.html index 5b6e3bb..9995c26 100644 --- a/examples/interacting-axes.html +++ b/examples/interacting-axes.html @@ -32,10 +32,10 @@ $(function () { } var data = [ - { data: generate(0, 10, function (x) { return Math.sqrt(x)}), xaxis: 1, yaxis:1 }, - { data: generate(0, 10, function (x) { return Math.sin(x)}), xaxis: 1, yaxis:2 }, - { data: generate(0, 10, function (x) { return Math.cos(x)}), xaxis: 1, yaxis:3 }, - { data: generate(2, 10, function (x) { return Math.tan(x)}), xaxis: 2, yaxis: 4 } + { data: generate(0, 10, function (x) { return Math.sqrt(x);}), xaxis: 1, yaxis:1 }, + { data: generate(0, 10, function (x) { return Math.sin(x);}), xaxis: 1, yaxis:2 }, + { data: generate(0, 10, function (x) { return Math.cos(x);}), xaxis: 1, yaxis:3 }, + { data: generate(2, 10, function (x) { return Math.tan(x);}), xaxis: 2, yaxis: 4 } ]; var plot = $.plot($("#placeholder"), @@ -54,29 +54,11 @@ $(function () { }); // now for each axis, create a div - - function getBoundingBoxForAxis(plot, axis) { - var left = axis.box.left, top = axis.box.top, - right = left + axis.box.width, bottom = top + axis.box.height; - - // some ticks may stick out, enlarge the box to encompass all ticks - var cls = axis.direction + axis.n + 'Axis'; - plot.getPlaceholder().find('.' + cls + ' .tickLabel').each(function () { - var pos = $(this).position(); - left = Math.min(pos.left, left); - top = Math.min(pos.top, top); - right = Math.max(Math.round(pos.left) + $(this).outerWidth(), right); - bottom = Math.max(Math.round(pos.top) + $(this).outerHeight(), bottom); - }); - - return { left: left, top: top, width: right - left, height: bottom - top }; - } - $.each(plot.getAxes(), function (i, axis) { if (!axis.show) return; - var box = getBoundingBoxForAxis(plot, axis); + var box = axis.box; $('
') .data('axis.direction', axis.direction) diff --git a/jquery.flot.js b/jquery.flot.js index aabc544..8ba08a8 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -57,6 +57,7 @@ show: null, // null = auto-detect, true = always, false = never position: "bottom", // or "top" mode: null, // null or "time" + font: null, // null (derived from CSS in placeholder) or object like { size: 11, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } color: null, // base color, labels, ticks tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" transform: null, // null or f: number -> number to transform axis @@ -844,80 +845,72 @@ } function measureTickLabels(axis) { - var opts = axis.options, i, ticks = axis.ticks || [], labels = [], - l, w = opts.labelWidth, h = opts.labelHeight, dummyDiv; + var opts = axis.options, ticks = axis.ticks || [], + axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0, + f = axis.font; - function makeDummyDiv(labels, width) { - return $('
' + - '
' - + labels.join("") + '
') - .appendTo(placeholder); - } + if (opts.labelWidth == null || opts.labelHeight == null) { + ctx.save(); + ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px '" + f.family + "'"; - if (axis.direction == "x") { - // to avoid measuring the widths of the labels (it's slow), 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 (w == null) - w = Math.floor(canvasWidth / (ticks.length > 0 ? ticks.length : 1)); - - // measure x label heights - if (h == null) { - labels = []; - for (i = 0; i < ticks.length; ++i) { - l = ticks[i].label; - if (l) - labels.push('
' + l + '
'); - } + for (var i = 0; i < ticks.length; ++i) { + var t = ticks[i]; + + t.lines = []; + t.width = t.height = 0; + + if (!t.label) + continue; + + // accept various kinds of newlines, including HTML ones + // (you can actually split directly on regexps in Javascript, + // but IE is unfortunately broken) + var lines = t.label.replace(/
|\r\n|\r/g, "\n").split("\n"); + for (var j = 0; j < lines.length; ++j) { + var line = { text: lines[j] }, + m = ctx.measureText(line.text); + + line.width = m.width; + // m.height might not be defined, not in the + // standard yet + line.height = m.height != null ? m.height : f.size; + + // add a bit of margin since font rendering is + // not pixel perfect and cut off letters look + // bad, this also doubles as spacing between + // lines + line.height += Math.round(f.size * 0.15); + + t.width = Math.max(line.width, t.width); + t.height += line.height; - if (labels.length > 0) { - // stick them all in the same div and measure - // collective height - labels.push('
'); - dummyDiv = makeDummyDiv(labels, "width:10000px;"); - h = dummyDiv.height(); - dummyDiv.remove(); + t.lines.push(line); } + + if (opts.labelWidth == null) + axisw = Math.max(axisw, t.width); + if (opts.labelHeight == null) + axish = Math.max(axish, t.height); } - } - else if (w == null || h == null) { - // calculate y label dimensions - for (i = 0; i < ticks.length; ++i) { - l = ticks[i].label; - if (l) - labels.push('
' + l + '
'); - } - - if (labels.length > 0) { - dummyDiv = makeDummyDiv(labels, ""); - if (w == null) - w = dummyDiv.children().width(); - if (h == null) - h = dummyDiv.find("div.tickLabel").height(); - dummyDiv.remove(); - } + ctx.restore(); } - if (w == null) - w = 0; - if (h == null) - h = 0; - - axis.labelWidth = w; - axis.labelHeight = h; + axis.labelWidth = axisw; + axis.labelHeight = axish; } function allocateAxisBoxFirstPhase(axis) { // find the bounding box of the axis by looking at label // widths/heights and ticks, make room by diminishing the - // plotOffset + // plotOffset; this first phase only looks at one + // dimension per axis, the other dimension depends on the + // other axes so will have to wait var lw = axis.labelWidth, lh = axis.labelHeight, pos = axis.options.position, tickLength = axis.options.tickLength, - axismargin = options.grid.axisMargin, + axisMargin = options.grid.axisMargin, padding = options.grid.labelMargin, all = axis.direction == "x" ? xaxes : yaxes, index; @@ -927,20 +920,21 @@ return a && a.options.position == pos && a.reserveSpace; }); if ($.inArray(axis, samePosition) == samePosition.length - 1) - axismargin = 0; // outermost + axisMargin = 0; // outermost // determine tick length - if we're innermost, we can use "full" - if (tickLength == null) - tickLength = "full"; - - var sameDirection = $.grep(all, function (a) { - return a && a.reserveSpace; - }); - - var innermost = $.inArray(axis, sameDirection) == 0; - if (!innermost && tickLength == "full") - tickLength = 5; + if (tickLength == null) { + var sameDirection = $.grep(all, function (a) { + return a && a.reserveSpace; + }); + var innermost = $.inArray(axis, sameDirection) == 0; + if (innermost) + tickLength = "full" + else + tickLength = 5; + } + if (!isNaN(+tickLength)) padding += +tickLength; @@ -949,23 +943,23 @@ lh += padding; if (pos == "bottom") { - plotOffset.bottom += lh + axismargin; + plotOffset.bottom += lh + axisMargin; axis.box = { top: canvasHeight - plotOffset.bottom, height: lh }; } else { - axis.box = { top: plotOffset.top + axismargin, height: lh }; - plotOffset.top += lh + axismargin; + axis.box = { top: plotOffset.top + axisMargin, height: lh }; + plotOffset.top += lh + axisMargin; } } else { lw += padding; if (pos == "left") { - axis.box = { left: plotOffset.left + axismargin, width: lw }; - plotOffset.left += lw + axismargin; + axis.box = { left: plotOffset.left + axisMargin, width: lw }; + plotOffset.left += lw + axisMargin; } else { - plotOffset.right += lw + axismargin; + plotOffset.right += lw + axisMargin; axis.box = { left: canvasWidth - plotOffset.right, width: lw }; } } @@ -978,22 +972,59 @@ } function allocateAxisBoxSecondPhase(axis) { - // set remaining bounding box coordinates + // now that all axis boxes have been placed in one + // dimension, we can set the remaining dimension coordinates if (axis.direction == "x") { - axis.box.left = plotOffset.left; - axis.box.width = plotWidth; + axis.box.left = plotOffset.left - axis.labelWidth / 2; + axis.box.width = canvasWidth - plotOffset.left - plotOffset.right + axis.labelWidth; } else { - axis.box.top = plotOffset.top; - axis.box.height = plotHeight; + axis.box.top = plotOffset.top - axis.labelHeight / 2; + axis.box.height = canvasHeight - plotOffset.bottom - plotOffset.top + axis.labelHeight; } } + + function adjustLayoutForThingsStickingOut() { + // possibly adjust plot offset to ensure everything stays + // inside the canvas and isn't clipped off + + var minMargin = options.grid.minBorderMargin, + margins = { x: 0, y: 0 }, i, axis; + + // check stuff from the plot (FIXME: this should just read + // a value from the series, otherwise it's impossible to + // customize) + if (minMargin == null) { + minMargin = 0; + for (i = 0; i < series.length; ++i) + minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2)); + } + + margins.x = margins.y = minMargin; + + // check axis labels, note we don't check the actual + // labels but instead use the overall width/height to not + // jump as much around with replots + $.each(allAxes(), function (_, axis) { + var dir = axis.direction; + if (axis.reserveSpace) + margins[dir] = Math.max(margins[dir], (dir == "x" ? axis.labelWidth : axis.labelHeight) / 2); + }); + + plotOffset.left = Math.max(margins.x, plotOffset.left); + plotOffset.right = Math.max(margins.x, plotOffset.right); + plotOffset.top = Math.max(margins.y, plotOffset.top); + plotOffset.bottom = Math.max(margins.y, plotOffset.bottom); + } function setupGrid() { - var i, axes = allAxes(); - - // first calculate the plot and axis box dimensions + var i, axes = allAxes(), showGrid = options.grid.show; + // init plot offset + for (var a in plotOffset) + plotOffset[a] = showGrid ? options.grid.borderWidth : 0; + + // init axes $.each(axes, function (_, axis) { axis.show = axis.options.show; if (axis.show == null) @@ -1003,11 +1034,19 @@ setRange(axis); }); + + if (showGrid) { + // determine from the placeholder the font size ~ height of font ~ 1 em + var fontDefaults = { + style: placeholder.css("font-style"), + size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)), + variant: placeholder.css("font-variant"), + weight: placeholder.css("font-weight"), + family: placeholder.css("font-family") + }; - allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; }); + var allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; }); - plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0; - if (options.grid.show) { $.each(allocatedAxes, function (_, axis) { // make the ticks setupTickGeneration(axis); @@ -1015,44 +1054,32 @@ snapRangeToTicks(axis, axis.ticks); // find labelWidth/Height for axis + axis.font = $.extend({}, fontDefaults, axis.options.font); measureTickLabels(axis); }); - // with all dimensions in house, we can compute the - // axis boxes, start from the outside (reverse order) + // with all dimensions calculated, we can compute the + // axis bounding boxes, start from the outside + // (reverse order) for (i = allocatedAxes.length - 1; i >= 0; --i) allocateAxisBoxFirstPhase(allocatedAxes[i]); // make sure we've got enough space for things that // might stick out - var minMargin = options.grid.minBorderMargin; - if (minMargin == null) { - minMargin = 0; - for (i = 0; i < series.length; ++i) - minMargin = Math.max(minMargin, series[i].points.radius + series[i].points.lineWidth/2); - } - - for (var a in plotOffset) { - plotOffset[a] += options.grid.borderWidth; - plotOffset[a] = Math.max(minMargin, plotOffset[a]); - } + adjustLayoutForThingsStickingOut(); + + $.each(allocatedAxes, function (_, axis) { + allocateAxisBoxSecondPhase(axis); + }); } plotWidth = canvasWidth - plotOffset.left - plotOffset.right; plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; - // now we got the proper plotWidth/Height, we can compute the scaling + // now we got the proper plot dimensions, we can compute the scaling $.each(axes, function (_, axis) { setTransformationHelpers(axis); }); - - if (options.grid.show) { - $.each(allocatedAxes, function (_, axis) { - allocateAxisBoxSecondPhase(axis); - }); - - insertAxisLabels(); - } insertLegend(); } @@ -1419,8 +1446,10 @@ if (grid.show && grid.backgroundColor) drawBackground(); - if (grid.show && !grid.aboveData) + if (grid.show && !grid.aboveData) { drawGrid(); + drawAxisLabels(); + } for (var i = 0; i < series.length; ++i) { executeHooks(hooks.drawSeries, [ctx, series[i]]); @@ -1429,8 +1458,10 @@ executeHooks(hooks.draw, [ctx]); - if (grid.show && grid.aboveData) + if (grid.show && grid.aboveData) { drawGrid(); + drawAxisLabels(); + } } function extractRange(ranges, coord) { @@ -1650,59 +1681,68 @@ ctx.restore(); } - function insertAxisLabels() { - placeholder.find(".tickLabels").remove(); - - var html = ['
']; + function drawAxisLabels() { + ctx.save(); - var axes = allAxes(); - for (var j = 0; j < axes.length; ++j) { - var axis = axes[j], box = axis.box; - if (!axis.show) - continue; - //debug: html.push('
') - html.push('
'); + $.each(allAxes(), function (_, axis) { + var box = axis.box, f = axis.font; + // placeholder.append('
') // debug + + ctx.fillStyle = axis.options.color; + ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px '" + f.family + "'"; + ctx.textAlign = "start"; + // middle align the labels - top would be more + // natural, but browsers can differ a pixel or two in + // where they consider the top to be, so instead we + // middle align to minimize variation between browsers + // and compensate when calculating the coordinates + ctx.textBaseline = "middle"; + for (var i = 0; i < axis.ticks.length; ++i) { var tick = axis.ticks[i]; if (!tick.label || tick.v < axis.min || tick.v > axis.max) continue; - var pos = {}, align; - - if (axis.direction == "x") { - align = "center"; - pos.left = Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2); - if (axis.position == "bottom") - pos.top = box.top + box.padding; - else - pos.bottom = canvasHeight - (box.top + box.height - box.padding); - } - else { - pos.top = Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2); - if (axis.position == "left") { - pos.right = canvasWidth - (box.left + box.width - box.padding) - align = "right"; + var x, y, offset = 0, line; + for (var k = 0; k < tick.lines.length; ++k) { + line = tick.lines[k]; + + if (axis.direction == "x") { + x = plotOffset.left + axis.p2c(tick.v) - line.width/2; + if (axis.position == "bottom") + y = box.top + box.padding; + else + y = box.top + box.height - box.padding - tick.height; } else { - pos.left = box.left + box.padding; - align = "left"; + y = plotOffset.top + axis.p2c(tick.v) - tick.height/2; + if (axis.position == "left") + x = box.left + box.width - box.padding - line.width; + else + x = box.left + box.padding; } - } - - pos.width = axis.labelWidth; - var style = ["position:absolute", "text-align:" + align ]; - for (var a in pos) - style.push(a + ":" + pos[a] + "px") - - html.push('
' + tick.label + '
'); + // account for middle aligning and line number + y += line.height/2 + offset; + offset += line.height; + + if ($.browser.opera) { + // FIXME: UGLY BROWSER DETECTION + // round the coordinates since Opera + // otherwise switches to more ugly + // rendering (probably non-hinted) and + // offset the y coordinates since it seems + // to be off pretty consistently compared + // to the other browsers + x = Math.floor(x); + y = Math.ceil(y - 2); + } + ctx.fillText(line.text, x, y); + } } - html.push('
'); - } - - html.push('
'); + }); - placeholder.append(html.join("")); + ctx.restore(); } function drawSeries(series) {