Add result chart at my record page and backend answer list page.

Add flot js.
This commit is contained in:
BoHung Chiu 2021-10-26 09:55:36 +08:00
parent 2e45e1824c
commit 54dc77bfd1
17 changed files with 6034 additions and 2 deletions

View File

@ -0,0 +1,558 @@
/** ## jquery.flot.canvaswrapper
This plugin contains the function for creating and manipulating both the canvas
layers and svg layers.
The Canvas object is a wrapper around an HTML5 canvas tag.
The constructor Canvas(cls, container) takes as parameters cls,
the list of classes to apply to the canvas adnd the containter,
element onto which to append the canvas. The canvas operations
don't work unless the canvas is attached to the DOM.
### jquery.canvaswrapper.js API functions
*/
(function($) {
var Canvas = function(cls, container) {
var element = container.getElementsByClassName(cls)[0];
if (!element) {
element = document.createElement('canvas');
element.className = cls;
element.style.direction = 'ltr';
element.style.position = 'absolute';
element.style.left = '0px';
element.style.top = '0px';
container.appendChild(element);
// If HTML5 Canvas isn't available, throw
if (!element.getContext) {
throw new Error('Canvas is not available.');
}
}
this.element = element;
var context = this.context = element.getContext('2d');
this.pixelRatio = $.plot.browser.getPixelRatio(context);
// Size the canvas to match the internal dimensions of its container
var width = $(container).width();
var height = $(container).height();
this.resize(width, height);
// Collection of HTML div layers for text overlaid onto the canvas
this.SVGContainer = null;
this.SVG = {};
// 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 = {};
}
/**
- resize(width, height)
Resizes the canvas to the given dimensions.
The width represents the new width of the canvas, meanwhile the height
is the new height of the canvas, both of them in pixels.
*/
Canvas.prototype.resize = function(width, height) {
var minSize = 10;
width = width < minSize ? minSize : width;
height = height < minSize ? minSize : 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);
};
/**
- clear()
Clears the entire canvas area, not including any overlaid HTML text
*/
Canvas.prototype.clear = function() {
this.context.clearRect(0, 0, this.width, this.height);
};
/**
- render()
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.getSVGLayer(layerKey),
layerCache = cache[layerKey];
var display = layer.style.display;
layer.style.display = 'none';
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 val = styleCache[key],
positions = val.positions;
for (var i = 0, position; positions[i]; i++) {
position = positions[i];
if (position.active) {
if (!position.rendered) {
layer.appendChild(position.element);
position.rendered = true;
}
} else {
positions.splice(i--, 1);
if (position.rendered) {
while (position.element.firstChild) {
position.element.removeChild(position.element.firstChild);
}
position.element.parentNode.removeChild(position.element);
}
}
}
if (positions.length === 0) {
if (val.measured) {
val.measured = false;
} else {
delete styleCache[key];
}
}
}
}
}
}
layer.style.display = display;
}
}
};
/**
- getSVGLayer(classes)
Creates (if necessary) and returns the SVG overlay container.
The classes string represents the string of space-separated CSS classes
used to uniquely identify the text layer. It return the svg-layer div.
*/
Canvas.prototype.getSVGLayer = function(classes) {
var layer = this.SVG[classes];
// Create the SVG layer if it doesn't exist
if (!layer) {
// Create the svg layer container, if it doesn't exist
var svgElement;
if (!this.SVGContainer) {
this.SVGContainer = document.createElement('div');
this.SVGContainer.className = 'flot-svg';
this.SVGContainer.style.position = 'absolute';
this.SVGContainer.style.top = '0px';
this.SVGContainer.style.left = '0px';
this.SVGContainer.style.height = '100%';
this.SVGContainer.style.width = '100%';
this.SVGContainer.style.pointerEvents = 'none';
this.element.parentNode.appendChild(this.SVGContainer);
svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgElement.style.width = '100%';
svgElement.style.height = '100%';
this.SVGContainer.appendChild(svgElement);
} else {
svgElement = this.SVGContainer.firstChild;
}
layer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
layer.setAttribute('class', classes);
layer.style.position = 'absolute';
layer.style.top = '0px';
layer.style.left = '0px';
layer.style.bottom = '0px';
layer.style.right = '0px';
svgElement.appendChild(layer);
this.SVG[classes] = layer;
}
return layer;
};
/**
- getTextInfo(layer, text, font, angle, width)
Creates (if necessary) and returns a text info object.
The object looks like this:
```js
{
width //Width of the text's wrapper div.
height //Height of the text's wrapper div.
element //The HTML div containing the text.
positions //Array of positions at which this text is drawn.
}
```
The positions array contains objects that look like this:
```js
{
active //Flag indicating whether the text should be visible.
rendered //Flag indicating whether the text is currently visible.
element //The HTML div containing the text.
text //The actual text and is identical with element[0].textContent.
x //X coordinate at which to draw the text.
y //Y coordinate at which to draw the text.
}
```
Each position after the first receives a clone of the original element.
The idea is that that the width, height, and general 'identity' of the
text is constant no matter where it is placed; the placements are a
secondary property.
Canvas maintains a cache of recently-used text info objects; getTextInfo
either returns the cached element or creates a new entry.
The layer parameter is string of space-separated CSS classes uniquely
identifying the layer containing this text.
Text is the text string to retrieve info for.
Font is either a string of space-separated CSS classes or a font-spec object,
defining the text's font and style.
Angle is the angle at which to rotate the text, in degrees. Angle is currently unused,
it will be implemented in the future.
The last parameter is the Maximum width of the text before it wraps.
The method returns a text info object.
*/
Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) {
var textStyle, layerCache, styleCache, info;
// Cast the value to a string, in case we were given a number or such
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] = {};
}
var key = generateKey(text);
info = styleCache[key];
// If we can't find a matching element in our cache, create a new one
if (!info) {
var element = document.createElementNS('http://www.w3.org/2000/svg', 'text');
if (text.indexOf('<br>') !== -1) {
addTspanElements(text, element, -9999);
} else {
var textNode = document.createTextNode(text);
element.appendChild(textNode);
}
element.style.position = 'absolute';
element.style.maxWidth = width;
element.setAttributeNS(null, 'x', -9999);
element.setAttributeNS(null, 'y', -9999);
if (typeof font === 'object') {
element.style.font = textStyle;
element.style.fill = font.fill;
} else if (typeof font === 'string') {
element.setAttribute('class', font);
}
this.getSVGLayer(layer).appendChild(element);
var elementRect = element.getBBox();
info = styleCache[key] = {
width: elementRect.width,
height: elementRect.height,
measured: true,
element: element,
positions: []
};
//remove elements from dom
while (element.firstChild) {
element.removeChild(element.firstChild);
}
element.parentNode.removeChild(element);
}
info.measured = true;
return info;
};
function updateTransforms (element, transforms) {
element.transform.baseVal.clear();
if (transforms) {
transforms.forEach(function(t) {
element.transform.baseVal.appendItem(t);
});
}
}
/**
- addText (layer, x, y, text, font, angle, width, halign, valign, transforms)
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.
The layer is string of space-separated CSS classes uniquely
identifying the layer containing this text.
X and Y represents the X and Y coordinate at which to draw the text.
and text is the string to draw
*/
Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign, transforms) {
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;
}
y += 0.75 * 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; positions[i]; i++) {
position = positions[i];
if (position.x === x && position.y === y && position.text === text) {
position.active = true;
// update the transforms
updateTransforms(position.element, transforms);
return;
} else if (position.active === false) {
position.active = true;
position.text = text;
if(Number.isNaN(y)){
y = position.y;
}
if (text.indexOf('<br>') !== -1) {
y -= 0.25 * info.height;
addTspanElements(text, position.element, x);
} else {
position.element.textContent = text;
}
position.element.setAttributeNS(null, 'x', x);
position.element.setAttributeNS(null, 'y', y);
position.x = x;
position.y = y;
// update the transforms
updateTransforms(position.element, transforms);
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.cloneNode() : info.element,
text: text,
x: x,
y: y
};
positions.push(position);
if(Number.isNaN(y)){
y = position.y;
}
if(Number.isNaN(y)){
y = 0;
}
if (text.indexOf('<br>') !== -1) {
y -= 0.25 * info.height;
addTspanElements(text, position.element, x);
} else {
position.element.textContent = text;
}
// Move the element to its final position within the container
position.element.setAttributeNS(null, 'x', x);
position.element.setAttributeNS(null, 'y', y);
position.element.style.textAlign = halign;
// update the transforms
updateTransforms(position.element, transforms);
};
var addTspanElements = function(text, element, x) {
var lines = text.split('<br>'),
tspan, i, offset;
for (i = 0; i < lines.length; i++) {
if (!element.childNodes[i]) {
tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
element.appendChild(tspan);
} else {
tspan = element.childNodes[i];
}
tspan.textContent = lines[i];
offset = i * 1 + 'em';
tspan.setAttributeNS(null, 'dy', offset);
tspan.setAttributeNS(null, 'x', x);
}
}
/**
- removeText (layer, x, y, text, font, angle)
The function 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.
The layer is a string of space-separated CSS classes uniquely
identifying the layer containing this text. The following parameter are
X and Y coordinate of the text.
Text is the string to remove, while the font is either a string of space-separated CSS
classes or a font-spec object, defining the text's font and style.
*/
Canvas.prototype.removeText = function(layer, x, y, text, font, angle) {
var info, htmlYCoord;
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;
positions.forEach(function(position) {
position.active = false;
});
}
}
}
}
}
} else {
info = this.getTextInfo(layer, text, font, angle);
positions = info.positions;
positions.forEach(function(position) {
htmlYCoord = y + 0.75 * info.height;
if (position.x === x && position.y === htmlYCoord && position.text === text) {
position.active = false;
}
});
}
};
/**
- clearCache()
Clears the cache used to speed up the text size measurements.
As an (unfortunate) side effect all text within the text Layer is removed.
Use this function before plot.setupGrid() and plot.draw() if the plot just
became visible or the styles changed.
*/
Canvas.prototype.clearCache = function() {
var cache = this._textCache;
for (var layerKey in cache) {
if (hasOwnProperty.call(cache, layerKey)) {
var layer = this.getSVGLayer(layerKey);
while (layer.firstChild) {
layer.removeChild(layer.firstChild);
}
}
};
this._textCache = {};
};
function generateKey(text) {
return text.replace(/0|1|2|3|4|5|6|7|8|9/g, '0');
}
if (!window.Flot) {
window.Flot = {};
}
window.Flot.Canvas = Canvas;
})(jQuery);

View File

@ -0,0 +1,199 @@
/* Plugin for jQuery for working with colors.
*
* Version 1.1.
*
* Inspiration from jQuery color animation plugin by John Resig.
*
* Released under the MIT license by Ole Laursen, October 2009.
*
* Examples:
*
* $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
* var c = $.color.extract($("#mydiv"), 'background-color');
* console.log(c.r, c.g, c.b, c.a);
* $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
*
* Note that .scale() and .add() return the same modified object
* instead of making a new one.
*
* V. 1.1: Fix error handling so e.g. parsing an empty string does
* produce a color rather than just crashing.
*/
(function($) {
$.color = {};
// construct color object with some convenient chainable helpers
$.color.make = function (r, g, b, a) {
var o = {};
o.r = r || 0;
o.g = g || 0;
o.b = b || 0;
o.a = a != null ? a : 1;
o.add = function (c, d) {
for (var i = 0; i < c.length; ++i) {
o[c.charAt(i)] += d;
}
return o.normalize();
};
o.scale = function (c, f) {
for (var i = 0; i < c.length; ++i) {
o[c.charAt(i)] *= f;
}
return o.normalize();
};
o.toString = function () {
if (o.a >= 1.0) {
return "rgb(" + [o.r, o.g, o.b].join(",") + ")";
} else {
return "rgba(" + [o.r, o.g, o.b, o.a].join(",") + ")";
}
};
o.normalize = function () {
function clamp(min, value, max) {
return value < min ? min : (value > max ? max : value);
}
o.r = clamp(0, parseInt(o.r), 255);
o.g = clamp(0, parseInt(o.g), 255);
o.b = clamp(0, parseInt(o.b), 255);
o.a = clamp(0, o.a, 1);
return o;
};
o.clone = function () {
return $.color.make(o.r, o.b, o.g, o.a);
};
return o.normalize();
}
// extract CSS color property from element, going up in the DOM
// if it's "transparent"
$.color.extract = function (elem, css) {
var c;
do {
c = elem.css(css).toLowerCase();
// keep going until we find an element that has color, or
// we hit the body or root (have no parent)
if (c !== '' && c !== 'transparent') {
break;
}
elem = elem.parent();
} while (elem.length && !$.nodeName(elem.get(0), "body"));
// catch Safari's way of signalling transparent
if (c === "rgba(0, 0, 0, 0)") {
c = "transparent";
}
return $.color.parse(c);
}
// parse CSS color string (like "rgb(10, 32, 43)" or "#fff"),
// returns color object, if parsing failed, you get black (0, 0,
// 0) out
$.color.parse = function (str) {
var res, m = $.color.make;
// Look for rgb(num,num,num)
res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str);
if (res) {
return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10));
}
// Look for rgba(num,num,num,num)
res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)
if (res) {
return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4]));
}
// Look for rgb(num%,num%,num%)
res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*\)/.exec(str);
if (res) {
return m(parseFloat(res[1]) * 2.55, parseFloat(res[2]) * 2.55, parseFloat(res[3]) * 2.55);
}
// Look for rgba(num%,num%,num%,num)
res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str);
if (res) {
return m(parseFloat(res[1]) * 2.55, parseFloat(res[2]) * 2.55, parseFloat(res[3]) * 2.55, parseFloat(res[4]));
}
// Look for #a0b1c2
res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str);
if (res) {
return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16));
}
// Look for #fff
res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str);
if (res) {
return m(parseInt(res[1] + res[1], 16), parseInt(res[2] + res[2], 16), parseInt(res[3] + res[3], 16));
}
// Otherwise, we're most likely dealing with a named color
var name = $.trim(str).toLowerCase();
if (name === "transparent") {
return m(255, 255, 255, 0);
} else {
// default to black
res = lookupColors[name] || [0, 0, 0];
return m(res[0], res[1], res[2]);
}
}
var lookupColors = {
aqua: [0, 255, 255],
azure: [240, 255, 255],
beige: [245, 245, 220],
black: [0, 0, 0],
blue: [0, 0, 255],
brown: [165, 42, 42],
cyan: [0, 255, 255],
darkblue: [0, 0, 139],
darkcyan: [0, 139, 139],
darkgrey: [169, 169, 169],
darkgreen: [0, 100, 0],
darkkhaki: [189, 183, 107],
darkmagenta: [139, 0, 139],
darkolivegreen: [85, 107, 47],
darkorange: [255, 140, 0],
darkorchid: [153, 50, 204],
darkred: [139, 0, 0],
darksalmon: [233, 150, 122],
darkviolet: [148, 0, 211],
fuchsia: [255, 0, 255],
gold: [255, 215, 0],
green: [0, 128, 0],
indigo: [75, 0, 130],
khaki: [240, 230, 140],
lightblue: [173, 216, 230],
lightcyan: [224, 255, 255],
lightgreen: [144, 238, 144],
lightgrey: [211, 211, 211],
lightpink: [255, 182, 193],
lightyellow: [255, 255, 224],
lime: [0, 255, 0],
magenta: [255, 0, 255],
maroon: [128, 0, 0],
navy: [0, 0, 128],
olive: [128, 128, 0],
orange: [255, 165, 0],
pink: [255, 192, 203],
purple: [128, 0, 128],
violet: [128, 0, 128],
red: [255, 0, 0],
silver: [192, 192, 192],
white: [255, 255, 255],
yellow: [255, 255, 0]
};
})(jQuery);

View File

@ -0,0 +1,11 @@
/* jQuery Flot Animator version 1.0.
Flot Animator is a free jQuery Plugin that will add fluid animations to Flot charts.
Copyright (c) 2012-2013 Chtiwi Malek
http://www.codicode.com/art/jquery_flot_animator.aspx
Licensed under Creative Commons Attribution 3.0 Unported License.
*/
eval(function(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\\b'+c.toString(a)+'\\b','g'),k[c]);return p;}('$.1m({1w:b(e,t,n){b h(){3 e=o[0][0];3 t=o[o.8-1][0];3 n=(t-e)/a;3 r=[];r.6(o[0]);3 i=1;7=o[0];4=o[i];q(3 s=e+n;s<t+n;s+=n){9(s>t){s=t}$("#18").19(s);1a(s>4[0]){7=4;4=o[i++]}9(s==4[0]){r.6([s,4[1]]);7=4;4=o[i++]}11{3 u=(4[1]-7[1])/(4[0]-7[0]);16=u*s+(7[1]-u*7[0]);r.6([s,16])}}j r}b v(){3 n=[];p++;1b(c){14"1c":n=d.w(-1*p);y;14"1h":n=d.w(d.8/2-p/2,d.8/2+p/2);y;1d:n=d.w(0,p);y}9(!u){13=n[0][0];12=n[n.8-1][0];n=[];q(3 i=0;i<o.8;i++){9(o[i][0]>=13&&o[i][0]<=12){n.6(o[i])}}}t[r].x=p<a?n:o;g.1j(t);g.1i();9(p<a){15(v,f/a)}11{e.1g("1f")}}b m(i){3 s=[];s.6([i[0][0],k.1e.10(k,i.z(b(e){j e[1]}))]);s.6([i[0][0],17]);s.6([i[0][0],k.1k.10(k,i.z(b(e){j e[1]}))]);q(3 o=0;o<i.8;o++){s.6([i[o][0],17])}t[r].x=s;j $.1l(e,t,n)}3 r=0;q(3 i=0;i<t.8;i++){9(t[i].5){r=i}}3 s=t[r];3 o=s.x;3 u=t[r].1v?1x:1t;3 a=t[r].5&&t[r].5.1r||1q;3 f=t[r].5&&t[r].5.1p||1o;3 l=t[r].5&&t[r].5.1n||0;3 c=t[r].5&&t[r].5.1u||"1s";3 p=0;3 d=h();3 g=m(o);15(v,l);j g}})',36,70,'|||var|nPoint|animator|push|lPoint|length|if||function||||||||return|Math||||||for||||||slice|data|break|map|apply|else|laV|inV|case|setTimeout|curV|null|m2|html|while|switch|left|default|max|animatorComplete|trigger|center|draw|setData|min|plot|extend|start|1e3|duration|135|steps|right|false|direction|lines|plotAnimator|true'.split('|')))

View File

@ -0,0 +1,466 @@
/*
Axis Labels Plugin for flot.
http://github.com/markrcote/flot-axislabels
Original code is Copyright (c) 2010 Xuan Luo.
Original code was released under the GPLv3 license by Xuan Luo, September 2010.
Original code was rereleased under the MIT license by Xuan Luo, April 2012.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
(function ($) {
var options = {
axisLabels: {
show: true
}
};
function canvasSupported() {
return !!document.createElement('canvas').getContext;
}
function canvasTextSupported() {
if (!canvasSupported()) {
return false;
}
var dummy_canvas = document.createElement('canvas');
var context = dummy_canvas.getContext('2d');
return typeof context.fillText == 'function';
}
function css3TransitionSupported() {
var div = document.createElement('div');
return typeof div.style.MozTransition != 'undefined' // Gecko
|| typeof div.style.OTransition != 'undefined' // Opera
|| typeof div.style.webkitTransition != 'undefined' // WebKit
|| typeof div.style.transition != 'undefined';
}
function AxisLabel(axisName, position, padding, plot, opts) {
this.axisName = axisName;
this.position = position;
this.padding = padding;
this.plot = plot;
this.opts = opts;
this.width = 0;
this.height = 0;
}
AxisLabel.prototype.cleanup = function() {
};
CanvasAxisLabel.prototype = new AxisLabel();
CanvasAxisLabel.prototype.constructor = CanvasAxisLabel;
function CanvasAxisLabel(axisName, position, padding, plot, opts) {
AxisLabel.prototype.constructor.call(this, axisName, position, padding,
plot, opts);
}
CanvasAxisLabel.prototype.calculateSize = function() {
if (!this.opts.axisLabelFontSizePixels)
this.opts.axisLabelFontSizePixels = 14;
if (!this.opts.axisLabelFontFamily)
this.opts.axisLabelFontFamily = 'sans-serif';
var textWidth = this.opts.axisLabelFontSizePixels + this.padding;
var textHeight = this.opts.axisLabelFontSizePixels + this.padding;
if (this.position == 'left' || this.position == 'right') {
this.width = this.opts.axisLabelFontSizePixels + this.padding;
this.height = 0;
} else {
this.width = 0;
this.height = this.opts.axisLabelFontSizePixels + this.padding;
}
};
CanvasAxisLabel.prototype.draw = function(box) {
if (!this.opts.axisLabelColour)
this.opts.axisLabelColour = 'black';
var ctx = this.plot.getCanvas().getContext('2d');
ctx.save();
ctx.font = this.opts.axisLabelFontSizePixels + 'px ' +
this.opts.axisLabelFontFamily;
ctx.fillStyle = this.opts.axisLabelColour;
var width = ctx.measureText(this.opts.axisLabel).width;
var height = this.opts.axisLabelFontSizePixels;
var x, y, angle = 0;
if (this.position == 'top') {
x = box.left + box.width/2 - width/2;
y = box.top + height*0.72;
} else if (this.position == 'bottom') {
x = box.left + box.width/2 - width/2;
y = box.top + box.height - height*0.72;
} else if (this.position == 'left') {
x = box.left + height*0.72;
y = box.height/2 + box.top + width/2;
angle = -Math.PI/2;
} else if (this.position == 'right') {
x = box.left + box.width - height*0.72;
y = box.height/2 + box.top - width/2;
angle = Math.PI/2;
}
ctx.translate(x, y);
ctx.rotate(angle);
ctx.fillText(this.opts.axisLabel, 0, 0);
ctx.restore();
};
HtmlAxisLabel.prototype = new AxisLabel();
HtmlAxisLabel.prototype.constructor = HtmlAxisLabel;
function HtmlAxisLabel(axisName, position, padding, plot, opts) {
AxisLabel.prototype.constructor.call(this, axisName, position,
padding, plot, opts);
this.elem = null;
}
HtmlAxisLabel.prototype.calculateSize = function() {
var elem = $('<div class="axisLabels" style="position:absolute;">' +
this.opts.axisLabel + '</div>');
this.plot.getPlaceholder().append(elem);
// store height and width of label itself, for use in draw()
this.labelWidth = elem.outerWidth(true);
this.labelHeight = elem.outerHeight(true);
elem.remove();
this.width = this.height = 0;
if (this.position == 'left' || this.position == 'right') {
this.width = this.labelWidth + this.padding;
} else {
this.height = this.labelHeight + this.padding;
}
};
HtmlAxisLabel.prototype.cleanup = function() {
if (this.elem) {
this.elem.remove();
}
};
HtmlAxisLabel.prototype.draw = function(box) {
this.plot.getPlaceholder().find('#' + this.axisName + 'Label').remove();
this.elem = $('<div id="' + this.axisName +
'Label" " class="axisLabels" style="position:absolute;">'
+ this.opts.axisLabel + '</div>');
this.plot.getPlaceholder().append(this.elem);
if (this.position == 'top') {
this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 +
'px');
this.elem.css('top', box.top + 'px');
} else if (this.position == 'bottom') {
this.elem.css('left', box.left + box.width/2 - this.labelWidth/2 +
'px');
this.elem.css('top', box.top + box.height - this.labelHeight +
'px');
} else if (this.position == 'left') {
this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 +
'px');
this.elem.css('left', box.left + 'px');
} else if (this.position == 'right') {
this.elem.css('top', box.top + box.height/2 - this.labelHeight/2 +
'px');
this.elem.css('left', box.left + box.width - this.labelWidth +
'px');
}
};
CssTransformAxisLabel.prototype = new HtmlAxisLabel();
CssTransformAxisLabel.prototype.constructor = CssTransformAxisLabel;
function CssTransformAxisLabel(axisName, position, padding, plot, opts) {
HtmlAxisLabel.prototype.constructor.call(this, axisName, position,
padding, plot, opts);
}
CssTransformAxisLabel.prototype.calculateSize = function() {
HtmlAxisLabel.prototype.calculateSize.call(this);
this.width = this.height = 0;
if (this.position == 'left' || this.position == 'right') {
this.width = this.labelHeight + this.padding;
} else {
this.height = this.labelHeight + this.padding;
}
};
CssTransformAxisLabel.prototype.transforms = function(degrees, x, y) {
var stransforms = {
'-moz-transform': '',
'-webkit-transform': '',
'-o-transform': '',
'-ms-transform': ''
};
if (x != 0 || y != 0) {
var stdTranslate = ' translate(' + x + 'px, ' + y + 'px)';
stransforms['-moz-transform'] += stdTranslate;
stransforms['-webkit-transform'] += stdTranslate;
stransforms['-o-transform'] += stdTranslate;
stransforms['-ms-transform'] += stdTranslate;
}
if (degrees != 0) {
var rotation = degrees / 90;
var stdRotate = ' rotate(' + degrees + 'deg)';
stransforms['-moz-transform'] += stdRotate;
stransforms['-webkit-transform'] += stdRotate;
stransforms['-o-transform'] += stdRotate;
stransforms['-ms-transform'] += stdRotate;
}
var s = 'top: 0; left: 0; ';
for (var prop in stransforms) {
if (stransforms[prop]) {
s += prop + ':' + stransforms[prop] + ';';
}
}
s += ';';
return s;
};
CssTransformAxisLabel.prototype.calculateOffsets = function(box) {
var offsets = { x: 0, y: 0, degrees: 0 };
if (this.position == 'bottom') {
offsets.x = box.left + box.width/2 - this.labelWidth/2;
offsets.y = box.top + box.height - this.labelHeight;
} else if (this.position == 'top') {
offsets.x = box.left + box.width/2 - this.labelWidth/2;
offsets.y = box.top;
} else if (this.position == 'left') {
offsets.degrees = -90;
offsets.x = box.left - this.labelWidth/2 + this.labelHeight/2;
offsets.y = box.height/2 + box.top;
} else if (this.position == 'right') {
offsets.degrees = 90;
offsets.x = box.left + box.width - this.labelWidth/2
- this.labelHeight/2;
offsets.y = box.height/2 + box.top;
}
offsets.x = Math.round(offsets.x);
offsets.y = Math.round(offsets.y);
return offsets;
};
CssTransformAxisLabel.prototype.draw = function(box) {
this.plot.getPlaceholder().find("." + this.axisName + "Label").remove();
var offsets = this.calculateOffsets(box);
this.elem = $('<div class="axisLabels ' + this.axisName +
'Label" style="position:absolute; ' +
this.transforms(offsets.degrees, offsets.x, offsets.y) +
'">' + this.opts.axisLabel + '</div>');
this.plot.getPlaceholder().append(this.elem);
};
IeTransformAxisLabel.prototype = new CssTransformAxisLabel();
IeTransformAxisLabel.prototype.constructor = IeTransformAxisLabel;
function IeTransformAxisLabel(axisName, position, padding, plot, opts) {
CssTransformAxisLabel.prototype.constructor.call(this, axisName,
position, padding,
plot, opts);
this.requiresResize = false;
}
IeTransformAxisLabel.prototype.transforms = function(degrees, x, y) {
// I didn't feel like learning the crazy Matrix stuff, so this uses
// a combination of the rotation transform and CSS positioning.
var s = '';
if (degrees != 0) {
var rotation = degrees/90;
while (rotation < 0) {
rotation += 4;
}
s += ' filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=' + rotation + '); ';
// see below
this.requiresResize = (this.position == 'right');
}
if (x != 0) {
s += 'left: ' + x + 'px; ';
}
if (y != 0) {
s += 'top: ' + y + 'px; ';
}
return s;
};
IeTransformAxisLabel.prototype.calculateOffsets = function(box) {
var offsets = CssTransformAxisLabel.prototype.calculateOffsets.call(
this, box);
// adjust some values to take into account differences between
// CSS and IE rotations.
if (this.position == 'top') {
// FIXME: not sure why, but placing this exactly at the top causes
// the top axis label to flip to the bottom...
offsets.y = box.top + 1;
} else if (this.position == 'left') {
offsets.x = box.left;
offsets.y = box.height/2 + box.top - this.labelWidth/2;
} else if (this.position == 'right') {
offsets.x = box.left + box.width - this.labelHeight;
offsets.y = box.height/2 + box.top - this.labelWidth/2;
}
return offsets;
};
IeTransformAxisLabel.prototype.draw = function(box) {
CssTransformAxisLabel.prototype.draw.call(this, box);
if (this.requiresResize) {
this.elem = this.plot.getPlaceholder().find("." + this.axisName +
"Label");
// Since we used CSS positioning instead of transforms for
// translating the element, and since the positioning is done
// before any rotations, we have to reset the width and height
// in case the browser wrapped the text (specifically for the
// y2axis).
this.elem.css('width', this.labelWidth);
this.elem.css('height', this.labelHeight);
}
};
function init(plot) {
plot.hooks.processOptions.push(function (plot, options) {
if (!options.axisLabels.show)
return;
// This is kind of a hack. There are no hooks in Flot between
// the creation and measuring of the ticks (setTicks, measureTickLabels
// in setupGrid() ) and the drawing of the ticks and plot box
// (insertAxisLabels in setupGrid() ).
//
// Therefore, we use a trick where we run the draw routine twice:
// the first time to get the tick measurements, so that we can change
// them, and then have it draw it again.
var secondPass = false;
var axisLabels = {};
var axisOffsetCounts = { left: 0, right: 0, top: 0, bottom: 0 };
var defaultPadding = 2; // padding between axis and tick labels
plot.hooks.draw.push(function (plot, ctx) {
var hasAxisLabels = false;
if (!secondPass) {
// MEASURE AND SET OPTIONS
$.each(plot.getAxes(), function(axisName, axis) {
var opts = axis.options // Flot 0.7
|| plot.getOptions()[axisName]; // Flot 0.6
// Handle redraws initiated outside of this plug-in.
if (axisName in axisLabels) {
axis.labelHeight = axis.labelHeight -
axisLabels[axisName].height;
axis.labelWidth = axis.labelWidth -
axisLabels[axisName].width;
opts.labelHeight = axis.labelHeight;
opts.labelWidth = axis.labelWidth;
axisLabels[axisName].cleanup();
delete axisLabels[axisName];
}
if (!opts || !opts.axisLabel || !axis.show)
return;
hasAxisLabels = true;
var renderer = null;
if (!opts.axisLabelUseHtml &&
navigator.appName == 'Microsoft Internet Explorer') {
var ua = navigator.userAgent;
var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
if (re.exec(ua) != null) {
rv = parseFloat(RegExp.$1);
}
if (rv >= 9 && !opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
renderer = CssTransformAxisLabel;
} else if (!opts.axisLabelUseCanvas && !opts.axisLabelUseHtml) {
renderer = IeTransformAxisLabel;
} else if (opts.axisLabelUseCanvas) {
renderer = CanvasAxisLabel;
} else {
renderer = HtmlAxisLabel;
}
} else {
if (opts.axisLabelUseHtml || (!css3TransitionSupported() && !canvasTextSupported()) && !opts.axisLabelUseCanvas) {
renderer = HtmlAxisLabel;
} else if (opts.axisLabelUseCanvas || !css3TransitionSupported()) {
renderer = CanvasAxisLabel;
} else {
renderer = CssTransformAxisLabel;
}
}
var padding = opts.axisLabelPadding === undefined ?
defaultPadding : opts.axisLabelPadding;
axisLabels[axisName] = new renderer(axisName,
axis.position, padding,
plot, opts);
// flot interprets axis.labelHeight and .labelWidth as
// the height and width of the tick labels. We increase
// these values to make room for the axis label and
// padding.
axisLabels[axisName].calculateSize();
// AxisLabel.height and .width are the size of the
// axis label and padding.
// Just set opts here because axis will be sorted out on
// the redraw.
opts.labelHeight = axis.labelHeight +
axisLabels[axisName].height;
opts.labelWidth = axis.labelWidth +
axisLabels[axisName].width;
});
// If there are axis labels, re-draw with new label widths and
// heights.
if (hasAxisLabels) {
secondPass = true;
plot.setupGrid();
plot.draw();
}
} else {
secondPass = false;
// DRAW
$.each(plot.getAxes(), function(axisName, axis) {
var opts = axis.options // Flot 0.7
|| plot.getOptions()[axisName]; // Flot 0.6
if (!opts || !opts.axisLabel || !axis.show)
return;
axisLabels[axisName].draw(axis.box);
});
}
});
});
}
$.plot.plugins.push({
init: init,
options: options,
name: 'axisLabels',
version: '2.0'
});
})(jQuery);

View File

@ -0,0 +1,98 @@
/** ## jquery.flot.browser.js
This plugin is used to make available some browser-related utility functions.
### Methods
*/
(function ($) {
'use strict';
var browser = {
/**
- getPageXY(e)
Calculates the pageX and pageY using the screenX, screenY properties of the event
and the scrolling of the page. This is needed because the pageX and pageY
properties of the event are not correct while running tests in Edge. */
getPageXY: function (e) {
// This code is inspired from https://stackoverflow.com/a/3464890
var doc = document.documentElement,
pageX = e.clientX + (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0),
pageY = e.clientY + (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
return { X: pageX, Y: pageY };
},
/**
- getPixelRatio(context)
This function returns the current pixel ratio defined by the product of desktop
zoom and page zoom.
Additional info: https://www.html5rocks.com/en/tutorials/canvas/hidpi/
*/
getPixelRatio: function(context) {
var devicePixelRatio = window.devicePixelRatio || 1,
backingStoreRatio =
context.webkitBackingStorePixelRatio ||
context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio ||
context.oBackingStorePixelRatio ||
context.backingStorePixelRatio || 1;
return devicePixelRatio / backingStoreRatio;
},
/**
- isSafari, isMobileSafari, isOpera, isFirefox, isIE, isEdge, isChrome, isBlink
This is a collection of functions, used to check if the code is running in a
particular browser or Javascript engine.
*/
isSafari: function() {
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
// Safari 3.0+ "[object HTMLElementConstructor]"
return /constructor/i.test(window.top.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window.top['safari'] || (typeof window.top.safari !== 'undefined' && window.top.safari.pushNotification));
},
isMobileSafari: function() {
//isMobileSafari adapted from https://stackoverflow.com/questions/3007480/determine-if-user-navigated-from-mobile-safari
return navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/);
},
isOpera: function() {
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
//Opera 8.0+
return (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
},
isFirefox: function() {
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
// Firefox 1.0+
return typeof InstallTrigger !== 'undefined';
},
isIE: function() {
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
// Internet Explorer 6-11
return /*@cc_on!@*/false || !!document.documentMode;
},
isEdge: function() {
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
// Edge 20+
return !browser.isIE() && !!window.StyleMedia;
},
isChrome: function() {
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
// Chrome 1+
return !!window.chrome && !!window.chrome.webstore;
},
isBlink: function() {
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
return (browser.isChrome() || browser.isOpera()) && !!window.CSS;
}
};
$.plot.browser = browser;
})(jQuery);

View File

@ -0,0 +1,703 @@
/**
## jquery.flot.drawSeries.js
This plugin is used by flot for drawing lines, plots, bars or area.
### Public methods
*/
(function($) {
"use strict";
function DrawSeries() {
function plotLine(datapoints, xoffset, yoffset, axisx, axisy, ctx, steps) {
var points = datapoints.points,
ps = datapoints.pointsize,
prevx = null,
prevy = null;
var x1 = 0.0,
y1 = 0.0,
x2 = 0.0,
y2 = 0.0,
mx = null,
my = null,
i = 0;
var axisy_is_log = (axisy.options.mode == "log");
ctx.beginPath();
for (i = ps; i < points.length; i += ps) {
x1 = points[i - ps];
y1 = points[i - ps + 1];
x2 = points[i];
y2 = points[i + 1];
if (x1 === null || x2 === null) {
mx = null;
my = null;
continue;
}
if (isNaN(x1) || isNaN(x2) || isNaN(y1) || isNaN(y2)) {
prevx = null;
prevy = null;
continue;
}
if(steps){
if (mx !== null && my !== null) {
// if middle point exists, transfer p2 -> p1 and p1 -> mp
x2 = x1;
y2 = y1;
x1 = mx;
y1 = my;
// 'remove' middle point
mx = null;
my = null;
// subtract pointsize from i to have current point p1 handled again
i -= ps;
} else if (y1 !== y2 && x1 !== x2) {
// create a middle point
y2 = y1;
mx = x2;
my = y1;
}
}
// clip with ymin
if (y1 <= y2 && y1 < axisy.min) {
if (y2 < axisy.min && !axisy_is_log) {
// line segment is outside
continue;
}
// compute new intersection point
x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
y1 = axisy.min;
} else if (y2 <= y1 && y2 < axisy.min) {
if (y1 < axisy.min && !axisy_is_log) {
continue;
}
x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
y2 = axisy.min;
}
// clip with ymax
if (y1 >= y2 && y1 > axisy.max) {
if (y2 > axisy.max) {
continue;
}
x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
y1 = axisy.max;
} else if (y2 >= y1 && y2 > axisy.max) {
if (y1 > axisy.max) {
continue;
}
x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
y2 = axisy.max;
}
// clip with xmin
if (x1 <= x2 && x1 < axisx.min) {
if (x2 < axisx.min) {
continue;
}
y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
x1 = axisx.min;
} else if (x2 <= x1 && x2 < axisx.min) {
if (x1 < axisx.min) {
continue;
}
y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
x2 = axisx.min;
}
// clip with xmax
if (x1 >= x2 && x1 > axisx.max) {
if (x2 > axisx.max) {
continue;
}
y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
x1 = axisx.max;
} else if (x2 >= x1 && x2 > axisx.max) {
if (x1 > axisx.max) {
continue;
}
y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
x2 = axisx.max;
}
if (x1 !== prevx || y1 !== prevy) {
if(y1 < axisy.min && axisy_is_log){
y1 = axisy.min;
}
ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
}
prevx = x2;
prevy = y2;
if(y2 < axisy.min && axisy_is_log){
y2 = axisy.min;
}
ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
}
ctx.stroke();
}
function plotLineArea(datapoints, axisx, axisy, fillTowards, ctx, steps) {
var points = datapoints.points,
ps = datapoints.pointsize,
bottom = fillTowards > axisy.min ? Math.min(axisy.max, fillTowards) : axisy.min,
i = 0,
ypos = 1,
areaOpen = false,
segmentStart = 0,
segmentEnd = 0,
mx = null,
my = null;
// we process each segment in two turns, first forward
// direction to sketch out top, then once we hit the
// end we go backwards to sketch the bottom
while (true) {
if (ps > 0 && i > points.length + ps) {
break;
}
i += ps; // ps is negative if going backwards
var x1 = points[i - ps],
y1 = points[i - ps + ypos],
x2 = points[i],
y2 = points[i + ypos];
if (ps === -2) {
/* going backwards and no value for the bottom provided in the series*/
y1 = y2 = bottom;
}
if (areaOpen) {
if (ps > 0 && x1 != null && x2 == null) {
// at turning point
segmentEnd = i;
ps = -ps;
ypos = 2;
continue;
}
if (ps < 0 && i === segmentStart + ps) {
// done with the reverse sweep
ctx.fill();
areaOpen = false;
ps = -ps;
ypos = 1;
i = segmentStart = segmentEnd + ps;
continue;
}
}
if (x1 == null || x2 == null) {
mx = null;
my = null;
continue;
}
if(steps){
if (mx !== null && my !== null) {
// if middle point exists, transfer p2 -> p1 and p1 -> mp
x2 = x1;
y2 = y1;
x1 = mx;
y1 = my;
// 'remove' middle point
mx = null;
my = null;
// subtract pointsize from i to have current point p1 handled again
i -= ps;
} else if (y1 !== y2 && x1 !== x2) {
// create a middle point
y2 = y1;
mx = x2;
my = y1;
}
}
// clip x values
// clip with xmin
if (x1 <= x2 && x1 < axisx.min) {
if (x2 < axisx.min) {
continue;
}
y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
x1 = axisx.min;
} else if (x2 <= x1 && x2 < axisx.min) {
if (x1 < axisx.min) {
continue;
}
y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
x2 = axisx.min;
}
// clip with xmax
if (x1 >= x2 && x1 > axisx.max) {
if (x2 > axisx.max) {
continue;
}
y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
x1 = axisx.max;
} else if (x2 >= x1 && x2 > axisx.max) {
if (x1 > axisx.max) {
continue;
}
y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
x2 = axisx.max;
}
if (!areaOpen) {
// open area
ctx.beginPath();
ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
areaOpen = true;
}
// now first check the case where both is outside
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 <= axisy.min && y2 <= axisy.min) {
ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
continue;
}
// else it's a bit more complicated, there might
// be a flat maxed out rectangle first, then a
// triangular cutout or reverse; to find these
// keep track of the current x values
var x1old = x1,
x2old = x2;
// clip the y values, without shortcutting, we
// go through all cases in turn
// clip with ymin
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 < axisy.min && y1 >= axisy.min) {
x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
y2 = axisy.min;
}
// clip with ymax
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 > 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) {
ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
// it goes to (x1, y1), but we fill that below
}
// fill triangular section, this sometimes result
// in redundant points if (x1, y1) hasn't changed
// from previous line to, but we just ignore that
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) {
ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
}
}
}
function get_sort_indices(arr){
return Array.from(Array(arr.length).keys())
.sort((a, b) => arr[a] < arr[b] ? -1 : (arr[b] < arr[a]) | 0)
}
function sortWithIndices(toSort,indices) {
return indices.map(i => toSort[i]);
}
/**
- drawSeriesLines(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient)
This function is used for drawing lines or area fill. In case the series has line decimation function
attached, before starting to draw, as an optimization the points will first be decimated.
The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and
plotHeight are the corresponding parameters of flot used to determine the drawing surface.
The function getColorOrGradient is used to compute the fill style of lines and area.
*/
function drawSeriesLines(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) {
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
ctx.lineJoin = "round";
if (series.lines.dashes && ctx.setLineDash) {
ctx.setLineDash(series.lines.dashes);
}
var datapoints = {
format: series.datapoints.format,
points: series.datapoints.points,
pointsize: series.datapoints.pointsize
};
if (series.decimate) {
datapoints.points = series.decimate(series, series.xaxis.min, series.xaxis.max, plotWidth, series.yaxis.min, series.yaxis.max, plotHeight);
}
var lw = series.lines.lineWidth;
ctx.lineWidth = lw;
ctx.strokeStyle = series.color;
var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight, getColorOrGradient);
if(series.lines.is_sort){
var points_group = {};
var indice_sort = null;
var point_size = datapoints.pointsize;
for(var ii = 0;ii < point_size; ii++){
var points_tmp = datapoints.points.filter(function(v,i){
return i % point_size == ii;
})
points_group[ii] = points_tmp;
if(ii == 0){
indice_sort = get_sort_indices(points_tmp);
}
}
Object.keys(points_group).forEach(function(ii){
points_group[ii] = sortWithIndices(points_group[ii],indice_sort);
})
points_tmp = [];
var arr_size = points_group[0].length;
for(var i = 0;i < arr_size; i++){
Object.keys(points_group).forEach(function(ii){
points_tmp.push(points_group[ii][i]);
})
}
datapoints.points = points_tmp;
}
if (fillStyle) {
ctx.fillStyle = fillStyle;
plotLineArea(datapoints, series.xaxis, series.yaxis, series.lines.fillTowards || 0, ctx, series.lines.steps);
}
if (lw > 0) {
plotLine(datapoints, 0, 0, series.xaxis, series.yaxis, ctx, series.lines.steps);
}
ctx.restore();
}
/**
- drawSeriesPoints(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient)
This function is used for drawing points using a given symbol. In case the series has points decimation
function attached, before starting to draw, as an optimization the points will first be decimated.
The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and
plotHeight are the corresponding parameters of flot used to determine the drawing surface.
The function drawSymbol is used to compute and draw the symbol chosen for the points.
*/
function drawSeriesPoints(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) {
function drawCircle(ctx, x, y, radius, shadow, fill) {
ctx.moveTo(x + radius, y);
ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
}
drawCircle.fill = true;
function plotPoints(datapoints, radius, fill, offset, shadow, axisx, axisy, drawSymbolFn) {
var points = datapoints.points,
ps = datapoints.pointsize;
ctx.beginPath();
for (var i = 0; i < points.length; i += ps) {
var x = points[i],
y = points[i + 1];
if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) {
continue;
}
x = axisx.p2c(x);
y = axisy.p2c(y) + offset;
drawSymbolFn(ctx, x, y, radius, shadow, fill);
}
if (drawSymbolFn.fill && !shadow) {
ctx.fill();
}
ctx.stroke();
}
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
var datapoints = {
format: series.datapoints.format,
points: series.datapoints.points,
pointsize: series.datapoints.pointsize
};
if (series.decimatePoints) {
datapoints.points = series.decimatePoints(series, series.xaxis.min, series.xaxis.max, plotWidth, series.yaxis.min, series.yaxis.max, plotHeight);
}
var lw = series.points.lineWidth,
radius = series.points.radius,
symbol = series.points.symbol,
drawSymbolFn;
if (symbol === 'circle') {
drawSymbolFn = drawCircle;
} else if (typeof symbol === 'string' && drawSymbol && drawSymbol[symbol]) {
drawSymbolFn = drawSymbol[symbol];
} else if (typeof drawSymbol === 'function') {
drawSymbolFn = drawSymbol;
}
// If the user sets the line width to 0, we change it to a very
// small value. A line width of 0 seems to force the default of 1.
if (lw === 0) {
lw = 0.0001;
}
ctx.lineWidth = lw;
ctx.fillStyle = getFillStyle(series.points, series.color, null, null, getColorOrGradient);
ctx.strokeStyle = series.color;
plotPoints(datapoints, radius,
true, 0, false,
series.xaxis, series.yaxis, drawSymbolFn);
ctx.restore();
}
function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
var left = x + barLeft,
right = x + barRight,
bottom = b, top = y,
drawLeft, drawRight, drawTop, drawBottom = false,
tmp;
drawLeft = drawRight = drawTop = true;
// in horizontal mode, we start the bar from the left
// instead of from the bottom so it appears to be
// horizontal rather than vertical
if (horizontal) {
drawBottom = drawRight = drawTop = true;
drawLeft = false;
left = b;
right = x;
top = y + barLeft;
bottom = y + barRight;
// account for negative bars
if (right < left) {
tmp = right;
right = left;
left = tmp;
drawLeft = true;
drawRight = false;
}
}
else {
drawLeft = drawRight = drawTop = true;
drawBottom = false;
left = x + barLeft;
right = x + barRight;
bottom = b;
top = y;
// account for negative bars
if (top < bottom) {
tmp = top;
top = bottom;
bottom = tmp;
drawBottom = true;
drawTop = false;
}
}
// clip
if (right < axisx.min || left > axisx.max ||
top < axisy.min || bottom > axisy.max) {
return;
}
if (left < axisx.min) {
left = axisx.min;
drawLeft = false;
}
if (right > axisx.max) {
right = axisx.max;
drawRight = false;
}
if (bottom < axisy.min) {
bottom = axisy.min;
drawBottom = false;
}
if (top > axisy.max) {
top = axisy.max;
drawTop = false;
}
left = axisx.p2c(left);
bottom = axisy.p2c(bottom);
right = axisx.p2c(right);
top = axisy.p2c(top);
// fill the bar
if (fillStyleCallback) {
c.fillStyle = fillStyleCallback(bottom, top);
c.fillRect(left, top, right - left, bottom - top)
}
// draw outline
if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
c.beginPath();
// FIXME: inline moveTo is buggy with excanvas
c.moveTo(left, bottom);
if (drawLeft) {
c.lineTo(left, top);
} else {
c.moveTo(left, top);
}
if (drawTop) {
c.lineTo(right, top);
} else {
c.moveTo(right, top);
}
if (drawRight) {
c.lineTo(right, bottom);
} else {
c.moveTo(right, bottom);
}
if (drawBottom) {
c.lineTo(left, bottom);
} else {
c.moveTo(left, bottom);
}
c.stroke();
}
}
/**
- drawSeriesBars(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient)
This function is used for drawing series represented as bars. In case the series has decimation
function attached, before starting to draw, as an optimization the points will first be decimated.
The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and
plotHeight are the corresponding parameters of flot used to determine the drawing surface.
The function getColorOrGradient is used to compute the fill style of bars.
*/
function drawSeriesBars(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) {
function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) {
var points = datapoints.points,
ps = datapoints.pointsize,
fillTowards = series.bars.fillTowards || 0,
calculatedBottom = fillTowards > axisy.min ? Math.min(axisy.max, fillTowards) : axisy.min;
for (var i = 0; i < points.length; i += ps) {
if (points[i] == null) {
continue;
}
// Use third point as bottom if pointsize is 3
var bottom = ps === 3 ? points[i + 2] : calculatedBottom;
drawBar(points[i], points[i + 1], bottom, barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
}
}
ctx.save();
ctx.translate(plotOffset.left, plotOffset.top);
var datapoints = {
format: series.datapoints.format,
points: series.datapoints.points,
pointsize: series.datapoints.pointsize
};
if (series.decimate) {
datapoints.points = series.decimate(series, series.xaxis.min, series.xaxis.max, plotWidth);
}
ctx.lineWidth = series.bars.lineWidth;
ctx.strokeStyle = series.color;
var barLeft;
var barWidth = series.bars.barWidth[0] || series.bars.barWidth;
switch (series.bars.align) {
case "left":
barLeft = 0;
break;
case "right":
barLeft = -barWidth;
break;
default:
barLeft = -barWidth / 2;
}
var fillStyleCallback = series.bars.fill ? function(bottom, top) {
return getFillStyle(series.bars, series.color, bottom, top, getColorOrGradient);
} : null;
plotBars(datapoints, barLeft, barLeft + barWidth, fillStyleCallback, series.xaxis, series.yaxis);
ctx.restore();
}
function getFillStyle(filloptions, seriesColor, bottom, top, getColorOrGradient) {
var fill = filloptions.fill;
if (!fill) {
return null;
}
if (filloptions.fillColor) {
return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
}
var c = $.color.parse(seriesColor);
c.a = typeof fill === "number" ? fill : 0.4;
c.normalize();
return c.toString();
}
this.drawSeriesLines = drawSeriesLines;
this.drawSeriesPoints = drawSeriesPoints;
this.drawSeriesBars = drawSeriesBars;
this.drawBar = drawBar;
};
$.plot.drawSeries = new DrawSeries();
})(jQuery);

View File

@ -0,0 +1,354 @@
/* global jQuery */
/**
## jquery.flot.hover.js
This plugin is used for mouse hover and tap on a point of plot series.
It supports the following options:
```js
grid: {
hoverable: false, //to trigger plothover event on mouse hover or tap on a point
clickable: false //to trigger plotclick event on mouse hover
}
```
It listens to native mouse move event or click, as well as artificial generated
tap and touchevent.
When the mouse is over a point or a tap on a point is performed, that point or
the correscponding bar will be highlighted and a "plothover" event will be generated.
Custom "touchevent" is triggered when any touch interaction is made. Hover plugin
handles this events by unhighlighting all of the previously highlighted points and generates
"plothovercleanup" event to notify any part that is handling plothover (for exemple to cleanup
the tooltip from webcharts).
*/
(function($) {
'use strict';
var options = {
grid: {
hoverable: false,
clickable: false
}
};
var browser = $.plot.browser;
var eventType = {
click: 'click',
hover: 'hover'
}
function init(plot) {
var lastMouseMoveEvent;
var highlights = [];
function bindEvents(plot, eventHolder) {
var o = plot.getOptions();
if (o.grid.hoverable || o.grid.clickable) {
eventHolder[0].addEventListener('touchevent', triggerCleanupEvent, false);
eventHolder[0].addEventListener('tap', generatePlothoverEvent, false);
}
if (o.grid.clickable) {
eventHolder.bind("click", onClick);
}
if (o.grid.hoverable) {
eventHolder.bind("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);
}
}
function shutdown(plot, eventHolder) {
eventHolder[0].removeEventListener('tap', generatePlothoverEvent);
eventHolder[0].removeEventListener('touchevent', triggerCleanupEvent);
eventHolder.unbind("mousemove", onMouseMove);
eventHolder.unbind("mouseleave", onMouseLeave);
eventHolder.unbind("click", onClick);
highlights = [];
}
function generatePlothoverEvent(e) {
var o = plot.getOptions(),
newEvent = new CustomEvent('mouseevent');
//transform from touch event to mouse event format
newEvent.pageX = e.detail.changedTouches[0].pageX;
newEvent.pageY = e.detail.changedTouches[0].pageY;
newEvent.clientX = e.detail.changedTouches[0].clientX;
newEvent.clientY = e.detail.changedTouches[0].clientY;
if (o.grid.hoverable) {
doTriggerClickHoverEvent(newEvent, eventType.hover, 30);
}
return false;
}
function doTriggerClickHoverEvent(event, eventType, searchDistance) {
var series = plot.getData();
if (event !== undefined
&& series.length > 0
&& series[0].xaxis.c2p !== undefined
&& series[0].yaxis.c2p !== undefined) {
var eventToTrigger = "plot" + eventType;
var seriesFlag = eventType + "able";
triggerClickHoverEvent(eventToTrigger, event,
function(i) {
return series[i][seriesFlag] !== false;
}, searchDistance);
}
}
function onMouseMove(e) {
lastMouseMoveEvent = e;
plot.getPlaceholder()[0].lastMouseMoveEvent = e;
doTriggerClickHoverEvent(e, eventType.hover);
}
function onMouseLeave(e) {
lastMouseMoveEvent = undefined;
plot.getPlaceholder()[0].lastMouseMoveEvent = undefined;
triggerClickHoverEvent("plothover", e,
function(i) {
return false;
});
}
function onClick(e) {
doTriggerClickHoverEvent(e, eventType.click);
}
function triggerCleanupEvent() {
plot.unhighlight();
plot.getPlaceholder().trigger('plothovercleanup');
}
// trigger click or hover event (they send the same parameters
// so we share their code)
function triggerClickHoverEvent(eventname, event, seriesFilter, searchDistance) {
var options = plot.getOptions(),
offset = plot.offset(),
page = browser.getPageXY(event),
canvasX = page.X - offset.left,
canvasY = page.Y - offset.top,
pos = plot.c2p({
left: canvasX,
top: canvasY
}),
distance = searchDistance !== undefined ? searchDistance : options.grid.mouseActiveRadius;
pos.pageX = page.X;
pos.pageY = page.Y;
var item = plot.findNearbyItem(canvasX, canvasY, seriesFilter, distance);
if (item) {
// fill in mouse pos for any listeners out there
item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left, 10);
if(options.yaxis.mode == "log" && item.datapoint[1] < item.series.yaxis.autoScaledMin){
item.pageY = parseInt(item.series.yaxis.p2c(item.series.yaxis.autoScaledMin) + offset.top, 10);
}else{
item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.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])) || !item) {
unhighlight(h.series, h.point);
}
}
if (item) {
highlight(item.series, item.datapoint, eventname);
}
}
plot.getPlaceholder().trigger(eventname, [pos, item]);
}
function highlight(s, point, auto) {
if (typeof s === "number") {
s = plot.getData()[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
});
plot.triggerRedrawOverlay();
} else if (!auto) {
highlights[i].auto = false;
}
}
function unhighlight(s, point) {
if (s == null && point == null) {
highlights = [];
plot.triggerRedrawOverlay();
return;
}
if (typeof s === "number") {
s = plot.getData()[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);
plot.triggerRedrawOverlay();
}
}
function indexOfHighlight(s, p) {
for (var i = 0; i < highlights.length; ++i) {
var h = highlights[i];
if (h.series === s &&
h.point[0] === p[0] &&
h.point[1] === p[1]) {
return i;
}
}
return -1;
}
function processDatapoints() {
triggerCleanupEvent();
doTriggerClickHoverEvent(lastMouseMoveEvent, eventType.hover);
}
function setupGrid() {
doTriggerClickHoverEvent(lastMouseMoveEvent, eventType.hover);
}
function drawOverlay(plot, octx, overlay) {
var plotOffset = plot.getPlotOffset(),
i, hi;
octx.save();
octx.translate(plotOffset.left, plotOffset.top);
for (i = 0; i < highlights.length; ++i) {
hi = highlights[i];
if (hi.series.bars.show) drawBarHighlight(hi.series, hi.point, octx);
else drawPointHighlight(hi.series, hi.point, octx, plot);
}
octx.restore();
}
function drawPointHighlight(series, point, octx, plot) {
var x = point[0],
y = point[1],
axisx = series.xaxis,
axisy = series.yaxis,
highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString();
if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) {
return;
}
var pointRadius = series.points.radius + series.points.lineWidth / 2;
octx.lineWidth = pointRadius;
octx.strokeStyle = highlightColor;
var radius = 1.5 * pointRadius;
x = axisx.p2c(x);
y = axisy.p2c(y);
octx.beginPath();
var symbol = series.points.symbol;
if (symbol === 'circle') {
octx.arc(x, y, radius, 0, 2 * Math.PI, false);
} else if (typeof symbol === 'string' && plot.drawSymbol && plot.drawSymbol[symbol]) {
plot.drawSymbol[symbol](octx, x, y, radius, false);
}
octx.closePath();
octx.stroke();
}
function drawBarHighlight(series, point, octx) {
var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(),
fillStyle = highlightColor,
barLeft;
var barWidth = series.bars.barWidth[0] || series.bars.barWidth;
switch (series.bars.align) {
case "left":
barLeft = 0;
break;
case "right":
barLeft = -barWidth;
break;
default:
barLeft = -barWidth / 2;
}
octx.lineWidth = series.bars.lineWidth;
octx.strokeStyle = highlightColor;
var fillTowards = series.bars.fillTowards || 0,
bottom = fillTowards > series.yaxis.min ? Math.min(series.yaxis.max, fillTowards) : series.yaxis.min;
$.plot.drawSeries.drawBar(point[0], point[1], point[2] || bottom, barLeft, barLeft + barWidth,
function() {
return fillStyle;
}, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
}
function initHover(plot, options) {
plot.highlight = highlight;
plot.unhighlight = unhighlight;
if (options.grid.hoverable || options.grid.clickable) {
plot.hooks.drawOverlay.push(drawOverlay);
plot.hooks.processDatapoints.push(processDatapoints);
plot.hooks.setupGrid.push(setupGrid);
}
lastMouseMoveEvent = plot.getPlaceholder()[0].lastMouseMoveEvent;
}
plot.hooks.bindEvents.push(bindEvents);
plot.hooks.shutdown.push(shutdown);
plot.hooks.processOptions.push(initHover);
}
$.plot.plugins.push({
init: init,
options: options,
name: 'hover',
version: '0.1'
});
})(jQuery);

View File

@ -0,0 +1,405 @@
/* Flot plugin for drawing legends.
*/
(function($) {
var defaultOptions = {
legend: {
show: false,
noColumns: 1,
labelFormatter: null, // fn: string -> string
container: null, // container (as jQuery object) to put legend in, null means default on top of graph
position: 'ne', // position of default legend container within plot
margin: 5, // distance from grid edge to default legend container within plot
sorted: null // default to no legend sorting
}
};
var series = [];
function insertLegend() {
if (options.legend.container != null) {
$(options.legend.container).html("");
} else {
placeholder.find(".legend").remove();
}
if (!options.legend.show) {
return;
}
var fragments = [], entries = [], rowStarted = false,
lf = options.legend.labelFormatter, s, label;
// Build a list of legend entries, with each having a label and a color
for (var i = 0; i < series.length; ++i) {
s = series[i];
if (s.label) {
label = lf ? lf(s.label, s) : s.label;
if (label) {
entries.push({
label: label,
color: s.color
});
}
}
}
// Sort the legend using either the default or a custom comparator
if (options.legend.sorted) {
if ($.isFunction(options.legend.sorted)) {
entries.sort(options.legend.sorted);
} else if (options.legend.sorted == "reverse") {
entries.reverse();
} else {
var ascending = options.legend.sorted != "descending";
entries.sort(function(a, b) {
return a.label == b.label ? 0 : (
(a.label < b.label) != ascending ? 1 : -1 // Logical XOR
);
});
}
}
// Generate markup for the list of entries, in their final order
for (var i = 0; i < entries.length; ++i) {
var entry = entries[i];
if (i % options.legend.noColumns == 0) {
if (rowStarted)
fragments.push('</tr>');
fragments.push('<tr>');
rowStarted = true;
}
fragments.push(
'<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + entry.color + ';overflow:hidden"></div></div></td>' +
'<td class="legendLabel">' + entry.label + '</td>'
);
}
if (rowStarted)
fragments.push('</tr>');
if (fragments.length == 0)
return;
var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
if (options.legend.container != null)
$(options.legend.container).html(table);
else {
var pos = "",
p = options.legend.position,
m = options.legend.margin;
if (m[0] == null)
m = [m, m];
if (p.charAt(0) == "n")
pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
else if (p.charAt(0) == "s")
pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
if (p.charAt(1) == "e")
pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
else if (p.charAt(1) == "w")
pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
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) {
c = options.grid.backgroundColor;
if (c && typeof c == "string")
c = $.color.parse(c);
else
c = $.color.extract(legend, 'background-color');
c.a = 1;
c = c.toString();
}
var div = legend.children();
$('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
}
}
}
// Generate html for a shape
function getEntryIconHtml(shape) {
var html = '',
name = shape.name,
x = shape.xPos,
y = shape.yPos,
fill = shape.fillColor,
stroke = shape.strokeColor,
width = shape.strokeWidth;
switch (name) {
case 'circle':
html = '<use xlink:href="#circle" class="legendIcon" ' +
'x="' + x + '" ' +
'y="' + y + '" ' +
'fill="' + fill + '" ' +
'stroke="' + stroke + '" ' +
'stroke-width="' + width + '" ' +
'width="1.5em" height="1.5em"' +
'/>';
break;
case 'diamond':
html = '<use xlink:href="#diamond" class="legendIcon" ' +
'x="' + x + '" ' +
'y="' + y + '" ' +
'fill="' + fill + '" ' +
'stroke="' + stroke + '" ' +
'stroke-width="' + width + '" ' +
'width="1.5em" height="1.5em"' +
'/>';
break;
case 'cross':
html = '<use xlink:href="#cross" class="legendIcon" ' +
'x="' + x + '" ' +
'y="' + y + '" ' +
// 'fill="' + fill + '" ' +
'stroke="' + stroke + '" ' +
'stroke-width="' + width + '" ' +
'width="1.5em" height="1.5em"' +
'/>';
break;
case 'rectangle':
html = '<use xlink:href="#rectangle" class="legendIcon" ' +
'x="' + x + '" ' +
'y="' + y + '" ' +
'fill="' + fill + '" ' +
'stroke="' + stroke + '" ' +
'stroke-width="' + width + '" ' +
'width="1.5em" height="1.5em"' +
'/>';
break;
case 'plus':
html = '<use xlink:href="#plus" class="legendIcon" ' +
'x="' + x + '" ' +
'y="' + y + '" ' +
// 'fill="' + fill + '" ' +
'stroke="' + stroke + '" ' +
'stroke-width="' + width + '" ' +
'width="1.5em" height="1.5em"' +
'/>';
break;
case 'bar':
html = '<use xlink:href="#bars" class="legendIcon" ' +
'x="' + x + '" ' +
'y="' + y + '" ' +
'fill="' + fill + '" ' +
// 'stroke="' + stroke + '" ' +
// 'stroke-width="' + width + '" ' +
'width="1.5em" height="1.5em"' +
'/>';
break;
case 'area':
html = '<use xlink:href="#area" class="legendIcon" ' +
'x="' + x + '" ' +
'y="' + y + '" ' +
'fill="' + fill + '" ' +
// 'stroke="' + stroke + '" ' +
// 'stroke-width="' + width + '" ' +
'width="1.5em" height="1.5em"' +
'/>';
break;
case 'line':
html = '<use xlink:href="#line" class="legendIcon" ' +
'x="' + x + '" ' +
'y="' + y + '" ' +
// 'fill="' + fill + '" ' +
'stroke="' + stroke + '" ' +
'stroke-width="' + width + '" ' +
'width="1.5em" height="1.5em"' +
'/>';
break;
default:
// default is circle
html = '<use xlink:href="#circle" class="legendIcon" ' +
'x="' + x + '" ' +
'y="' + y + '" ' +
'fill="' + fill + '" ' +
'stroke="' + stroke + '" ' +
'stroke-width="' + width + '" ' +
'width="1.5em" height="1.5em"' +
'/>';
}
return html;
}
// Define svg symbols for shapes
var svgShapeDefs = '' +
'<defs>' +
'<symbol id="line" fill="none" viewBox="-5 -5 25 25">' +
'<polyline points="0,15 5,5 10,10 15,0"/>' +
'</symbol>' +
'<symbol id="area" stroke-width="1" viewBox="-5 -5 25 25">' +
'<polyline points="0,15 5,5 10,10 15,0, 15,15, 0,15"/>' +
'</symbol>' +
'<symbol id="bars" stroke-width="1" viewBox="-5 -5 25 25">' +
'<polyline points="1.5,15.5 1.5,12.5, 4.5,12.5 4.5,15.5 6.5,15.5 6.5,3.5, 9.5,3.5 9.5,15.5 11.5,15.5 11.5,7.5 14.5,7.5 14.5,15.5 1.5,15.5"/>' +
'</symbol>' +
'<symbol id="circle" viewBox="-5 -5 25 25">' +
'<circle cx="0" cy="15" r="2.5"/>' +
'<circle cx="5" cy="5" r="2.5"/>' +
'<circle cx="10" cy="10" r="2.5"/>' +
'<circle cx="15" cy="0" r="2.5"/>' +
'</symbol>' +
'<symbol id="rectangle" viewBox="-5 -5 25 25">' +
'<rect x="-2.1" y="12.9" width="4.2" height="4.2"/>' +
'<rect x="2.9" y="2.9" width="4.2" height="4.2"/>' +
'<rect x="7.9" y="7.9" width="4.2" height="4.2"/>' +
'<rect x="12.9" y="-2.1" width="4.2" height="4.2"/>' +
'</symbol>' +
'<symbol id="diamond" viewBox="-5 -5 25 25">' +
'<path d="M-3,15 L0,12 L3,15, L0,18 Z"/>' +
'<path d="M2,5 L5,2 L8,5, L5,8 Z"/>' +
'<path d="M7,10 L10,7 L13,10, L10,13 Z"/>' +
'<path d="M12,0 L15,-3 L18,0, L15,3 Z"/>' +
'</symbol>' +
'<symbol id="cross" fill="none" viewBox="-5 -5 25 25">' +
'<path d="M-2.1,12.9 L2.1,17.1, M2.1,12.9 L-2.1,17.1 Z"/>' +
'<path d="M2.9,2.9 L7.1,7.1 M7.1,2.9 L2.9,7.1 Z"/>' +
'<path d="M7.9,7.9 L12.1,12.1 M12.1,7.9 L7.9,12.1 Z"/>' +
'<path d="M12.9,-2.1 L17.1,2.1 M17.1,-2.1 L12.9,2.1 Z"/>' +
'</symbol>' +
'<symbol id="plus" fill="none" viewBox="-5 -5 25 25">' +
'<path d="M0,12 L0,18, M-3,15 L3,15 Z"/>' +
'<path d="M5,2 L5,8 M2,5 L8,5 Z"/>' +
'<path d="M10,7 L10,13 M7,10 L13,10 Z"/>' +
'<path d="M15,-3 L15,3 M12,0 L18,0 Z"/>' +
'</symbol>' +
'</defs>';
// Generate a list of legend entries in their final order
function getLegendEntries(series, labelFormatter, sorted) {
var lf = labelFormatter,
legendEntries = series.reduce(function(validEntries, s, i) {
var labelEval = (lf ? lf(s.label, s) : s.label)
if (s.hasOwnProperty("label") ? labelEval : true) {
var entry = {
label: labelEval || 'Plot ' + (i + 1),
color: s.color,
options: {
lines: s.lines,
points: s.points,
bars: s.bars
}
}
validEntries.push(entry)
}
return validEntries;
}, []);
// Sort the legend using either the default or a custom comparator
if (sorted) {
if ($.isFunction(sorted)) {
legendEntries.sort(sorted);
} else if (sorted === 'reverse') {
legendEntries.reverse();
} else {
var ascending = (sorted !== 'descending');
legendEntries.sort(function(a, b) {
return a.label === b.label
? 0
: ((a.label < b.label) !== ascending ? 1 : -1 // Logical XOR
);
});
}
}
return legendEntries;
}
// return false if opts1 same as opts2
function checkOptions(opts1, opts2) {
for (var prop in opts1) {
if (opts1.hasOwnProperty(prop)) {
if (opts1[prop] !== opts2[prop]) {
return true;
}
}
}
return false;
}
// Compare two lists of legend entries
function shouldRedraw(oldEntries, newEntries) {
if (!oldEntries || !newEntries) {
return true;
}
if (oldEntries.length !== newEntries.length) {
return true;
}
var i, newEntry, oldEntry, newOpts, oldOpts;
for (i = 0; i < newEntries.length; i++) {
newEntry = newEntries[i];
oldEntry = oldEntries[i];
if (newEntry.label !== oldEntry.label) {
return true;
}
if (newEntry.color !== oldEntry.color) {
return true;
}
// check for changes in lines options
newOpts = newEntry.options.lines;
oldOpts = oldEntry.options.lines;
if (checkOptions(newOpts, oldOpts)) {
return true;
}
// check for changes in points options
newOpts = newEntry.options.points;
oldOpts = oldEntry.options.points;
if (checkOptions(newOpts, oldOpts)) {
return true;
}
// check for changes in bars options
newOpts = newEntry.options.bars;
oldOpts = oldEntry.options.bars;
if (checkOptions(newOpts, oldOpts)) {
return true;
}
}
return false;
}
function init(plot) {
plot.hooks.setupGrid.push(function (plot) {
series = plot.getData()
var options = plot.getOptions();
var labelFormatter = options.legend.labelFormatter,
oldEntries = options.legend.legendEntries,
oldPlotOffset = options.legend.plotOffset,
newEntries = getLegendEntries(series, labelFormatter, options.legend.sorted),
newPlotOffset = plot.getPlotOffset();
if (shouldRedraw(oldEntries, newEntries) ||
checkOptions(oldPlotOffset, newPlotOffset)) {
insertLegend(plot, options, plot.getPlaceholder(), newEntries);
}
});
}
$.plot.plugins.push({
init: init,
options: defaultOptions,
name: 'legend',
version: '1.0'
});
})(jQuery);

View File

@ -0,0 +1,43 @@
(function ($) {
'use strict';
var saturated = {
saturate: function (a) {
if (a === Infinity) {
return Number.MAX_VALUE;
}
if (a === -Infinity) {
return -Number.MAX_VALUE;
}
return a;
},
delta: function(min, max, noTicks) {
return ((max - min) / noTicks) === Infinity ? (max / noTicks - min / noTicks) : (max - min) / noTicks
},
multiply: function (a, b) {
return saturated.saturate(a * b);
},
// returns c * bInt * a. Beahves properly in the case where c is negative
// and bInt * a is bigger that Number.MAX_VALUE (Infinity)
multiplyAdd: function (a, bInt, c) {
if (isFinite(a * bInt)) {
return saturated.saturate(a * bInt + c);
} else {
var result = c;
for (var i = 0; i < bInt; i++) {
result += a;
}
return saturated.saturate(result);
}
},
// round to nearby lower multiple of base
floorInBase: function(n, base) {
return base * Math.ceil(n / base);
}
};
$.plot.saturated = saturated;
})(jQuery);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,10 @@
(function ($) {
'use strict';
$.plot.uiConstants = {
SNAPPING_CONSTANT: 20,
PANHINT_LENGTH_CONSTANT: 10,
MINOR_TICKS_COUNT_CONSTANT: 4,
TICK_LENGTH_CONSTANT: 10,
ZOOM_DISTANCE_MARGIN: 25
};
})(jQuery);

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -45,6 +45,169 @@
</form>
<% end %>
</div>
<% type = nil %>
<% if @is_answer_list %>
<% type = 'result_chart' if params[:type] == 'result_chart' %>
<div class="pull-right">
<a href="?type=" class="btn <%= 'active' if type != 'result_chart' %>"><%=t("survey.table")%></a>
<a href="?type=result_chart" class="btn <%= 'active' if type == 'result_chart' %>"><%=t("survey.result_chart")%></a>
</div>
<% end %>
<% if type == "result_chart" %>
<div style="clear: both;"></div>
<style type="text/css">
#tooltip{
position: absolute;
background: #ffffff;
z-index: 1;
border-radius: 10px;
padding: 2px;
display: none;
font-weight: bold;
}
.flot_wrapper{
position: relative;
}
</style>
<div class="flot_wrapper">
<span id="tooltip"></span>
<div class="legend-container"></div>
<div id="resultchart-container" class="flot-placeholder"></div>
</div>
<%= javascript_include_tag "survey/jquery.colorhelpers.js" %>
<%= javascript_include_tag "survey/jquery.canvaswrapper.js" %>
<%= javascript_include_tag "survey/jquery.flot_3.0.js" %>
<%= javascript_include_tag "survey/jquery.flot.time.min.js" %>
<%= javascript_include_tag "survey/jquery.flot.uiConstants.js" %>
<%= javascript_include_tag "survey/jquery.flot.saturated.js" %>
<%= javascript_include_tag "survey/jquery.flot.browser.js" %>
<%= javascript_include_tag "survey/jquery.flot.drawSeries.js" %>
<%= javascript_include_tag "survey/jquery.flot.axislabels.js" %>
<%= javascript_include_tag "survey/jquery.flot.hover.js" %>
<%= javascript_include_tag "survey/jquery.flot.animator.min.js" %>
<!-- <%= javascript_include_tag "survey/jquery.flot.legend.js" %> -->
<%= javascript_include_tag "survey/moment.min.js" %>
<style type="text/css">
.axisLabels {
font-weight: bold;
}
</style>
<script>
<% data = @survey_answers.order_by(:updated_at=>1).map{|sa| [sa.updated_at.to_i * 1000 , sa.scored_points ]} %>
var min_time = <%= data[0][0] rescue 0 %>;
var max_time = <%= data[-1][0] rescue 0 %>;
var min_y = <%= data.map{|a| a[1]}.min.to_i %>;
var max_y = <%= data.map{|a| a[1]}.max.to_i %>;
var xaxes = [{
mode: "time",
axisLabelUseCanvas: false,
timeformat: "%Y/%m/%d",
timezone: "browser",
font: {
size: '16',
color: 'black'
},
// min: min_time,
// max: max_time,
axisLabel: "<%=t("survey.taken_date")%>",
position: "bottom",
}];
var yaxes = [{
use_min_force: false, //modify at jquery_flot_3.0.js
axisLabelUseCanvas: false,
showTickLabels: 'all',
font: {
size: '16',
color: 'black'
},
// min: min_y,
// max: max_y,
axisLabel: "<%=t("survey.result_score")%>",
position: "left",
axisLabelFontSizePixels: 12,
axisLabelFontFamily: 'Verdana, Arial, Helvetica, Tahoma, sans-serif',
axisLabelPadding: 5
}];
var options = {
series: {
lines: {
show: true,
lineWidth: 2,
steps: false , //畫斜線
is_sort: true
},
points: {
radius: 3,
show: true
},
downsample: {
threshold: 0
}
},
grid: {
hoverable: true,
clickable: true,
show: true
},
xaxes: xaxes,
yaxes: yaxes,
zoom: {
interactive: true
},
pan: {
interactive: true,
enableTouch: true
},
colors: ["#708fff","#ffc107","#96478c","#1e7e34"],
polycolors: ["#3f66f4","#ffc107","#dc3545","#1e7e34"],
// legend: {
// show: true,
// noColumns: 1
// },
bar: {
zero: false
}
};
// options['legend']['container'] = $('.legend-container')[0];
var chart_data = [{"label": "<%=@user.member_name rescue "NA" %>", "data": <%= data.to_json.html_safe %>}];
$(document).ready(function(){
var chart_height = $(window).outerWidth(true)*0.2;
$("#resultchart-container").height(chart_height).plot(chart_data,options);
$(".flot-placeholder").bind("plothover", function (event, pos, item) {
if (!pos.x || !pos.y) {
return;
}
if (!item) {
$("#tooltip").hide();
}
});
$(".flot-placeholder").bind("plotclick", function (event, pos, item) {
if (!pos.x || !pos.y) {
return;
}
var x = item.datapoint[0].toFixed(2),
y = item.datapoint[1].toFixed(0);
var date = new moment(Math.round(x));
var tp = "(" + date.format("yyyy/MM/DD hh:mm") + ")"
$("#tooltip").html("<%=t("survey.type.0")%>: "+y+"<br>"+tp);
var left = 0;
var content_width = tp.length*8.2
if (content_width+item.pageX+5>$(window).width()){
left = $(window).width()-content_width;
}else{
left = item.pageX - content_width / 2;
}
var offset = $(this).parents(".flot_wrapper").eq(0).offset();
$("#tooltip").css({top: item.pageY+5-offset.top, left: left-offset.left})
.fadeIn(200);
});
})
$(window).resize(function(){
var chart_height = $(window).outerWidth(true)*0.2;
$("#resultchart-container").height(chart_height).plot(chart_data,options);
})
</script>
<% else %>
<table class="table main-list">
<thead>
<tr class="sort-header">
@ -68,6 +231,46 @@
<a class="btn btn-primary" href="/admin/surveys/<%= sa.id.to_s %>/answer_list"><%= t("survey.view") %>(<%=sa.survey_answer_ids.count%>)</a>
<button class="btn btn-primary export-xls" data-href="/admin/surveys/<%= sa.id.to_s %>/export_answers"><%= t("survey.export") %></button>
<% else %>
<% if @survey.result_type == QuestionnaireSurvey::ResultCriteria %>
<% tmp_msgs = []
answer_model_attrs = sa.attributes
weight_relations = @survey.survey_questions.map{|q| [q.id.to_s,(q.weight.nil? ? 1 : q.weight)]}.to_h
types = [] %>
<% @survey.result_criteria.each do |criteria| %>
<%
total_criteria_score = 0
total_weight = 0
((criteria["questions"][0].to_i - 1)..(criteria["questions"][1].to_i - 1)).each do |x|
total_criteria_score = (total_criteria_score + sa.individual_total[x].to_i) rescue 0
k = weight_relations.keys[x]
if k && answer_model_attrs.has_key?(k)
total_weight += weight_relations[k]
end
end
type = criteria["type"].to_i
%>
<% if type == 0 %>
<% if (criteria["range"][0].to_i..criteria["range"][1].to_i).cover?(total_criteria_score) %>
<% tmp_msgs << criteria["msg"] %>
<% types << type %>
<% end %>
<% else %>
<% if (criteria["range"][0].to_i..criteria["range"][1].to_i).cover?(total_criteria_score / total_weight) %>
<% tmp_msgs << criteria["msg"] %>
<% types << type %>
<% end %>
<% end %>
<% end %>
<% if types.include?(0) %>
<h4>Your total score is <%= sa.scored_points %> </h4>
<% end %>
<% if types.include?(1) %>
<h4>Your average score is <%= sa.get_avg_points %> </h4>
<% end %>
<% tmp_msgs.each do |msg| %>
<div><%=msg.html_safe%></div>
<% end %>
<% end %>
<a class="btn btn-primary" href="/admin/surveys/<%= @is_answer_list ? sa.id : sa.survey_answer_ids.last.to_s %>/answer_set"><%= t("survey.view_answers") %></a>
<% end %>
</td>
@ -128,4 +331,5 @@
return false;
})
</script>
</script>
<% end %>

View File

@ -29,6 +29,169 @@
<div>
<h4><%=t('survey.taken_by')%>: <%=@user.member_name%></h4>
</div>
<% if @is_answer_list %>
<% type = 'result_chart' if params[:type] == 'result_chart' %>
<div class="pull-right">
<a href="?method=my_record&type=" class="btn <%= 'active' if type != 'result_chart' %>"><%=t("survey.table")%></a>
<a href="?method=my_record&type=result_chart" class="btn <%= 'active' if type == 'result_chart' %>"><%=t("survey.result_chart")%></a>
</div>
<% end %>
<% if type == "result_chart" %>
<div style="clear: both;"></div>
<style type="text/css">
#tooltip{
position: absolute;
background: #ffffff;
z-index: 1;
border-radius: 10px;
padding: 2px;
display: none;
font-weight: bold;
}
</style>
<div class="flot_wrapper">
<span id="tooltip"></span>
<div class="legend-container"></div>
<div id="resultchart-container" class="flot-placeholder"></div>
</div>
<%= javascript_include_tag "survey/jquery.colorhelpers.js" %>
<%= javascript_include_tag "survey/jquery.canvaswrapper.js" %>
<%= javascript_include_tag "survey/jquery.flot_3.0.js" %>
<%= javascript_include_tag "survey/jquery.flot.time.min.js" %>
<%= javascript_include_tag "survey/jquery.flot.uiConstants.js" %>
<%= javascript_include_tag "survey/jquery.flot.saturated.js" %>
<%= javascript_include_tag "survey/jquery.flot.browser.js" %>
<%= javascript_include_tag "survey/jquery.flot.drawSeries.js" %>
<%= javascript_include_tag "survey/jquery.flot.axislabels.js" %>
<%= javascript_include_tag "survey/jquery.flot.hover.js" %>
<%= javascript_include_tag "survey/jquery.flot.animator.min.js" %>
<!-- <%= javascript_include_tag "survey/jquery.flot.legend.js" %> -->
<%= javascript_include_tag "survey/moment.min.js" %>
<style type="text/css">
.axisLabels {
font-weight: bold;
}
.flot_wrapper{
position: relative;
}
</style>
<script>
<% data = @survey_answers.order_by(:updated_at=>1).map{|sa| [sa.updated_at.to_i * 1000 , sa.scored_points ]} %>
var min_time = <%= data[0][0] rescue 0 %>;
var max_time = <%= data[-1][0] rescue 0 %>;
var min_y = <%= data.map{|a| a[1]}.min.to_i %>;
var max_y = <%= data.map{|a| a[1]}.max.to_i %>;
var xaxes = [{
mode: "time",
axisLabelUseCanvas: false,
timeformat: "%Y/%m/%d",
timezone: "browser",
font: {
size: '16',
color: 'black'
},
// min: min_time,
// max: max_time,
axisLabel: "<%=t("survey.taken_date")%>",
position: "bottom",
}];
var yaxes = [{
use_min_force: false, //modify at jquery_flot_3.0.js
axisLabelUseCanvas: false,
showTickLabels: 'all',
font: {
size: '16',
color: 'black'
},
// min: min_y,
// max: max_y,
axisLabel: "<%=t("survey.result_score")%>",
position: "left",
axisLabelFontSizePixels: 12,
axisLabelFontFamily: 'Verdana, Arial, Helvetica, Tahoma, sans-serif',
axisLabelPadding: 5
}];
var options = {
series: {
lines: {
show: true,
lineWidth: 2,
steps: false , //畫斜線
is_sort: true
},
points: {
radius: 3,
show: false
},
downsample: {
threshold: 0
}
},
grid: {
hoverable: true,
clickable: true,
show: true
},
xaxes: xaxes,
yaxes: yaxes,
zoom: {
interactive: true
},
pan: {
interactive: true,
enableTouch: true
},
colors: ["#708fff","#ffc107","#96478c","#1e7e34"],
polycolors: ["#3f66f4","#ffc107","#dc3545","#1e7e34"],
// legend: {
// show: true,
// noColumns: 1
// },
bar: {
zero: false
}
};
// options['legend']['container'] = $('.legend-container')[0];
var chart_data = [{"label": "<%=@user.member_name rescue "NA" %>", "data": <%= data.to_json.html_safe %>}];
$(document).ready(function(){
var chart_height = $(window).outerWidth(true)*0.2;
$("#resultchart-container").height(chart_height).plot(chart_data,options);
$(".flot-placeholder").bind("plothover", function (event, pos, item) {
if (!pos.x || !pos.y) {
return;
}
if (!item) {
$("#tooltip").hide();
}
});
$(".flot-placeholder").bind("plotclick", function (event, pos, item) {
if (!pos.x || !pos.y) {
return;
}
var x = item.datapoint[0].toFixed(2),
y = item.datapoint[1].toFixed(0);
var date = new moment(Math.round(x));
var tp = "(" + date.format("yyyy/MM/DD hh:mm") + ")"
$("#tooltip").html("<%=t("survey.type.0")%>: "+y+"<br>"+tp);
var left = 0;
var content_width = tp.length*8.2
if (content_width+item.pageX+5>$(window).width()){
left = $(window).width()-content_width;
}else{
left = item.pageX - content_width / 2;
}
var offset = $(this).parents(".flot_wrapper").eq(0).offset();
$("#tooltip").css({top: item.pageY+5-offset.top, left: left-offset.left})
.fadeIn(200);
});
})
$(window).resize(function(){
var chart_height = $(window).outerWidth(true)*0.2;
$("#resultchart-container").height(chart_height).plot(chart_data,options);
})
</script>
<% else %>
<table class="table main-list">
<thead>
<tr class="sort-header">
@ -101,4 +264,5 @@
</tbody>
</table>
<%= create_pagination((@survey_answer_groups || @survey_answers).total_pages).html_safe %>
<% end %>
<% end %>

View File

@ -2,6 +2,9 @@ en:
module_name:
survey: Survey
survey:
result_score: Result score
table: Table
result_chart: Result chart
type:
'0': "Total"
'1': "Average"
@ -34,6 +37,7 @@ en:
add_section: Add Section
taken_by: Taken By
taken_date: Taken On
latest_taken_date: Latest taken On
records: Records
view_answers: View Answers

View File

@ -4,6 +4,9 @@ zh_tw:
survey: 問卷調查
survey:
result_score: 結果分數
table: 列表
result_chart: 結果圖表
type:
'0': "總分"
'1': "平均"
@ -35,7 +38,8 @@ zh_tw:
add: 新增題目
add_section: 增加題組
taken_by: 填寫人
taken_date: 上次填寫時間
taken_date: 填寫時間
latest_taken_date: 上次填寫時間
records: 填寫紀錄
view_answers: View Answers
set_sections: 設定題組