Comparar commits

...

4 Commits

Autor SHA1 Mensagem Data
Evert Timberg 50d23ff119 updates to quad tree impl 2016-02-23 21:58:41 -05:00
Evert Timberg 43efbc6e45 Should not have added closepath command 2016-02-21 21:49:26 -05:00
Evert Timberg 000eb6a687 test fixes 2016-02-21 20:13:56 -05:00
Evert Timberg 32e259b822 initial quad tree implementation (unoptimized). 2016-02-21 17:43:07 -05:00
9 arquivos alterados com 421 adições e 117 exclusões
+16 -9
Ver Arquivo
@@ -44,23 +44,22 @@
var config = {
type: 'line',
data: {
labels: ["January", "February", "March", "April", "May", "June", "July"],
labels: [],
datasets: [{
label: "My First dataset",
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
data: [],
fill: false,
borderDash: [5, 5],
}, {
hidden: true,
label: 'hidden dataset',
data: [],
}, {
label: "My Second dataset",
data: [randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor(), randomScalingFactor()],
data: [],
}]
},
options: {
responsive: true,
animation: {
duration: 0
},
tooltips: {
mode: 'label',
callbacks: {
@@ -88,7 +87,7 @@
}
},
hover: {
mode: 'dataset'
mode: 'single'
},
scales: {
xAxes: [{
@@ -121,7 +120,15 @@
dataset.pointBorderWidth = 1;
});
console.log(config.data);
$.each(config.data.datasets, function(i, dataset) {
for (var i = 0; i < 1000; ++i) {
dataset.data.push(randomScalingFactor());
}
});
for (var i = 0; i < 1000; ++i) {
config.data.labels.push('label ' + i);
}
window.onload = function() {
var ctx = document.getElementById("canvas").getContext("2d");
+1
Ver Arquivo
@@ -18,6 +18,7 @@ require('./core/core.controller')(Chart);
require('./core/core.datasetController')(Chart);
require('./core/core.layoutService')(Chart);
require('./core/core.legend')(Chart);
require('./core/core.quadtree.js')(Chart);
require('./core/core.scale')(Chart);
require('./core/core.scaleService')(Chart);
require('./core/core.title')(Chart);
+15 -1
Ver Arquivo
@@ -58,8 +58,9 @@ module.exports = function(Chart) {
this.getDataset().metaData.splice(index, 0, point);
// Make sure bezier control points are updated
if (this.chart.options.showLines && this.chart.options.elements.line.tension !== 0)
if (this.chart.options.showLines && this.chart.options.elements.line.tension !== 0) {
this.updateBezierControlPoints();
}
},
update: function update(reset) {
@@ -193,6 +194,19 @@ module.exports = function(Chart) {
};
point._model.skip = point.custom && point.custom.skip ? point.custom.skip : (isNaN(point._model.x) || isNaN(point._model.y));
this.removeElementFromQuadTree(point); // remove old quadtree point always
if (point._model.radius > 0) {
// Add to quadtree lookup if being drawn
var r = point._model.radius + point._model.borderWidth + point._model.hitRadius;
this.insertElementIntoQuadTree(point, {
x: point._model.x - r,
y: point._model.y - r,
width: 2 * r,
height: 2 * r
});
}
},
calculatePointY: function(value, index, datasetIndex, isCombo) {
+56 -18
Ver Arquivo
@@ -17,6 +17,14 @@ module.exports = function(Chart) {
Chart.Controller = function(instance) {
this.chart = instance;
// Allocate new quadtree
this.quadTree = new Chart.QuadTree({
x: 0,
y: 0,
width: this.chart.width,
height: this.chart.height
});
this.config = instance.config;
this.options = this.config.options = helpers.configMerge(Chart.defaults.global, Chart.defaults[this.config.type], this.config.options || {});
this.id = helpers.uid();
@@ -93,6 +101,14 @@ module.exports = function(Chart) {
helpers.retinaScale(this.chart);
this.quadTree.clear();
this.quadTree.resize({
x: 0,
y: 0,
width: newWidth,
height: newHeight
});
if (!silent) {
this.stop();
this.update(this.options.responsiveAnimationDuration);
@@ -323,25 +339,48 @@ module.exports = function(Chart) {
// Get the single element that was clicked on
// @return : An object containing the dataset index and element index of the matching element. Also contains the rectangle that was draw
getElementAtEvent: function(e) {
var elements = this.getElementsAtEvent(e);
return elements.length ? [elements[0]] : [];
},
getElementsAtEvent: function(e) {
var eventPosition = helpers.getRelativePosition(e, this.chart);
var elementsArray = [];
helpers.each(this.data.datasets, function(dataset, datasetIndex) {
if (helpers.isDatasetVisible(dataset)) {
helpers.each(dataset.metaData, function(element, index) {
if (element.inRange(eventPosition.x, eventPosition.y)) {
elementsArray.push(element);
return elementsArray;
}
});
}
var size = 5; // pixels
var objects = this.quadTree.retrieve({
x: eventPosition.x - size,
y: eventPosition.y - size,
width: 2 * size,
height: 2 * size,
});
// get elements
objects = objects.map(function(object) {
return object.element;
});
helpers.each(objects, function(obj) {
// If this dataset is visible and in range, consider this objects
if (helpers.isDatasetVisible(this.data.datasets[obj._datasetIndex]) && obj.inRange(eventPosition.x, eventPosition.y)) {
elementsArray.push(obj);
}
}, this);
var sortByDistance = function(a, b) {
var dA = a.distanceToCenter(eventPosition);
var dB = b.distanceToCenter(eventPosition);
return dA - dB;
};
// sort by closest to point
elementsArray.sort(sortByDistance);
return elementsArray;
},
getElementsAtEvent: function(e) {
getElementsAtLabelFromEvent: function(e) {
var eventPosition = helpers.getRelativePosition(e, this.chart);
var elementsArray = [];
@@ -371,13 +410,8 @@ module.exports = function(Chart) {
},
getDatasetAtEvent: function(e) {
var elementsArray = this.getElementAtEvent(e);
if (elementsArray.length > 0) {
elementsArray = this.data.datasets[elementsArray[0]._datasetIndex].metaData;
}
return elementsArray;
var elements = this.getElementsAtEvent(e);
return elements.length ? this.data.datasets[elements[0]._datasetIndex].metaData : [];
},
generateLegend: function generateLegend() {
@@ -439,8 +473,10 @@ module.exports = function(Chart) {
switch (mode) {
case 'single':
return _this.getElementAtEvent(e);
case 'label':
case 'near':
return _this.getElementsAtEvent(e);
case 'label':
return _this.getElementsAtLabelFromEvent(e);
case 'dataset':
return _this.getDatasetAtEvent(e);
default:
@@ -478,6 +514,7 @@ module.exports = function(Chart) {
break;
case 'label':
case 'dataset':
case 'near':
for (var i = 0; i < this.lastActive.length; i++) {
if (this.lastActive[i])
this.data.datasets[this.lastActive[i]._datasetIndex].controller.removeHoverStyle(this.lastActive[i], this.lastActive[i]._datasetIndex, this.lastActive[i]._index);
@@ -496,6 +533,7 @@ module.exports = function(Chart) {
break;
case 'label':
case 'dataset':
case 'near':
for (var j = 0; j < this.active.length; j++) {
if (this.active[j])
this.data.datasets[this.active[j]._datasetIndex].controller.setHoverStyle(this.active[j]);
+22
Ver Arquivo
@@ -59,6 +59,28 @@ module.exports = function(Chart) {
}
},
// Insert an element into the quadtree. set the treeProxy member of the element.
insertElementIntoQuadTree: function(element, bounds) {
var treeProxy = element.treeProxy || {}; // reuse tree proxy elements to prevent GC lag
treeProxy.x = bounds.x;
treeProxy.y = bounds.y;
treeProxy.width = bounds.width,
treeProxy.height = bounds.height,
treeProxy.element = element
this.chart.quadTree.insert(treeProxy);
// So we can look the item back up later
element.treeProxy = treeProxy;
},
// Removes the given element from the quadtree
removeElementFromQuadTree: function(element) {
if (element.treeProxy) {
this.chart.quadTree.remove(element.treeProxy);
}
},
// Controllers should implement the following
addElements: helpers.noop,
addElementAndReset: helpers.noop,
+80 -76
Ver Arquivo
@@ -2,92 +2,96 @@
module.exports = function(Chart) {
var helpers = Chart.helpers;
var helpers = Chart.helpers;
Chart.elements = {};
Chart.elements = {};
Chart.Element = function(configuration) {
helpers.extend(this, configuration);
this.initialize.apply(this, arguments);
};
helpers.extend(Chart.Element.prototype, {
initialize: function() {},
pivot: function() {
if (!this._view) {
this._view = helpers.clone(this._model);
}
this._start = helpers.clone(this._view);
return this;
},
transition: function(ease) {
if (!this._view) {
this._view = helpers.clone(this._model);
}
Chart.Element = function(configuration) {
helpers.extend(this, configuration);
this.initialize.apply(this, arguments);
};
helpers.extend(Chart.Element.prototype, {
initialize: function() {},
pivot: function() {
if (!this._view) {
this._view = helpers.clone(this._model);
}
this._start = helpers.clone(this._view);
return this;
},
transition: function(ease) {
if (!this._view) {
this._view = helpers.clone(this._model);
}
// No animation -> No Transition
if (ease === 1) {
this._view = this._model;
this._start = null;
return this;
}
// No animation -> No Transition
if (ease === 1) {
this._view = this._model;
this._start = null;
return this;
}
if (!this._start) {
this.pivot();
}
if (!this._start) {
this.pivot();
}
helpers.each(this._model, function(value, key) {
helpers.each(this._model, function(value, key) {
if (key[0] === '_' || !this._model.hasOwnProperty(key)) {
// Only non-underscored properties
}
if (key[0] === '_' || !this._model.hasOwnProperty(key)) {
// Only non-underscored properties
}
// Init if doesn't exist
else if (!this._view.hasOwnProperty(key)) {
if (typeof value === 'number' && !isNaN(this._view[key])) {
this._view[key] = value * ease;
} else {
this._view[key] = value;
}
}
// Init if doesn't exist
else if (!this._view.hasOwnProperty(key)) {
if (typeof value === 'number' && !isNaN(this._view[key])) {
this._view[key] = value * ease;
} else {
this._view[key] = value;
}
}
// No unnecessary computations
else if (value === this._view[key]) {
// It's the same! Woohoo!
}
// No unnecessary computations
else if (value === this._view[key]) {
// It's the same! Woohoo!
}
// Color transitions if possible
else if (typeof value === 'string') {
try {
var color = helpers.color(this._start[key]).mix(helpers.color(this._model[key]), ease);
this._view[key] = color.rgbString();
} catch (err) {
this._view[key] = value;
}
}
// Number transitions
else if (typeof value === 'number') {
var startVal = this._start[key] !== undefined && isNaN(this._start[key]) === false ? this._start[key] : 0;
this._view[key] = ((this._model[key] - startVal) * ease) + startVal;
}
// Everything else
else {
this._view[key] = value;
}
}, this);
// Color transitions if possible
else if (typeof value === 'string') {
try {
var color = helpers.color(this._start[key]).mix(helpers.color(this._model[key]), ease);
this._view[key] = color.rgbString();
} catch (err) {
this._view[key] = value;
}
}
// Number transitions
else if (typeof value === 'number') {
var startVal = this._start[key] !== undefined && isNaN(this._start[key]) === false ? this._start[key] : 0;
this._view[key] = ((this._model[key] - startVal) * ease) + startVal;
}
// Everything else
else {
this._view[key] = value;
}
}, this);
return this;
},
tooltipPosition: function() {
return {
x: this._model.x,
y: this._model.y
};
},
hasValue: function() {
return helpers.isNumber(this._model.x) && helpers.isNumber(this._model.y);
}
});
return this;
},
tooltipPosition: function() {
return {
x: this._model.x,
y: this._model.y
};
},
hasValue: function() {
return helpers.isNumber(this._model.x) && helpers.isNumber(this._model.y);
},
Chart.Element.extend = helpers.inherits;
// Derived items need to implement
inRange: helpers.noop,
distanceToCenter: helpers.noop
});
Chart.Element.extend = helpers.inherits;
};
+160
Ver Arquivo
@@ -0,0 +1,160 @@
"use strict";
// Static constants. Used values from a tutorial on quad-trees
var maxDepth = 5;
var maxItems = 10;
// Implementation of a quadtree for looking up items on the canvas
var QuadTree = function(bounds, parent, level) {
if (isNaN(bounds.x) || isNaN(bounds.y) || isNaN(bounds.width) || isNaN(bounds.height)) {
throw new Error("Must specify bounds when constructing quad tree");
}
this.parent = parent || null;
this.level = level || 0; // start at first level if not defined
this.bounds = bounds;
this.nodes = []; // Child quad trees
this.objects = [];
};
QuadTree.prototype.clear = function() {
this.objects = [];
this.nodes.forEach(function(node) {
node.clear();
});
this.nodes = [];
};
/// Determine if this node is empty
QuadTree.prototype.isEmpty = function() {
// We are only empty if all our nodes below us are empty
var empty = this.nodes.reduce(function(p, c) {
return p && c.isEmpty();
}, true);
if (empty) {
empty = empty && this.objects.length === 0;
}
return empty;
};
QuadTree.prototype.insert = function(obj) {
var node;
if (this.nodes.length) {
node = this.getNode(obj);
if (node) {
// Fits completely into child node? insert and end
node.insert(obj);
return;
}
}
this.objects.push(obj);
if (this.objects.length > maxItems && this.level < maxDepth) {
if (this.nodes.length === 0) {
this.split();
}
this.pushDown();
}
};
QuadTree.prototype.pushDown = function() {
var i = 0;
while (i < this.objects.length) {
var node = this.getNode(this.objects[i]);
if (node !== null) {
// remove and insert below
node.insert(this.objects.splice(i, 1)[0]);
} else {
++i;
}
}
};
// Split a tree node into 4 sub nodes
QuadTree.prototype.split = function() {
var subWidth = this.bounds.width / 2;
var subHeight = this.bounds.height / 2;
var x = this.bounds.x;
var y = this.bounds.y;
// Create 4 sub nodes
this.nodes[0] = new QuadTree({ x: x + subWidth, y: y, width: subWidth, height: subHeight }, this, this.level + 1);
this.nodes[1] = new QuadTree({ x: x, y: y, width: subWidth, height: subHeight }, this, this.level + 1);
this.nodes[2] = new QuadTree({ x: x, y: y + subHeight, width: subWidth, height: subHeight }, this, this.level + 1);
this.nodes[3] = new QuadTree({ x: x + subWidth, y: y + subHeight, width: subWidth, height: subHeight }, this, this.level + 1);
};
// Helper to figure out which node the rect goes into
// returns the node. null if it is bigger than all
QuadTree.prototype.getNode = function(rect) {
var node = null;
var midX = this.bounds.x + (this.bounds.width / 2);
var midY = this.bounds.y + (this.bounds.height/ 2);
var topQuadrant = rect.y < midY && rect.y + rect.height < midY; // fits completely in top
var bottomQuadrant = rect.y > midY; // fits completely in bottom
if (rect.x < midX && rect.x + rect.width < midX) {
// Completely in the left quadrants
node = topQuadrant? this.nodes[1] : bottomQuadrant ? this.nodes[2] : null;
} else if (rect.x > midX) {
// Completely within right quadrants
node = topQuadrant ? this.nodes[0] : bottomQuadrant ? this.nodes[3] : null;
}
return node;
};
QuadTree.prototype.remove = function(obj) {
var node = this.getNode(obj);
if (node) {
node.remove(obj);
}
var idx = this.objects.indexOf(obj);
if (idx !== -1) {
// Remove from ourself
this.objects.splice(idx, 1);
}
};
QuadTree.prototype.resize = function(newBounds) {
this.bounds = newBounds;
var subWidth = this.bounds.width / 2;
var subHeight = this.bounds.height / 2;
var x = this.bounds.x;
var y = this.bounds.y;
if (this.nodes.length) {
this.split();
}
};
// Retrieves all items that could collide with the rect
// Returns array of items. Empty array if none found
QuadTree.prototype.retrieve = function(rect) {
var node = this.getNode(rect);
var matches = [];
if (node) {
matches = node.retrieve(rect);
}
matches = matches.concat(this.objects);
return matches;
};
module.exports = function(Chart) {
Chart.QuadTree = QuadTree;
};
+14 -4
Ver Arquivo
@@ -37,6 +37,15 @@ module.exports = function(Chart) {
return false;
}
},
distanceToCenter: function(point) {
var vm = this._view;
if (vm) {
return Math.sqrt(Math.pow(point.x - vm.x, 2) + Math.pow(point.y - vm.y));
} else {
return NaN;
}
},
tooltipPosition: function() {
var vm = this._view;
return {
@@ -74,10 +83,11 @@ module.exports = function(Chart) {
switch (vm.pointStyle) {
// Default includes circle
default: ctx.beginPath();
ctx.arc(vm.x, vm.y, radius, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
default:
ctx.beginPath();
ctx.arc(vm.x, vm.y, radius, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
break;
case 'triangle':
ctx.beginPath();
+57 -9
Ver Arquivo
@@ -173,7 +173,7 @@ describe('Line controller tests', function() {
ctx: mockContext,
options: verticalScaleConfig,
chart: {
data: data
data: data,
},
id: 'firstYScaleID'
});
@@ -253,7 +253,13 @@ describe('Line controller tests', function() {
scales: {
firstXScaleID: xScale,
firstYScaleID: yScale,
}
},
quadTree: new Chart.QuadTree({
x: 0,
y: 0,
width: 250,
height: 250
})
};
var controller = new Chart.controllers.line(chart, 0);
@@ -651,7 +657,13 @@ describe('Line controller tests', function() {
scales: {
firstXScaleID: xScale,
firstYScaleID: yScale,
}
},
quadTree: new Chart.QuadTree({
x: 0,
y: 0,
width: 250,
height: 250
})
};
var controller = new Chart.controllers.line(chart, 0);
@@ -818,7 +830,13 @@ describe('Line controller tests', function() {
scales: {
firstXScaleID: xScale,
firstYScaleID: yScale,
}
},
quadTree: new Chart.QuadTree({
x: 0,
y: 0,
width: 250,
height: 250
})
};
var controller = new Chart.controllers.line(chart, 0);
@@ -933,7 +951,13 @@ describe('Line controller tests', function() {
scales: {
firstXScaleID: xScale,
firstYScaleID: yScale,
}
},
quadTree: new Chart.QuadTree({
x: 0,
y: 0,
width: 250,
height: 250
})
};
var controller = new Chart.controllers.line(chart, 0);
@@ -1048,7 +1072,13 @@ describe('Line controller tests', function() {
scales: {
firstXScaleID: xScale,
firstYScaleID: yScale,
}
},
quadTree: new Chart.QuadTree({
x: 0,
y: 0,
width: 250,
height: 250
})
};
var controller = new Chart.controllers.line(chart, 0);
@@ -1156,7 +1186,13 @@ describe('Line controller tests', function() {
scales: {
firstXScaleID: xScale,
firstYScaleID: yScale,
}
},
quadTree: new Chart.QuadTree({
x: 0,
y: 0,
width: 250,
height: 250
})
};
var controller = new Chart.controllers.line(chart, 0);
@@ -1276,7 +1312,13 @@ describe('Line controller tests', function() {
scales: {
firstXScaleID: xScale,
firstYScaleID: yScale,
}
},
quadTree: new Chart.QuadTree({
x: 0,
y: 0,
width: 250,
height: 250
})
};
var controller = new Chart.controllers.line(chart, 0);
@@ -1413,7 +1455,13 @@ describe('Line controller tests', function() {
scales: {
firstXScaleID: xScale,
firstYScaleID: yScale,
}
},
quadTree: new Chart.QuadTree({
x: 0,
y: 0,
width: 250,
height: 250
})
};
var controller = new Chart.controllers.line(chart, 0);