mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2024-11-11 18:03:30 +08:00
928 lines
26 KiB
JavaScript
928 lines
26 KiB
JavaScript
/*!
|
|
* chartjs-plugin-annotation.js
|
|
* http://chartjs.org/
|
|
* Version: 0.5.7
|
|
*
|
|
* Copyright 2016 Evert Timberg
|
|
* Released under the MIT license
|
|
* https://github.com/chartjs/Chart.Annotation.js/blob/master/LICENSE.md
|
|
*/
|
|
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
|
|
|
},{}],2:[function(require,module,exports){
|
|
module.exports = function(Chart) {
|
|
var chartHelpers = Chart.helpers;
|
|
|
|
var helpers = require('./helpers.js')(Chart);
|
|
var events = require('./events.js')(Chart);
|
|
|
|
var annotationTypes = Chart.Annotation.types;
|
|
|
|
function setAfterDataLimitsHook(axisOptions) {
|
|
helpers.decorate(axisOptions, 'afterDataLimits', function(previous, scale) {
|
|
if (previous) previous(scale);
|
|
helpers.adjustScaleRange(scale);
|
|
});
|
|
}
|
|
|
|
function draw(drawTime) {
|
|
return function(chartInstance, easingDecimal) {
|
|
var defaultDrawTime = chartInstance.annotation.options.drawTime;
|
|
|
|
helpers.elements(chartInstance)
|
|
.filter(function(element) {
|
|
return drawTime === (element.options.drawTime || defaultDrawTime);
|
|
})
|
|
.forEach(function(element) {
|
|
element.transition(easingDecimal).draw();
|
|
});
|
|
};
|
|
}
|
|
|
|
return {
|
|
beforeInit: function(chartInstance) {
|
|
var chartOptions = chartInstance.options;
|
|
|
|
// Initialize chart instance plugin namespace
|
|
var ns = chartInstance.annotation = {
|
|
elements: {},
|
|
options: helpers.initConfig(chartOptions.annotation || {}),
|
|
onDestroy: [],
|
|
firstRun: true,
|
|
supported: false
|
|
};
|
|
|
|
// Add the annotation scale adjuster to each scale's afterDataLimits hook
|
|
chartInstance.ensureScalesHaveIDs();
|
|
if (chartOptions.scales) {
|
|
ns.supported = true;
|
|
chartHelpers.each(chartOptions.scales.xAxes, setAfterDataLimitsHook);
|
|
chartHelpers.each(chartOptions.scales.yAxes, setAfterDataLimitsHook);
|
|
}
|
|
},
|
|
beforeUpdate: function(chartInstance) {
|
|
var ns = chartInstance.annotation;
|
|
|
|
if (!ns.supported) {
|
|
return;
|
|
}
|
|
|
|
if (!ns.firstRun) {
|
|
ns.options = helpers.initConfig(chartInstance.options.annotation || {});
|
|
} else {
|
|
ns.firstRun = false;
|
|
}
|
|
|
|
var elementIds = [];
|
|
|
|
// Add new elements, or update existing ones
|
|
ns.options.annotations.forEach(function(annotation) {
|
|
var id = annotation.id || helpers.objectId();
|
|
|
|
// No element with that ID exists, and it's a valid annotation type
|
|
if (!ns.elements[id] && annotationTypes[annotation.type]) {
|
|
var cls = annotationTypes[annotation.type];
|
|
var element = new cls({
|
|
id: id,
|
|
options: annotation,
|
|
chartInstance: chartInstance,
|
|
});
|
|
element.initialize();
|
|
ns.elements[id] = element;
|
|
annotation.id = id;
|
|
elementIds.push(id);
|
|
} else if (ns.elements[id]) {
|
|
// Nothing to do for update, since the element config references
|
|
// the same object that exists in the chart annotation config
|
|
elementIds.push(id);
|
|
}
|
|
});
|
|
|
|
// Delete removed elements
|
|
Object.keys(ns.elements).forEach(function(id) {
|
|
if (elementIds.indexOf(id) === -1) {
|
|
ns.elements[id].destroy();
|
|
delete ns.elements[id];
|
|
}
|
|
});
|
|
},
|
|
afterScaleUpdate: function(chartInstance) {
|
|
helpers.elements(chartInstance).forEach(function(element) {
|
|
element.configure();
|
|
});
|
|
},
|
|
beforeDatasetsDraw: draw('beforeDatasetsDraw'),
|
|
afterDatasetsDraw: draw('afterDatasetsDraw'),
|
|
afterDraw: draw('afterDraw'),
|
|
afterInit: function(chartInstance) {
|
|
// Detect and intercept events that happen on an annotation element
|
|
var watchFor = chartInstance.annotation.options.events;
|
|
if (chartHelpers.isArray(watchFor) && watchFor.length > 0) {
|
|
var canvas = chartInstance.chart.canvas;
|
|
var eventHandler = events.dispatcher.bind(chartInstance);
|
|
events.collapseHoverEvents(watchFor).forEach(function(eventName) {
|
|
chartHelpers.addEvent(canvas, eventName, eventHandler);
|
|
chartInstance.annotation.onDestroy.push(function() {
|
|
chartHelpers.removeEvent(canvas, eventName, eventHandler);
|
|
});
|
|
});
|
|
}
|
|
},
|
|
destroy: function(chartInstance) {
|
|
var deregisterers = chartInstance.annotation.onDestroy;
|
|
while (deregisterers.length > 0) {
|
|
deregisterers.pop()();
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
},{"./events.js":4,"./helpers.js":5}],3:[function(require,module,exports){
|
|
module.exports = function(Chart) {
|
|
var chartHelpers = Chart.helpers;
|
|
|
|
var AnnotationElement = Chart.Element.extend({
|
|
initialize: function() {
|
|
this.hidden = false;
|
|
this.hovering = false;
|
|
this._model = chartHelpers.clone(this._model) || {};
|
|
this.setDataLimits();
|
|
},
|
|
destroy: function() {},
|
|
setDataLimits: function() {},
|
|
configure: function() {},
|
|
inRange: function() {},
|
|
getCenterPoint: function() {},
|
|
getWidth: function() {},
|
|
getHeight: function() {},
|
|
getArea: function() {},
|
|
draw: function() {}
|
|
});
|
|
|
|
return AnnotationElement;
|
|
};
|
|
|
|
},{}],4:[function(require,module,exports){
|
|
module.exports = function(Chart) {
|
|
var chartHelpers = Chart.helpers;
|
|
var helpers = require('./helpers.js')(Chart);
|
|
|
|
function collapseHoverEvents(events) {
|
|
var hover = false;
|
|
var filteredEvents = events.filter(function(eventName) {
|
|
switch (eventName) {
|
|
case 'mouseenter':
|
|
case 'mouseover':
|
|
case 'mouseout':
|
|
case 'mouseleave':
|
|
hover = true;
|
|
return false;
|
|
|
|
default:
|
|
return true;
|
|
}
|
|
});
|
|
if (hover && filteredEvents.indexOf('mousemove') === -1) {
|
|
filteredEvents.push('mousemove');
|
|
}
|
|
return filteredEvents;
|
|
}
|
|
|
|
function dispatcher(e) {
|
|
var ns = this.annotation;
|
|
var elements = helpers.elements(this);
|
|
var position = chartHelpers.getRelativePosition(e, this.chart);
|
|
var element = helpers.getNearestItems(elements, position);
|
|
var events = collapseHoverEvents(ns.options.events);
|
|
var dblClickSpeed = ns.options.dblClickSpeed;
|
|
var eventHandlers = [];
|
|
var eventHandlerName = helpers.getEventHandlerName(e.type);
|
|
var options = (element || {}).options;
|
|
|
|
// Detect hover events
|
|
if (e.type === 'mousemove') {
|
|
if (element && !element.hovering) {
|
|
// hover started
|
|
['mouseenter', 'mouseover'].forEach(function(eventName) {
|
|
var eventHandlerName = helpers.getEventHandlerName(eventName);
|
|
var hoverEvent = helpers.createMouseEvent(eventName, e); // recreate the event to match the handler
|
|
element.hovering = true;
|
|
if (typeof options[eventHandlerName] === 'function') {
|
|
eventHandlers.push([ options[eventHandlerName], hoverEvent, element ]);
|
|
}
|
|
});
|
|
} else if (!element) {
|
|
// hover ended
|
|
elements.forEach(function(element) {
|
|
if (element.hovering) {
|
|
element.hovering = false;
|
|
var options = element.options;
|
|
['mouseout', 'mouseleave'].forEach(function(eventName) {
|
|
var eventHandlerName = helpers.getEventHandlerName(eventName);
|
|
var hoverEvent = helpers.createMouseEvent(eventName, e); // recreate the event to match the handler
|
|
if (typeof options[eventHandlerName] === 'function') {
|
|
eventHandlers.push([ options[eventHandlerName], hoverEvent, element ]);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Suppress duplicate click events during a double click
|
|
// 1. click -> 2. click -> 3. dblclick
|
|
//
|
|
// 1: wait dblClickSpeed ms, then fire click
|
|
// 2: cancel (1) if it is waiting then wait dblClickSpeed ms then fire click, else fire click immediately
|
|
// 3: cancel (1) or (2) if waiting, then fire dblclick
|
|
if (element && events.indexOf('dblclick') > -1 && typeof options.onDblclick === 'function') {
|
|
if (e.type === 'click' && typeof options.onClick === 'function') {
|
|
clearTimeout(element.clickTimeout);
|
|
element.clickTimeout = setTimeout(function() {
|
|
delete element.clickTimeout;
|
|
options.onClick.call(element, e);
|
|
}, dblClickSpeed);
|
|
e.stopImmediatePropagation();
|
|
e.preventDefault();
|
|
return;
|
|
} else if (e.type === 'dblclick' && element.clickTimeout) {
|
|
clearTimeout(element.clickTimeout);
|
|
delete element.clickTimeout;
|
|
}
|
|
}
|
|
|
|
// Dispatch the event to the usual handler, but only if we haven't substituted it
|
|
if (element && typeof options[eventHandlerName] === 'function' && eventHandlers.length === 0) {
|
|
eventHandlers.push([ options[eventHandlerName], e, element ]);
|
|
}
|
|
|
|
if (eventHandlers.length > 0) {
|
|
e.stopImmediatePropagation();
|
|
e.preventDefault();
|
|
eventHandlers.forEach(function(eventHandler) {
|
|
// [handler, event, element]
|
|
eventHandler[0].call(eventHandler[2], eventHandler[1]);
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
dispatcher: dispatcher,
|
|
collapseHoverEvents: collapseHoverEvents
|
|
};
|
|
};
|
|
|
|
},{"./helpers.js":5}],5:[function(require,module,exports){
|
|
function noop() {}
|
|
|
|
function elements(chartInstance) {
|
|
// Turn the elements object into an array of elements
|
|
var elements = chartInstance.annotation.elements;
|
|
return Object.keys(elements).map(function(id) {
|
|
return elements[id];
|
|
});
|
|
}
|
|
|
|
function objectId() {
|
|
return Math.random().toString(36).substr(2, 6);
|
|
}
|
|
|
|
function isValid(rawValue) {
|
|
if (rawValue === null || typeof rawValue === 'undefined') {
|
|
return false;
|
|
} else if (typeof rawValue === 'number') {
|
|
return isFinite(rawValue);
|
|
} else {
|
|
return !!rawValue;
|
|
}
|
|
}
|
|
|
|
function decorate(obj, prop, func) {
|
|
var prefix = '$';
|
|
if (!obj[prefix + prop]) {
|
|
if (obj[prop]) {
|
|
obj[prefix + prop] = obj[prop].bind(obj);
|
|
obj[prop] = function() {
|
|
var args = [ obj[prefix + prop] ].concat(Array.prototype.slice.call(arguments));
|
|
return func.apply(obj, args);
|
|
};
|
|
} else {
|
|
obj[prop] = function() {
|
|
var args = [ undefined ].concat(Array.prototype.slice.call(arguments));
|
|
return func.apply(obj, args);
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
function callEach(fns, method) {
|
|
fns.forEach(function(fn) {
|
|
(method ? fn[method] : fn)();
|
|
});
|
|
}
|
|
|
|
function getEventHandlerName(eventName) {
|
|
return 'on' + eventName[0].toUpperCase() + eventName.substring(1);
|
|
}
|
|
|
|
function createMouseEvent(type, previousEvent) {
|
|
try {
|
|
return new MouseEvent(type, previousEvent);
|
|
} catch (exception) {
|
|
try {
|
|
var m = document.createEvent('MouseEvent');
|
|
m.initMouseEvent(
|
|
type,
|
|
previousEvent.canBubble,
|
|
previousEvent.cancelable,
|
|
previousEvent.view,
|
|
previousEvent.detail,
|
|
previousEvent.screenX,
|
|
previousEvent.screenY,
|
|
previousEvent.clientX,
|
|
previousEvent.clientY,
|
|
previousEvent.ctrlKey,
|
|
previousEvent.altKey,
|
|
previousEvent.shiftKey,
|
|
previousEvent.metaKey,
|
|
previousEvent.button,
|
|
previousEvent.relatedTarget
|
|
);
|
|
return m;
|
|
} catch (exception2) {
|
|
var e = document.createEvent('Event');
|
|
e.initEvent(
|
|
type,
|
|
previousEvent.canBubble,
|
|
previousEvent.cancelable
|
|
);
|
|
return e;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = function(Chart) {
|
|
var chartHelpers = Chart.helpers;
|
|
|
|
function initConfig(config) {
|
|
config = chartHelpers.configMerge(Chart.Annotation.defaults, config);
|
|
if (chartHelpers.isArray(config.annotations)) {
|
|
config.annotations.forEach(function(annotation) {
|
|
annotation.label = chartHelpers.configMerge(Chart.Annotation.labelDefaults, annotation.label);
|
|
});
|
|
}
|
|
return config;
|
|
}
|
|
|
|
function getScaleLimits(scaleId, annotations, scaleMin, scaleMax) {
|
|
var ranges = annotations.filter(function(annotation) {
|
|
return !!annotation._model.ranges[scaleId];
|
|
}).map(function(annotation) {
|
|
return annotation._model.ranges[scaleId];
|
|
});
|
|
|
|
var min = ranges.map(function(range) {
|
|
return Number(range.min);
|
|
}).reduce(function(a, b) {
|
|
return isFinite(b) && !isNaN(b) && b < a ? b : a;
|
|
}, scaleMin);
|
|
|
|
var max = ranges.map(function(range) {
|
|
return Number(range.max);
|
|
}).reduce(function(a, b) {
|
|
return isFinite(b) && !isNaN(b) && b > a ? b : a;
|
|
}, scaleMax);
|
|
|
|
return {
|
|
min: min,
|
|
max: max
|
|
};
|
|
}
|
|
|
|
function adjustScaleRange(scale) {
|
|
// Adjust the scale range to include annotation values
|
|
var range = getScaleLimits(scale.id, elements(scale.chart), scale.min, scale.max);
|
|
if (typeof scale.options.ticks.min === 'undefined' && typeof scale.options.ticks.suggestedMin === 'undefined') {
|
|
scale.min = range.min;
|
|
}
|
|
if (typeof scale.options.ticks.max === 'undefined' && typeof scale.options.ticks.suggestedMax === 'undefined') {
|
|
scale.max = range.max;
|
|
}
|
|
if (scale.handleTickRangeOptions) {
|
|
scale.handleTickRangeOptions();
|
|
}
|
|
}
|
|
|
|
function getNearestItems(annotations, position) {
|
|
var minDistance = Number.POSITIVE_INFINITY;
|
|
|
|
return annotations
|
|
.filter(function(element) {
|
|
return element.inRange(position.x, position.y);
|
|
})
|
|
.reduce(function(nearestItems, element) {
|
|
var center = element.getCenterPoint();
|
|
var distance = chartHelpers.distanceBetweenPoints(position, center);
|
|
|
|
if (distance < minDistance) {
|
|
nearestItems = [element];
|
|
minDistance = distance;
|
|
} else if (distance === minDistance) {
|
|
// Can have multiple items at the same distance in which case we sort by size
|
|
nearestItems.push(element);
|
|
}
|
|
|
|
return nearestItems;
|
|
}, [])
|
|
.sort(function(a, b) {
|
|
// If there are multiple elements equally close,
|
|
// sort them by size, then by index
|
|
var sizeA = a.getArea(), sizeB = b.getArea();
|
|
return (sizeA > sizeB || sizeA < sizeB) ? sizeA - sizeB : a._index - b._index;
|
|
})
|
|
.slice(0, 1)[0]; // return only the top item
|
|
}
|
|
|
|
return {
|
|
initConfig: initConfig,
|
|
elements: elements,
|
|
callEach: callEach,
|
|
noop: noop,
|
|
objectId: objectId,
|
|
isValid: isValid,
|
|
decorate: decorate,
|
|
adjustScaleRange: adjustScaleRange,
|
|
getNearestItems: getNearestItems,
|
|
getEventHandlerName: getEventHandlerName,
|
|
createMouseEvent: createMouseEvent
|
|
};
|
|
};
|
|
|
|
|
|
},{}],6:[function(require,module,exports){
|
|
// Get the chart variable
|
|
var Chart = require('chart.js');
|
|
Chart = typeof(Chart) === 'function' ? Chart : window.Chart;
|
|
|
|
// Configure plugin namespace
|
|
Chart.Annotation = Chart.Annotation || {};
|
|
|
|
Chart.Annotation.drawTimeOptions = {
|
|
afterDraw: 'afterDraw',
|
|
afterDatasetsDraw: 'afterDatasetsDraw',
|
|
beforeDatasetsDraw: 'beforeDatasetsDraw'
|
|
};
|
|
|
|
Chart.Annotation.defaults = {
|
|
drawTime: 'afterDatasetsDraw',
|
|
dblClickSpeed: 350, // ms
|
|
events: [],
|
|
annotations: []
|
|
};
|
|
|
|
Chart.Annotation.labelDefaults = {
|
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
fontFamily: Chart.defaults.global.defaultFontFamily,
|
|
fontSize: Chart.defaults.global.defaultFontSize,
|
|
fontStyle: 'bold',
|
|
fontColor: '#fff',
|
|
xPadding: 6,
|
|
yPadding: 6,
|
|
cornerRadius: 6,
|
|
position: 'center',
|
|
xAdjust: 0,
|
|
yAdjust: 0,
|
|
enabled: false,
|
|
content: null
|
|
};
|
|
|
|
Chart.Annotation.Element = require('./element.js')(Chart);
|
|
|
|
Chart.Annotation.types = {
|
|
line: require('./types/line.js')(Chart),
|
|
box: require('./types/box.js')(Chart)
|
|
};
|
|
|
|
var annotationPlugin = require('./annotation.js')(Chart);
|
|
|
|
module.exports = annotationPlugin;
|
|
Chart.pluginService.register(annotationPlugin);
|
|
|
|
},{"./annotation.js":2,"./element.js":3,"./types/box.js":7,"./types/line.js":8,"chart.js":1}],7:[function(require,module,exports){
|
|
// Box Annotation implementation
|
|
module.exports = function(Chart) {
|
|
var helpers = require('../helpers.js')(Chart);
|
|
|
|
var BoxAnnotation = Chart.Annotation.Element.extend({
|
|
setDataLimits: function() {
|
|
var model = this._model;
|
|
var options = this.options;
|
|
var chartInstance = this.chartInstance;
|
|
|
|
var xScale = chartInstance.scales[options.xScaleID];
|
|
var yScale = chartInstance.scales[options.yScaleID];
|
|
var chartArea = chartInstance.chartArea;
|
|
|
|
// Set the data range for this annotation
|
|
model.ranges = {};
|
|
|
|
if (!chartArea) {
|
|
return;
|
|
}
|
|
|
|
var min = 0;
|
|
var max = 0;
|
|
|
|
if (xScale) {
|
|
min = helpers.isValid(options.xMin) ? options.xMin : xScale.getPixelForValue(chartArea.left);
|
|
max = helpers.isValid(options.xMax) ? options.xMax : xScale.getPixelForValue(chartArea.right);
|
|
|
|
model.ranges[options.xScaleID] = {
|
|
min: Math.min(min, max),
|
|
max: Math.max(min, max)
|
|
};
|
|
}
|
|
|
|
if (yScale) {
|
|
min = helpers.isValid(options.yMin) ? options.yMin : yScale.getPixelForValue(chartArea.bottom);
|
|
max = helpers.isValid(options.yMax) ? options.yMax : yScale.getPixelForValue(chartArea.top);
|
|
|
|
model.ranges[options.yScaleID] = {
|
|
min: Math.min(min, max),
|
|
max: Math.max(min, max)
|
|
};
|
|
}
|
|
},
|
|
configure: function() {
|
|
var model = this._model;
|
|
var options = this.options;
|
|
var chartInstance = this.chartInstance;
|
|
|
|
var xScale = chartInstance.scales[options.xScaleID];
|
|
var yScale = chartInstance.scales[options.yScaleID];
|
|
var chartArea = chartInstance.chartArea;
|
|
|
|
// clip annotations to the chart area
|
|
model.clip = {
|
|
x1: chartArea.left,
|
|
x2: chartArea.right,
|
|
y1: chartArea.top,
|
|
y2: chartArea.bottom
|
|
};
|
|
|
|
var left = chartArea.left,
|
|
top = chartArea.top,
|
|
right = chartArea.right,
|
|
bottom = chartArea.bottom;
|
|
|
|
var min, max;
|
|
|
|
if (xScale) {
|
|
min = helpers.isValid(options.xMin) ? xScale.getPixelForValue(options.xMin) : chartArea.left;
|
|
max = helpers.isValid(options.xMax) ? xScale.getPixelForValue(options.xMax) : chartArea.right;
|
|
left = Math.min(min, max);
|
|
right = Math.max(min, max);
|
|
}
|
|
|
|
if (yScale) {
|
|
min = helpers.isValid(options.yMin) ? yScale.getPixelForValue(options.yMin) : chartArea.bottom;
|
|
max = helpers.isValid(options.yMax) ? yScale.getPixelForValue(options.yMax) : chartArea.top;
|
|
top = Math.min(min, max);
|
|
bottom = Math.max(min, max);
|
|
}
|
|
|
|
// Ensure model has rect coordinates
|
|
model.left = left;
|
|
model.top = top;
|
|
model.right = right;
|
|
model.bottom = bottom;
|
|
|
|
// Stylistic options
|
|
model.borderColor = options.borderColor;
|
|
model.borderWidth = options.borderWidth;
|
|
model.backgroundColor = options.backgroundColor;
|
|
},
|
|
inRange: function(mouseX, mouseY) {
|
|
var model = this._model;
|
|
return model &&
|
|
mouseX >= model.left &&
|
|
mouseX <= model.right &&
|
|
mouseY >= model.top &&
|
|
mouseY <= model.bottom;
|
|
},
|
|
getCenterPoint: function() {
|
|
var model = this._model;
|
|
return {
|
|
x: (model.right + model.left) / 2,
|
|
y: (model.bottom + model.top) / 2
|
|
};
|
|
},
|
|
getWidth: function() {
|
|
var model = this._model;
|
|
return Math.abs(model.right - model.left);
|
|
},
|
|
getHeight: function() {
|
|
var model = this._model;
|
|
return Math.abs(model.bottom - model.top);
|
|
},
|
|
getArea: function() {
|
|
return this.getWidth() * this.getHeight();
|
|
},
|
|
draw: function() {
|
|
var view = this._view;
|
|
var ctx = this.chartInstance.chart.ctx;
|
|
|
|
ctx.save();
|
|
|
|
// Canvas setup
|
|
ctx.beginPath();
|
|
ctx.rect(view.clip.x1, view.clip.y1, view.clip.x2 - view.clip.x1, view.clip.y2 - view.clip.y1);
|
|
ctx.clip();
|
|
|
|
ctx.lineWidth = view.borderWidth;
|
|
ctx.strokeStyle = view.borderColor;
|
|
ctx.fillStyle = view.backgroundColor;
|
|
|
|
// Draw
|
|
var width = view.right - view.left,
|
|
height = view.bottom - view.top;
|
|
ctx.fillRect(view.left, view.top, width, height);
|
|
ctx.strokeRect(view.left, view.top, width, height);
|
|
|
|
ctx.restore();
|
|
}
|
|
});
|
|
|
|
return BoxAnnotation;
|
|
};
|
|
|
|
},{"../helpers.js":5}],8:[function(require,module,exports){
|
|
// Line Annotation implementation
|
|
module.exports = function(Chart) {
|
|
var chartHelpers = Chart.helpers;
|
|
var helpers = require('../helpers.js')(Chart);
|
|
|
|
var horizontalKeyword = 'horizontal';
|
|
var verticalKeyword = 'vertical';
|
|
|
|
var LineAnnotation = Chart.Annotation.Element.extend({
|
|
setDataLimits: function() {
|
|
var model = this._model;
|
|
var options = this.options;
|
|
|
|
// Set the data range for this annotation
|
|
model.ranges = {};
|
|
model.ranges[options.scaleID] = {
|
|
min: options.value,
|
|
max: options.endValue || options.value
|
|
};
|
|
},
|
|
configure: function() {
|
|
var model = this._model;
|
|
var options = this.options;
|
|
var chartInstance = this.chartInstance;
|
|
var ctx = chartInstance.chart.ctx;
|
|
|
|
var scale = chartInstance.scales[options.scaleID];
|
|
var pixel, endPixel;
|
|
if (scale) {
|
|
pixel = helpers.isValid(options.value) ? scale.getPixelForValue(options.value) : NaN;
|
|
endPixel = helpers.isValid(options.endValue) ? scale.getPixelForValue(options.endValue) : pixel;
|
|
}
|
|
|
|
if (isNaN(pixel)) {
|
|
return;
|
|
}
|
|
|
|
var chartArea = chartInstance.chartArea;
|
|
|
|
// clip annotations to the chart area
|
|
model.clip = {
|
|
x1: chartArea.left,
|
|
x2: chartArea.right,
|
|
y1: chartArea.top,
|
|
y2: chartArea.bottom
|
|
};
|
|
|
|
if (this.options.mode == horizontalKeyword) {
|
|
model.x1 = chartArea.left;
|
|
model.x2 = chartArea.right;
|
|
model.y1 = pixel;
|
|
model.y2 = endPixel;
|
|
} else {
|
|
model.y1 = chartArea.top;
|
|
model.y2 = chartArea.bottom;
|
|
model.x1 = pixel;
|
|
model.x2 = endPixel;
|
|
}
|
|
|
|
model.line = new LineFunction(model);
|
|
model.mode = options.mode;
|
|
|
|
// Figure out the label:
|
|
model.labelBackgroundColor = options.label.backgroundColor;
|
|
model.labelFontFamily = options.label.fontFamily;
|
|
model.labelFontSize = options.label.fontSize;
|
|
model.labelFontStyle = options.label.fontStyle;
|
|
model.labelFontColor = options.label.fontColor;
|
|
model.labelXPadding = options.label.xPadding;
|
|
model.labelYPadding = options.label.yPadding;
|
|
model.labelCornerRadius = options.label.cornerRadius;
|
|
model.labelPosition = options.label.position;
|
|
model.labelXAdjust = options.label.xAdjust;
|
|
model.labelYAdjust = options.label.yAdjust;
|
|
model.labelEnabled = options.label.enabled;
|
|
model.labelContent = options.label.content;
|
|
|
|
ctx.font = chartHelpers.fontString(model.labelFontSize, model.labelFontStyle, model.labelFontFamily);
|
|
var textWidth = ctx.measureText(model.labelContent).width;
|
|
var textHeight = ctx.measureText('M').width;
|
|
var labelPosition = calculateLabelPosition(model, textWidth, textHeight, model.labelXPadding, model.labelYPadding);
|
|
model.labelX = labelPosition.x - model.labelXPadding;
|
|
model.labelY = labelPosition.y - model.labelYPadding;
|
|
model.labelWidth = textWidth + (2 * model.labelXPadding);
|
|
model.labelHeight = textHeight + (2 * model.labelYPadding);
|
|
|
|
model.borderColor = options.borderColor;
|
|
model.borderWidth = options.borderWidth;
|
|
model.borderDash = options.borderDash || [];
|
|
model.borderDashOffset = options.borderDashOffset || 0;
|
|
},
|
|
inRange: function(mouseX, mouseY) {
|
|
var model = this._model;
|
|
|
|
return (
|
|
// On the line
|
|
model.line &&
|
|
model.line.intersects(mouseX, mouseY, this.getHeight())
|
|
) || (
|
|
// On the label
|
|
model.labelEnabled &&
|
|
model.labelContent &&
|
|
mouseX >= model.labelX &&
|
|
mouseX <= model.labelX + model.labelWidth &&
|
|
mouseY >= model.labelY &&
|
|
mouseY <= model.labelY + model.labelHeight
|
|
);
|
|
},
|
|
getCenterPoint: function() {
|
|
return {
|
|
x: (this._model.x2 + this._model.x1) / 2,
|
|
y: (this._model.y2 + this._model.y1) / 2
|
|
};
|
|
},
|
|
getWidth: function() {
|
|
return Math.abs(this._model.right - this._model.left);
|
|
},
|
|
getHeight: function() {
|
|
return this._model.borderWidth || 1;
|
|
},
|
|
getArea: function() {
|
|
return Math.sqrt(Math.pow(this.getWidth(), 2) + Math.pow(this.getHeight(), 2));
|
|
},
|
|
draw: function() {
|
|
var view = this._view;
|
|
var ctx = this.chartInstance.chart.ctx;
|
|
|
|
if (!view.clip) {
|
|
return;
|
|
}
|
|
|
|
ctx.save();
|
|
|
|
// Canvas setup
|
|
ctx.beginPath();
|
|
ctx.rect(view.clip.x1, view.clip.y1, view.clip.x2 - view.clip.x1, view.clip.y2 - view.clip.y1);
|
|
ctx.clip();
|
|
|
|
ctx.lineWidth = view.borderWidth;
|
|
ctx.strokeStyle = view.borderColor;
|
|
|
|
if (ctx.setLineDash) {
|
|
ctx.setLineDash(view.borderDash);
|
|
}
|
|
ctx.lineDashOffset = view.borderDashOffset;
|
|
|
|
// Draw
|
|
ctx.beginPath();
|
|
ctx.moveTo(view.x1, view.y1);
|
|
ctx.lineTo(view.x2, view.y2);
|
|
ctx.stroke();
|
|
|
|
if (view.labelEnabled && view.labelContent) {
|
|
ctx.beginPath();
|
|
ctx.rect(view.clip.x1, view.clip.y1, view.clip.x2 - view.clip.x1, view.clip.y2 - view.clip.y1);
|
|
ctx.clip();
|
|
|
|
ctx.fillStyle = view.labelBackgroundColor;
|
|
// Draw the tooltip
|
|
chartHelpers.drawRoundedRectangle(
|
|
ctx,
|
|
view.labelX, // x
|
|
view.labelY, // y
|
|
view.labelWidth, // width
|
|
view.labelHeight, // height
|
|
view.labelCornerRadius // radius
|
|
);
|
|
ctx.fill();
|
|
|
|
// Draw the text
|
|
ctx.font = chartHelpers.fontString(
|
|
view.labelFontSize,
|
|
view.labelFontStyle,
|
|
view.labelFontFamily
|
|
);
|
|
ctx.fillStyle = view.labelFontColor;
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(
|
|
view.labelContent,
|
|
view.labelX + (view.labelWidth / 2),
|
|
view.labelY + (view.labelHeight / 2)
|
|
);
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
});
|
|
|
|
function LineFunction(view) {
|
|
// Describe the line in slope-intercept form (y = mx + b).
|
|
// Note that the axes are rotated 90° CCW, which causes the
|
|
// x- and y-axes to be swapped.
|
|
var m = (view.x2 - view.x1) / (view.y2 - view.y1);
|
|
var b = view.x1 || 0;
|
|
|
|
this.m = m;
|
|
this.b = b;
|
|
|
|
this.getX = function(y) {
|
|
// Coordinates are relative to the origin of the canvas
|
|
return m * (y - view.y1) + b;
|
|
};
|
|
|
|
this.getY = function(x) {
|
|
return ((x - b) / m) + view.y1;
|
|
};
|
|
|
|
this.intersects = function(x, y, epsilon) {
|
|
epsilon = epsilon || 0.001;
|
|
var dy = this.getY(x),
|
|
dx = this.getX(y);
|
|
return (
|
|
(!isFinite(dy) || Math.abs(y - dy) < epsilon) &&
|
|
(!isFinite(dx) || Math.abs(x - dx) < epsilon)
|
|
);
|
|
};
|
|
}
|
|
|
|
function calculateLabelPosition(view, width, height, padWidth, padHeight) {
|
|
var line = view.line;
|
|
var ret = {}, xa = 0, ya = 0;
|
|
|
|
switch (true) {
|
|
// top align
|
|
case view.mode == verticalKeyword && view.labelPosition == "top":
|
|
ya = padHeight + view.labelYAdjust;
|
|
xa = (width / 2) + view.labelXAdjust;
|
|
ret.y = view.y1 + ya;
|
|
ret.x = (isFinite(line.m) ? line.getX(ret.y) : view.x1) - xa;
|
|
break;
|
|
|
|
// bottom align
|
|
case view.mode == verticalKeyword && view.labelPosition == "bottom":
|
|
ya = height + padHeight + view.labelYAdjust;
|
|
xa = (width / 2) + view.labelXAdjust;
|
|
ret.y = view.y2 - ya;
|
|
ret.x = (isFinite(line.m) ? line.getX(ret.y) : view.x1) - xa;
|
|
break;
|
|
|
|
// left align
|
|
case view.mode == horizontalKeyword && view.labelPosition == "left":
|
|
xa = padWidth + view.labelXAdjust;
|
|
ya = -(height / 2) + view.labelYAdjust;
|
|
ret.x = view.x1 + xa;
|
|
ret.y = line.getY(ret.x) + ya;
|
|
break;
|
|
|
|
// right align
|
|
case view.mode == horizontalKeyword && view.labelPosition == "right":
|
|
xa = width + padWidth + view.labelXAdjust;
|
|
ya = -(height / 2) + view.labelYAdjust;
|
|
ret.x = view.x2 - xa;
|
|
ret.y = line.getY(ret.x) + ya;
|
|
break;
|
|
|
|
// center align
|
|
default:
|
|
ret.x = ((view.x1 + view.x2 - width) / 2) + view.labelXAdjust;
|
|
ret.y = ((view.y1 + view.y2 - height) / 2) + view.labelYAdjust;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
return LineAnnotation;
|
|
};
|
|
|
|
},{"../helpers.js":5}]},{},[6]);
|