Rest-HtmlVis
view release on metacpan or search on metacpan
share/flot/jquery.flot.js view on Meta::CPAN
// Cache of text fragments and metrics, so we can avoid expensively
// re-calculating them when the plot is re-rendered in a loop.
this._textCache = {};
}
// Resizes the canvas to the given dimensions.
//
// @param {number} width New width of the canvas, in pixels.
// @param {number} width New height of the canvas, in pixels.
Canvas.prototype.resize = function(width, height) {
if (width <= 0 || height <= 0) {
throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height);
}
var element = this.element,
context = this.context,
pixelRatio = this.pixelRatio;
// Resize the canvas, increasing its density based on the display's
// pixel ratio; basically giving it more pixels without increasing the
// size of its element, to take advantage of the fact that retina
// displays have that many more pixels in the same advertised space.
// Resizing should reset the state (excanvas seems to be buggy though)
if (this.width != width) {
element.width = width * pixelRatio;
element.style.width = width + "px";
this.width = width;
}
if (this.height != height) {
element.height = height * pixelRatio;
element.style.height = height + "px";
this.height = height;
}
// Save the context, so we can reset in case we get replotted. The
// restore ensure that we're really back at the initial state, and
// should be safe even if we haven't saved the initial state yet.
context.restore();
context.save();
// Scale the coordinate space to match the display density; so even though we
// may have twice as many pixels, we still want lines and other drawing to
// appear at the same size; the extra pixels will just make them crisper.
context.scale(pixelRatio, pixelRatio);
};
// Clears the entire canvas area, not including any overlaid HTML text
Canvas.prototype.clear = function() {
this.context.clearRect(0, 0, this.width, this.height);
};
// Finishes rendering the canvas, including managing the text overlay.
Canvas.prototype.render = function() {
var cache = this._textCache;
// For each text layer, add elements marked as active that haven't
// already been rendered, and remove those that are no longer active.
for (var layerKey in cache) {
if (hasOwnProperty.call(cache, layerKey)) {
var layer = this.getTextLayer(layerKey),
layerCache = cache[layerKey];
layer.hide();
for (var styleKey in layerCache) {
if (hasOwnProperty.call(layerCache, styleKey)) {
var styleCache = layerCache[styleKey];
for (var key in styleCache) {
if (hasOwnProperty.call(styleCache, key)) {
var positions = styleCache[key].positions;
for (var i = 0, position; position = positions[i]; i++) {
if (position.active) {
if (!position.rendered) {
layer.append(position.element);
position.rendered = true;
}
} else {
positions.splice(i--, 1);
if (position.rendered) {
position.element.detach();
}
}
}
if (positions.length == 0) {
delete styleCache[key];
}
}
}
}
}
layer.show();
}
}
};
// Creates (if necessary) and returns the text overlay container.
//
// @param {string} classes String of space-separated CSS classes used to
// uniquely identify the text layer.
// @return {object} The jQuery-wrapped text-layer div.
Canvas.prototype.getTextLayer = function(classes) {
var layer = this.text[classes];
// Create the text layer if it doesn't exist
if (layer == null) {
// Create the text layer container, if it doesn't exist
if (this.textContainer == null) {
this.textContainer = $("<div class='flot-text'></div>")
.css({
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
'font-size': "smaller",
color: "#545454"
})
.insertAfter(this.element);
}
layer = this.text[classes] = $("<div></div>")
.addClass(classes)
.css({
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0
})
.appendTo(this.textContainer);
}
return layer;
};
// Creates (if necessary) and returns a text info object.
//
// The object looks like this:
//
// {
// width: Width of the text's wrapper div.
// height: Height of the text's wrapper div.
// element: The jQuery-wrapped HTML div containing the text.
// positions: Array of positions at which this text is drawn.
// }
//
// The positions array contains objects that look like this:
//
// {
// active: Flag indicating whether the text should be visible.
// rendered: Flag indicating whether the text is currently visible.
share/flot/jquery.flot.js view on Meta::CPAN
text = "" + text;
// If the font is a font-spec object, generate a CSS font definition
if (typeof font === "object") {
textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family;
} else {
textStyle = font;
}
// Retrieve (or create) the cache for the text's layer and styles
layerCache = this._textCache[layer];
if (layerCache == null) {
layerCache = this._textCache[layer] = {};
}
styleCache = layerCache[textStyle];
if (styleCache == null) {
styleCache = layerCache[textStyle] = {};
}
info = styleCache[text];
// If we can't find a matching element in our cache, create a new one
if (info == null) {
var element = $("<div></div>").html(text)
.css({
position: "absolute",
'max-width': width,
top: -9999
})
.appendTo(this.getTextLayer(layer));
if (typeof font === "object") {
element.css({
font: textStyle,
color: font.color
});
} else if (typeof font === "string") {
element.addClass(font);
}
info = styleCache[text] = {
width: element.outerWidth(true),
height: element.outerHeight(true),
element: element,
positions: []
};
element.detach();
}
return info;
};
// Adds a text string to the canvas text overlay.
//
// The text isn't drawn immediately; it is marked as rendering, which will
// result in its addition to the canvas on the next render pass.
//
// @param {string} layer A string of space-separated CSS classes uniquely
// identifying the layer containing this text.
// @param {number} x X coordinate at which to draw the text.
// @param {number} y Y coordinate at which to draw the text.
// @param {string} text Text string to draw.
// @param {(string|object)=} font Either a string of space-separated CSS
// classes or a font-spec object, defining the text's font and style.
// @param {number=} angle Angle at which to rotate the text, in degrees.
// Angle is currently unused, it will be implemented in the future.
// @param {number=} width Maximum width of the text before it wraps.
// @param {string=} halign Horizontal alignment of the text; either "left",
// "center" or "right".
// @param {string=} valign Vertical alignment of the text; either "top",
// "middle" or "bottom".
Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) {
var info = this.getTextInfo(layer, text, font, angle, width),
positions = info.positions;
// Tweak the div's position to match the text's alignment
if (halign == "center") {
x -= info.width / 2;
} else if (halign == "right") {
x -= info.width;
}
if (valign == "middle") {
y -= info.height / 2;
} else if (valign == "bottom") {
y -= info.height;
}
// Determine whether this text already exists at this position.
// If so, mark it for inclusion in the next render pass.
for (var i = 0, position; position = positions[i]; i++) {
if (position.x == x && position.y == y) {
position.active = true;
return;
}
}
// If the text doesn't exist at this position, create a new entry
// For the very first position we'll re-use the original element,
// while for subsequent ones we'll clone it.
position = {
active: true,
rendered: false,
element: positions.length ? info.element.clone() : info.element,
x: x,
y: y
};
positions.push(position);
// Move the element to its final position within the container
position.element.css({
top: Math.round(y),
left: Math.round(x),
'text-align': halign // In case the text wraps
});
};
// Removes one or more text strings from the canvas text overlay.
//
// If no parameters are given, all text within the layer is removed.
//
// Note that the text is not immediately removed; it is simply marked as
// inactive, which will result in its removal on the next render pass.
// This avoids the performance penalty for 'clear and redraw' behavior,
// where we potentially get rid of all text on a layer, but will likely
// add back most or all of it later, as when redrawing axes, for example.
//
// @param {string} layer A string of space-separated CSS classes uniquely
// identifying the layer containing this text.
// @param {number=} x X coordinate of the text.
// @param {number=} y Y coordinate of the text.
// @param {string=} text Text string to remove.
// @param {(string|object)=} font Either a string of space-separated CSS
// classes or a font-spec object, defining the text's font and style.
// @param {number=} angle Angle at which the text is rotated, in degrees.
// Angle is currently unused, it will be implemented in the future.
Canvas.prototype.removeText = function(layer, x, y, text, font, angle) {
if (text == null) {
var layerCache = this._textCache[layer];
if (layerCache != null) {
for (var styleKey in layerCache) {
if (hasOwnProperty.call(layerCache, styleKey)) {
var styleCache = layerCache[styleKey];
for (var key in styleCache) {
if (hasOwnProperty.call(styleCache, key)) {
var positions = styleCache[key].positions;
for (var i = 0, position; position = positions[i]; i++) {
position.active = false;
}
}
}
}
}
}
} else {
var positions = this.getTextInfo(layer, text, font, angle).positions;
for (var i = 0, position; position = positions[i]; i++) {
if (position.x == x && position.y == y) {
position.active = false;
}
}
}
};
///////////////////////////////////////////////////////////////////////////
// The top-level container for the entire plot.
function Plot(placeholder, data_, options_, plugins) {
// data is on the form:
// [ series1, series2 ... ]
// where series is either just the data as [ [x1, y1], [x2, y2], ... ]
// or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
var series = [],
options = {
// the color theme used for graphs
colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
share/flot/jquery.flot.js view on Meta::CPAN
yaxes: [],
series: {
points: {
show: false,
radius: 3,
lineWidth: 2, // in pixels
fill: true,
fillColor: "#ffffff",
symbol: "circle" // or callback
},
lines: {
// we don't put in show: false so we can see
// whether lines were actively disabled
lineWidth: 2, // in pixels
fill: false,
fillColor: null,
steps: false
// Omit 'zero', so we can later default its value to
// match that of the 'fill' option.
},
bars: {
show: false,
lineWidth: 2, // in pixels
barWidth: 1, // in units of the x axis
fill: true,
fillColor: null,
align: "left", // "left", "right", or "center"
horizontal: false,
zero: true
},
shadowSize: 3,
highlightColor: null
},
grid: {
show: true,
aboveData: false,
color: "#545454", // primary color used for outline and labels
backgroundColor: null, // null for transparent, else color
borderColor: null, // set if different from the grid color
tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
margin: 0, // distance from the canvas edge to the grid
labelMargin: 5, // in pixels
axisMargin: 8, // in pixels
borderWidth: 2, // in pixels
minBorderMargin: null, // in pixels, null means taken from points radius
markings: null, // array of ranges or fn: axes -> array of ranges
markingsColor: "#f4f4f4",
markingsLineWidth: 2,
// interactive stuff
clickable: false,
hoverable: false,
autoHighlight: true, // highlight in case mouse is near
mouseActiveRadius: 10 // how far the mouse can be away to activate an item
},
interaction: {
redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow
},
hooks: {}
},
surface = null, // the canvas for the plot itself
overlay = null, // canvas for interactive stuff on top of plot
eventHolder = null, // jQuery object that events should be bound to
ctx = null, octx = null,
xaxes = [], yaxes = [],
plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
plotWidth = 0, plotHeight = 0,
hooks = {
processOptions: [],
processRawData: [],
processDatapoints: [],
processOffset: [],
drawBackground: [],
drawSeries: [],
draw: [],
bindEvents: [],
drawOverlay: [],
shutdown: []
},
plot = this;
// public functions
plot.setData = setData;
plot.setupGrid = setupGrid;
plot.draw = draw;
plot.getPlaceholder = function() { return placeholder; };
plot.getCanvas = function() { return surface.element; };
plot.getPlotOffset = function() { return plotOffset; };
plot.width = function () { return plotWidth; };
plot.height = function () { return plotHeight; };
plot.offset = function () {
var o = eventHolder.offset();
o.left += plotOffset.left;
o.top += plotOffset.top;
return o;
};
plot.getData = function () { return series; };
plot.getAxes = function () {
var res = {}, i;
$.each(xaxes.concat(yaxes), function (_, axis) {
if (axis)
res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
});
return res;
};
plot.getXAxes = function () { return xaxes; };
plot.getYAxes = function () { return yaxes; };
plot.c2p = canvasToAxisCoords;
plot.p2c = axisToCanvasCoords;
plot.getOptions = function () { return options; };
plot.highlight = highlight;
plot.unhighlight = unhighlight;
plot.triggerRedrawOverlay = triggerRedrawOverlay;
plot.pointOffset = function(point) {
return {
left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left, 10),
top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top, 10)
};
};
plot.shutdown = shutdown;
plot.destroy = function () {
shutdown();
placeholder.removeData("plot").empty();
series = [];
options = null;
surface = null;
overlay = null;
eventHolder = null;
ctx = null;
octx = null;
xaxes = [];
yaxes = [];
hooks = null;
highlights = [];
plot = null;
};
plot.resize = function () {
var width = placeholder.width(),
height = placeholder.height();
surface.resize(width, height);
overlay.resize(width, height);
};
// public attributes
plot.hooks = hooks;
// initialize
initPlugins(plot);
parseOptions(options_);
setupCanvases();
setData(data_);
setupGrid();
draw();
bindEvents();
function executeHooks(hook, args) {
args = [plot].concat(args);
for (var i = 0; i < hook.length; ++i)
hook[i].apply(this, args);
}
function initPlugins() {
// References to key classes, allowing plugins to modify them
var classes = {
Canvas: Canvas
};
for (var i = 0; i < plugins.length; ++i) {
var p = plugins[i];
p.init(plot, classes);
if (p.options)
$.extend(true, options, p.options);
}
}
function parseOptions(opts) {
$.extend(true, options, opts);
// $.extend merges arrays, rather than replacing them. When less
// colors are provided than the size of the default palette, we
// end up with those colors plus the remaining defaults, which is
// not expected behavior; avoid it by replacing them here.
if (opts && opts.colors) {
options.colors = opts.colors;
}
if (options.xaxis.color == null)
options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString();
if (options.yaxis.color == null)
options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString();
if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility
options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color;
if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility
options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color;
share/flot/jquery.flot.js view on Meta::CPAN
if (f.x) {
if (val < xmin)
xmin = val;
if (val > xmax)
xmax = val;
}
if (f.y) {
if (val < ymin)
ymin = val;
if (val > ymax)
ymax = val;
}
}
}
if (s.bars.show) {
// make sure we got room for the bar on the dancing floor
var delta;
switch (s.bars.align) {
case "left":
delta = 0;
break;
case "right":
delta = -s.bars.barWidth;
break;
default:
delta = -s.bars.barWidth / 2;
}
if (s.bars.horizontal) {
ymin += delta;
ymax += delta + s.bars.barWidth;
}
else {
xmin += delta;
xmax += delta + s.bars.barWidth;
}
}
updateAxis(s.xaxis, xmin, xmax);
updateAxis(s.yaxis, ymin, ymax);
}
$.each(allAxes(), function (_, axis) {
if (axis.datamin == topSentry)
axis.datamin = null;
if (axis.datamax == bottomSentry)
axis.datamax = null;
});
}
function setupCanvases() {
// Make sure the placeholder is clear of everything except canvases
// from a previous plot in this container that we'll try to re-use.
placeholder.css("padding", 0) // padding messes up the positioning
.children().filter(function(){
return !$(this).hasClass("flot-overlay") && !$(this).hasClass('flot-base');
}).remove();
if (placeholder.css("position") == 'static')
placeholder.css("position", "relative"); // for positioning labels and overlay
surface = new Canvas("flot-base", placeholder);
overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features
ctx = surface.context;
octx = overlay.context;
// define which element we're listening for events on
eventHolder = $(overlay.element).unbind();
// If we're re-using a plot object, shut down the old one
var existing = placeholder.data("plot");
if (existing) {
existing.shutdown();
overlay.clear();
}
// save in case we get replotted
placeholder.data("plot", plot);
}
function bindEvents() {
// bind events
if (options.grid.hoverable) {
eventHolder.mousemove(onMouseMove);
// Use bind, rather than .mouseleave, because we officially
// still support jQuery 1.2.6, which doesn't define a shortcut
// for mouseenter or mouseleave. This was a bug/oversight that
// was fixed somewhere around 1.3.x. We can return to using
// .mouseleave when we drop support for 1.2.6.
eventHolder.bind("mouseleave", onMouseLeave);
}
if (options.grid.clickable)
eventHolder.click(onClick);
executeHooks(hooks.bindEvents, [eventHolder]);
}
function shutdown() {
if (redrawTimeout)
clearTimeout(redrawTimeout);
eventHolder.unbind("mousemove", onMouseMove);
eventHolder.unbind("mouseleave", onMouseLeave);
eventHolder.unbind("click", onClick);
executeHooks(hooks.shutdown, [eventHolder]);
}
function setTransformationHelpers(axis) {
// set helper functions on the axis, assumes plot area
// has been computed already
function identity(x) { return x; }
var s, m, t = axis.options.transform || identity,
it = axis.options.inverseTransform;
// precompute how much the axis is scaling a point
// in canvas space
if (axis.direction == "x") {
s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
m = Math.min(t(axis.max), t(axis.min));
}
else {
s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
s = -s;
m = Math.max(t(axis.max), t(axis.min));
}
// data point to canvas coordinate
if (t == identity) // slight optimization
share/flot/jquery.flot.js view on Meta::CPAN
// clean up/labelify the supplied ticks, copy them over
var i, v;
axis.ticks = [];
for (i = 0; i < ticks.length; ++i) {
var label = null;
var t = ticks[i];
if (typeof t == "object") {
v = +t[0];
if (t.length > 1)
label = t[1];
}
else
v = +t;
if (label == null)
label = axis.tickFormatter(v, axis);
if (!isNaN(v))
axis.ticks.push({ v: v, label: label });
}
}
function snapRangeToTicks(axis, ticks) {
if (axis.options.autoscaleMargin && ticks.length > 0) {
// snap to ticks
if (axis.options.min == null)
axis.min = Math.min(axis.min, ticks[0].v);
if (axis.options.max == null && ticks.length > 1)
axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
}
}
function draw() {
surface.clear();
executeHooks(hooks.drawBackground, [ctx]);
var grid = options.grid;
// draw background, if any
if (grid.show && grid.backgroundColor)
drawBackground();
if (grid.show && !grid.aboveData) {
drawGrid();
}
for (var i = 0; i < series.length; ++i) {
executeHooks(hooks.drawSeries, [ctx, series[i]]);
drawSeries(series[i]);
}
executeHooks(hooks.draw, [ctx]);
if (grid.show && grid.aboveData) {
drawGrid();
}
surface.render();
// A draw implies that either the axes or data have changed, so we
// should probably update the overlay highlights as well.
triggerRedrawOverlay();
}
function extractRange(ranges, coord) {
var axis, from, to, key, axes = allAxes();
for (var i = 0; i < axes.length; ++i) {
axis = axes[i];
if (axis.direction == coord) {
key = coord + axis.n + "axis";
if (!ranges[key] && axis.n == 1)
key = coord + "axis"; // support x1axis as xaxis
if (ranges[key]) {
from = ranges[key].from;
to = ranges[key].to;
break;
}
}
}
// backwards-compat stuff - to be removed in future
if (!ranges[key]) {
axis = coord == "x" ? xaxes[0] : yaxes[0];
from = ranges[coord + "1"];
to = ranges[coord + "2"];
}
// auto-reverse as an added bonus
if (from != null && to != null && from > to) {
var tmp = from;
from = to;
to = tmp;
}
return { from: from, to: to, axis: axis };
}
function drawBackground() {
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
ctx.fillRect(0, 0, plotWidth, plotHeight);
ctx.restore();
}
function drawGrid() {
var i, axes, bw, bc;
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
// draw markings
var markings = options.grid.markings;
if (markings) {
if ($.isFunction(markings)) {
axes = plot.getAxes();
// xmin etc. is backwards compatibility, to be
// removed in the future
share/flot/jquery.flot.js view on Meta::CPAN
}
function onClick(e) {
triggerClickHoverEvent("plotclick", e,
function (s) { return s["clickable"] != false; });
}
// trigger click or hover event (they send the same parameters
// so we share their code)
function triggerClickHoverEvent(eventname, event, seriesFilter) {
var offset = eventHolder.offset(),
canvasX = event.pageX - offset.left - plotOffset.left,
canvasY = event.pageY - offset.top - plotOffset.top,
pos = canvasToAxisCoords({ left: canvasX, top: canvasY });
pos.pageX = event.pageX;
pos.pageY = event.pageY;
var item = findNearbyItem(canvasX, canvasY, seriesFilter);
if (item) {
// fill in mouse pos for any listeners out there
item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left, 10);
item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top, 10);
}
if (options.grid.autoHighlight) {
// clear auto-highlights
for (var i = 0; i < highlights.length; ++i) {
var h = highlights[i];
if (h.auto == eventname &&
!(item && h.series == item.series &&
h.point[0] == item.datapoint[0] &&
h.point[1] == item.datapoint[1]))
unhighlight(h.series, h.point);
}
if (item)
highlight(item.series, item.datapoint, eventname);
}
placeholder.trigger(eventname, [ pos, item ]);
}
function triggerRedrawOverlay() {
var t = options.interaction.redrawOverlayInterval;
if (t == -1) { // skip event queue
drawOverlay();
return;
}
if (!redrawTimeout)
redrawTimeout = setTimeout(drawOverlay, t);
}
function drawOverlay() {
redrawTimeout = null;
// draw highlights
octx.save();
overlay.clear();
octx.translate(plotOffset.left, plotOffset.top);
var i, hi;
for (i = 0; i < highlights.length; ++i) {
hi = highlights[i];
if (hi.series.bars.show)
drawBarHighlight(hi.series, hi.point);
else
drawPointHighlight(hi.series, hi.point);
}
octx.restore();
executeHooks(hooks.drawOverlay, [octx]);
}
function highlight(s, point, auto) {
if (typeof s == "number")
s = series[s];
if (typeof point == "number") {
var ps = s.datapoints.pointsize;
point = s.datapoints.points.slice(ps * point, ps * (point + 1));
}
var i = indexOfHighlight(s, point);
if (i == -1) {
highlights.push({ series: s, point: point, auto: auto });
triggerRedrawOverlay();
}
else if (!auto)
highlights[i].auto = false;
}
function unhighlight(s, point) {
if (s == null && point == null) {
highlights = [];
triggerRedrawOverlay();
return;
}
if (typeof s == "number")
s = series[s];
if (typeof point == "number") {
var ps = s.datapoints.pointsize;
point = s.datapoints.points.slice(ps * point, ps * (point + 1));
}
var i = indexOfHighlight(s, point);
if (i != -1) {
highlights.splice(i, 1);
triggerRedrawOverlay();
}
}
function indexOfHighlight(s, p) {
for (var i = 0; i < highlights.length; ++i) {
( run in 0.656 second using v1.01-cache-2.11-cpan-39bf76dae61 )