chartjs-chart-graph
Version:
Chart.js module for charting graphs
1,473 lines (1,338 loc) • 92 kB
JavaScript
/**
* chartjs-chart-graph
* https://github.com/sgratzl/chartjs-chart-graph
*
* Copyright (c) 2019-2023 Samuel Gratzl <sam@sgratzl.com>
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('chart.js'), require('chart.js/helpers')) :
typeof define === 'function' && define.amd ? define(['exports', 'chart.js', 'chart.js/helpers'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ChartGraphs = {}, global.Chart, global.Chart.helpers));
})(this, (function (exports, chart_js, helpers) { 'use strict';
function horizontal(from, to, options) {
return {
fx: (to.x - from.x) * options.tension,
fy: 0,
tx: (from.x - to.x) * options.tension,
ty: 0,
};
}
function vertical(from, to, options) {
return {
fx: 0,
fy: (to.y - from.y) * options.tension,
tx: 0,
ty: (from.y - to.y) * options.tension,
};
}
function radial(from, to, options) {
const angleHelper = Math.hypot(to.x - from.x, to.y - from.y) * options.tension;
return {
fx: Number.isNaN(from.angle) ? 0 : Math.cos(from.angle || 0) * angleHelper,
fy: Number.isNaN(from.angle) ? 0 : Math.sin(from.angle || 0) * -angleHelper,
tx: Number.isNaN(to.angle) ? 0 : Math.cos(to.angle || 0) * -angleHelper,
ty: Number.isNaN(to.angle) ? 0 : Math.sin(to.angle || 0) * angleHelper,
};
}
class EdgeLine extends chart_js.LineElement {
draw(ctx) {
const { options } = this;
ctx.save();
ctx.lineCap = options.borderCapStyle;
ctx.setLineDash(options.borderDash || []);
ctx.lineDashOffset = options.borderDashOffset;
ctx.lineJoin = options.borderJoinStyle;
ctx.lineWidth = options.borderWidth;
ctx.strokeStyle = options.borderColor;
const orientations = {
horizontal,
vertical,
radial,
};
const layout = orientations[this._orientation] || orientations.horizontal;
const renderLine = (from, to) => {
const shift = layout(from, to, options);
const fromX = {
cpx: from.x + shift.fx,
cpy: from.y + shift.fy,
};
const toX = {
cpx: to.x + shift.tx,
cpy: to.y + shift.ty,
};
if (options.stepped === 'middle') {
const midpoint = (from.x + to.x) / 2.0;
ctx.lineTo(midpoint, from.y);
ctx.lineTo(midpoint, to.y);
ctx.lineTo(to.x, to.y);
}
else if (options.stepped === 'after') {
ctx.lineTo(from.x, to.y);
ctx.lineTo(to.x, to.y);
}
else if (options.stepped) {
ctx.lineTo(to.x, from.y);
ctx.lineTo(to.x, to.y);
}
else if (options.tension) {
ctx.bezierCurveTo(fromX.cpx, fromX.cpy, toX.cpx, toX.cpy, to.x, to.y);
}
else {
ctx.lineTo(to.x, to.y);
}
return to;
};
const source = this.source.getProps(['x', 'y', 'angle']);
const target = this.target.getProps(['x', 'y', 'angle']);
const points = this.getProps(['points']).points;
ctx.beginPath();
let from = source;
ctx.moveTo(from.x, from.y);
if (points && points.length > 0) {
from = points.reduce(renderLine, from);
}
renderLine(from, target);
ctx.stroke();
if (options.directed) {
const to = target;
const shift = layout(from, to, options);
const s = options.arrowHeadSize;
const offset = options.arrowHeadOffset;
ctx.save();
ctx.translate(to.x, target.y);
if (options.stepped === 'middle') {
const midpoint = (from.x + to.x) / 2.0;
ctx.rotate(Math.atan2(to.y - to.y, to.x - midpoint));
}
else if (options.stepped === 'after') {
ctx.rotate(Math.atan2(to.y - to.y, to.x - from.x));
}
else if (options.stepped) {
ctx.rotate(Math.atan2(to.y - from.y, to.x - to.x));
}
else if (options.tension) {
const toX = {
x: to.x + shift.tx,
y: to.y + shift.ty,
};
const f = 0.1;
ctx.rotate(Math.atan2(to.y - toX.y * (1 - f) - from.y * f, to.x - toX.x * (1 - f) - from.x * f));
}
else {
ctx.rotate(Math.atan2(to.y - from.y, to.x - from.x));
}
ctx.translate(-offset, 0);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(-s, -s / 2);
ctx.lineTo(-s * 0.9, 0);
ctx.lineTo(-s, s / 2);
ctx.closePath();
ctx.fillStyle = ctx.strokeStyle;
ctx.fill();
ctx.restore();
}
ctx.restore();
}
}
EdgeLine.id = 'edgeLine';
EdgeLine.defaults = {
...chart_js.LineElement.defaults,
tension: 0,
directed: false,
arrowHeadSize: 15,
arrowHeadOffset: 5,
};
EdgeLine.defaultRoutes = chart_js.LineElement.defaultRoutes;
EdgeLine.descriptors = {
_scriptable: true,
_indexable: (name) => name !== 'borderDash',
};
function interpolateNumber(from, to, factor) {
if (from === to) {
return to;
}
return from + (to - from) * factor;
}
function interpolatorPoint(fromArray, i, to, factor) {
const from = fromArray[i] || fromArray[i - 1] || fromArray._source;
if (!from) {
return to;
}
const x = interpolateNumber(from.x, to.x, factor);
const y = interpolateNumber(from.y, to.y, factor);
const angle = Number.isNaN(from.angle) ? interpolateNumber(from.angle, to.angle, factor) : undefined;
return { x, y, angle };
}
function interpolatePoints(from, to, factor) {
if (Array.isArray(from) && Array.isArray(to) && to.length > 0) {
return to.map((t, i) => interpolatorPoint(from, i, t, factor));
}
return to;
}
function patchController(type, config, controller, elements = [], scales = []) {
chart_js.registry.addControllers(controller);
if (Array.isArray(elements)) {
chart_js.registry.addElements(...elements);
}
else {
chart_js.registry.addElements(elements);
}
if (Array.isArray(scales)) {
chart_js.registry.addScales(...scales);
}
else {
chart_js.registry.addScales(scales);
}
const c = config;
c.type = type;
return c;
}
class GraphController extends chart_js.ScatterController {
constructor() {
super(...arguments);
this._scheduleResyncLayoutId = -1;
this._edgeListener = {
_onDataPush: (...args) => {
const count = args.length;
const start = this.getDataset().edges.length - count;
const parsed = this._cachedMeta._parsedEdges;
args.forEach((edge) => {
parsed.push(this._parseDefinedEdge(edge));
});
this._insertEdgeElements(start, count);
},
_onDataPop: () => {
this._cachedMeta.edges.pop();
this._cachedMeta._parsedEdges.pop();
this._scheduleResyncLayout();
},
_onDataShift: () => {
this._cachedMeta.edges.shift();
this._cachedMeta._parsedEdges.shift();
this._scheduleResyncLayout();
},
_onDataSplice: (start, count, ...args) => {
this._cachedMeta.edges.splice(start, count);
this._cachedMeta._parsedEdges.splice(start, count);
if (args.length > 0) {
const parsed = this._cachedMeta._parsedEdges;
parsed.splice(start, 0, ...args.map((edge) => this._parseDefinedEdge(edge)));
this._insertEdgeElements(start, args.length);
}
else {
this._scheduleResyncLayout();
}
},
_onDataUnshift: (...args) => {
const parsed = this._cachedMeta._parsedEdges;
parsed.unshift(...args.map((edge) => this._parseDefinedEdge(edge)));
this._insertEdgeElements(0, args.length);
},
};
}
initialize() {
const type = this._type;
const defaultConfig = chart_js.defaults.datasets[type];
this.edgeElementType = chart_js.registry.getElement(defaultConfig.edgeElementType);
super.initialize();
this.enableOptionSharing = true;
this._scheduleResyncLayout();
}
parse(start, count) {
const meta = this._cachedMeta;
const data = this._data;
const { iScale, vScale } = meta;
for (let i = 0; i < count; i += 1) {
const index = i + start;
const d = data[index];
const v = (meta._parsed[index] || {});
if (d && typeof d.x === 'number') {
v.x = d.x;
}
if (d && typeof d.y === 'number') {
v.y = d.y;
}
meta._parsed[index] = v;
}
if (meta._parsed.length > data.length) {
meta._parsed.splice(data.length, meta._parsed.length - data.length);
}
this._cachedMeta._sorted = false;
iScale._dataLimitsCached = false;
vScale._dataLimitsCached = false;
this._parseEdges();
}
reset() {
this.resetLayout();
super.reset();
}
update(mode) {
super.update(mode);
const meta = this._cachedMeta;
const edges = meta.edges || [];
this.updateEdgeElements(edges, 0, mode);
}
_destroy() {
chart_js.ScatterController.prototype._destroy.call(this);
if (this._edges) {
helpers.unlistenArrayEvents(this._edges, this._edgeListener);
}
this.stopLayout();
}
updateEdgeElements(edges, start, mode) {
var _a, _b, _c;
const bak = {
_cachedDataOpts: this._cachedDataOpts,
dataElementType: this.dataElementType,
_sharedOptions: this._sharedOptions,
};
this._cachedDataOpts = {};
this.dataElementType = this.edgeElementType;
this._sharedOptions = this._edgeSharedOptions;
const dataset = this.getDataset();
const meta = this._cachedMeta;
const nodeElements = meta.data;
const data = this._cachedMeta._parsedEdges;
this.getContext(-1, false, mode);
this.getDataset = () => {
return new Proxy(dataset, {
get(obj, prop) {
var _a;
return prop === 'data' ? ((_a = obj.edges) !== null && _a !== void 0 ? _a : []) : obj[prop];
},
});
};
this.getParsed = (index) => {
return data[index];
};
meta.data = meta.edges;
const reset = mode === 'reset';
const firstOpts = this.resolveDataElementOptions(start, mode);
const dummyShared = {};
const sharedOptions = (_a = this.getSharedOptions(firstOpts)) !== null && _a !== void 0 ? _a : dummyShared;
const includeOptions = this.includeOptions(mode, sharedOptions);
const { xScale, yScale } = meta;
const base = {
x: (_b = xScale === null || xScale === void 0 ? void 0 : xScale.getBasePixel()) !== null && _b !== void 0 ? _b : 0,
y: (_c = yScale === null || yScale === void 0 ? void 0 : yScale.getBasePixel()) !== null && _c !== void 0 ? _c : 0,
};
function copyPoint(point) {
var _a, _b;
const x = reset ? base.x : ((_a = xScale === null || xScale === void 0 ? void 0 : xScale.getPixelForValue(point.x, 0)) !== null && _a !== void 0 ? _a : 0);
const y = reset ? base.y : ((_b = yScale === null || yScale === void 0 ? void 0 : yScale.getPixelForValue(point.y, 0)) !== null && _b !== void 0 ? _b : 0);
return {
x,
y,
angle: point.angle,
};
}
for (let i = 0; i < edges.length; i += 1) {
const edge = edges[i];
const index = start + i;
const parsed = data[index];
const properties = {
source: nodeElements[parsed.source],
target: nodeElements[parsed.target],
points: Array.isArray(parsed.points) ? parsed.points.map((p) => copyPoint(p)) : [],
};
properties.points._source = nodeElements[parsed.source];
if (includeOptions) {
if (sharedOptions !== dummyShared) {
properties.options = sharedOptions;
}
else {
properties.options = this.resolveDataElementOptions(index, mode);
}
}
this.updateEdgeElement(edge, index, properties, mode);
}
this.updateSharedOptions(sharedOptions, mode, firstOpts);
this._edgeSharedOptions = this._sharedOptions;
Object.assign(this, bak);
delete this.getDataset;
delete this.getParsed;
meta.data = nodeElements;
}
updateEdgeElement(edge, index, properties, mode) {
super.updateElement(edge, index, properties, mode);
}
updateElement(point, index, properties, mode) {
var _a;
if (mode === 'reset') {
const { xScale } = this._cachedMeta;
properties.x = (_a = xScale === null || xScale === void 0 ? void 0 : xScale.getBasePixel()) !== null && _a !== void 0 ? _a : 0;
}
super.updateElement(point, index, properties, mode);
}
resolveNodeIndex(nodes, ref) {
if (typeof ref === 'number') {
return ref;
}
if (typeof ref === 'string') {
const labels = this.chart.data.labels;
return labels.indexOf(ref);
}
const nIndex = nodes.indexOf(ref);
if (nIndex >= 0) {
return nIndex;
}
const data = this.getDataset().data;
const index = data.indexOf(ref);
if (index >= 0) {
return index;
}
console.warn('cannot resolve edge ref', ref);
return -1;
}
buildOrUpdateElements() {
const dataset = this.getDataset();
const edges = dataset.edges || [];
if (this._edges !== edges) {
if (this._edges) {
helpers.unlistenArrayEvents(this._edges, this._edgeListener);
}
if (edges && Object.isExtensible(edges)) {
helpers.listenArrayEvents(edges, this._edgeListener);
}
this._edges = edges;
}
super.buildOrUpdateElements();
}
draw() {
const meta = this._cachedMeta;
const edges = this._cachedMeta.edges || [];
const elements = (meta.data || []);
const area = this.chart.chartArea;
const ctx = this._ctx;
if (edges.length > 0) {
helpers.clipArea(ctx, area);
edges.forEach((edge) => edge.draw.call(edge, ctx, area));
helpers.unclipArea(ctx);
}
elements.forEach((elem) => elem.draw.call(elem, ctx, area));
}
_resyncElements() {
chart_js.ScatterController.prototype._resyncElements.call(this);
const meta = this._cachedMeta;
const edges = meta._parsedEdges;
const metaEdges = meta.edges || (meta.edges = []);
const numMeta = metaEdges.length;
const numData = edges.length;
if (numData < numMeta) {
metaEdges.splice(numData, numMeta - numData);
this._scheduleResyncLayout();
}
else if (numData > numMeta) {
this._insertEdgeElements(numMeta, numData - numMeta);
}
}
getTreeRootIndex() {
const ds = this.getDataset();
const nodes = ds.data;
if (ds.derivedEdges) {
return nodes.findIndex((d) => d.parent == null);
}
const edges = this._cachedMeta._parsedEdges || [];
const nodeIndices = new Set(nodes.map((_, i) => i));
edges.forEach((edge) => {
nodeIndices.delete(edge.target);
});
return Array.from(nodeIndices)[0];
}
getTreeRoot() {
const index = this.getTreeRootIndex();
const p = this.getParsed(index);
p.index = index;
return p;
}
getTreeChildren(node) {
var _a;
const edges = this._cachedMeta._parsedEdges;
const index = (_a = node.index) !== null && _a !== void 0 ? _a : 0;
return edges
.filter((d) => d.source === index)
.map((d) => {
const p = this.getParsed(d.target);
p.index = d.target;
return p;
});
}
_parseDefinedEdge(edge) {
const ds = this.getDataset();
const { data } = ds;
return {
source: this.resolveNodeIndex(data, edge.source),
target: this.resolveNodeIndex(data, edge.target),
points: [],
};
}
_parseEdges() {
const ds = this.getDataset();
const data = ds.data;
const meta = this._cachedMeta;
if (ds.edges) {
const edges = ds.edges.map((edge) => this._parseDefinedEdge(edge));
meta._parsedEdges = edges;
return edges;
}
const edges = [];
meta._parsedEdges = edges;
data.forEach((node, i) => {
if (node.parent != null) {
const parent = this.resolveNodeIndex(data, node.parent);
edges.push({
source: parent,
target: i,
points: [],
});
}
});
return edges;
}
addElements() {
super.addElements();
const meta = this._cachedMeta;
const edges = this._parseEdges();
const metaData = new Array(edges.length);
meta.edges = metaData;
for (let i = 0; i < edges.length; i += 1) {
metaData[i] = new this.edgeElementType();
}
}
_resyncEdgeElements() {
const meta = this._cachedMeta;
const edges = this._parseEdges();
const metaData = meta.edges || (meta.edges = []);
for (let i = 0; i < edges.length; i += 1) {
metaData[i] = metaData[i] || new this.edgeElementType();
}
if (edges.length < metaData.length) {
metaData.splice(edges.length, metaData.length);
}
}
_insertElements(start, count) {
chart_js.ScatterController.prototype._insertElements.call(this, start, count);
if (count > 0) {
this._resyncEdgeElements();
}
}
_removeElements(start, count) {
chart_js.ScatterController.prototype._removeElements.call(this, start, count);
if (count > 0) {
this._resyncEdgeElements();
}
}
_insertEdgeElements(start, count) {
const elements = [];
for (let i = 0; i < count; i += 1) {
elements.push(new this.edgeElementType());
}
this._cachedMeta.edges.splice(start, 0, ...elements);
this.updateEdgeElements(elements, start, 'reset');
this._scheduleResyncLayout();
}
reLayout() {
}
resetLayout() {
}
stopLayout() {
}
_scheduleResyncLayout() {
if (this._scheduleResyncLayoutId != null && this._scheduleResyncLayoutId >= 0) {
return;
}
this._scheduleResyncLayoutId = requestAnimationFrame(() => {
this._scheduleResyncLayoutId = -1;
this.resyncLayout();
});
}
resyncLayout() {
}
}
GraphController.id = 'graph';
GraphController.defaults = helpers.merge({}, [
chart_js.ScatterController.defaults,
{
clip: 10,
animations: {
points: {
fn: interpolatePoints,
properties: ['points'],
},
},
edgeElementType: EdgeLine.id,
},
]);
GraphController.overrides = helpers.merge({}, [
chart_js.ScatterController.overrides,
{
layout: {
padding: 10,
},
scales: {
x: {
display: false,
ticks: {
maxTicksLimit: 2,
precision: 100,
minRotation: 0,
maxRotation: 0,
},
},
y: {
display: false,
ticks: {
maxTicksLimit: 2,
precision: 100,
minRotation: 0,
maxRotation: 0,
},
},
},
plugins: {
tooltip: {
callbacks: {
label(item) {
var _a, _b;
return (_b = (_a = item.chart.data) === null || _a === void 0 ? void 0 : _a.labels) === null || _b === void 0 ? void 0 : _b[item.dataIndex];
},
},
},
},
},
]);
class GraphChart extends chart_js.Chart {
constructor(item, config) {
super(item, patchController('graph', config, GraphController, [EdgeLine, chart_js.PointElement], chart_js.LinearScale));
}
}
GraphChart.id = GraphController.id;
function forceCenter(x, y) {
var nodes, strength = 1;
if (x == null) x = 0;
if (y == null) y = 0;
function force() {
var i,
n = nodes.length,
node,
sx = 0,
sy = 0;
for (i = 0; i < n; ++i) {
node = nodes[i], sx += node.x, sy += node.y;
}
for (sx = (sx / n - x) * strength, sy = (sy / n - y) * strength, i = 0; i < n; ++i) {
node = nodes[i], node.x -= sx, node.y -= sy;
}
}
force.initialize = function(_) {
nodes = _;
};
force.x = function(_) {
return arguments.length ? (x = +_, force) : x;
};
force.y = function(_) {
return arguments.length ? (y = +_, force) : y;
};
force.strength = function(_) {
return arguments.length ? (strength = +_, force) : strength;
};
return force;
}
function tree_add(d) {
const x = +this._x.call(null, d),
y = +this._y.call(null, d);
return add(this.cover(x, y), x, y, d);
}
function add(tree, x, y, d) {
if (isNaN(x) || isNaN(y)) return tree; // ignore invalid points
var parent,
node = tree._root,
leaf = {data: d},
x0 = tree._x0,
y0 = tree._y0,
x1 = tree._x1,
y1 = tree._y1,
xm,
ym,
xp,
yp,
right,
bottom,
i,
j;
// If the tree is empty, initialize the root as a leaf.
if (!node) return tree._root = leaf, tree;
// Find the existing leaf for the new point, or add it.
while (node.length) {
if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;
if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;
if (parent = node, !(node = node[i = bottom << 1 | right])) return parent[i] = leaf, tree;
}
// Is the new point is exactly coincident with the existing point?
xp = +tree._x.call(null, node.data);
yp = +tree._y.call(null, node.data);
if (x === xp && y === yp) return leaf.next = node, parent ? parent[i] = leaf : tree._root = leaf, tree;
// Otherwise, split the leaf node until the old and new point are separated.
do {
parent = parent ? parent[i] = new Array(4) : tree._root = new Array(4);
if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;
if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;
} while ((i = bottom << 1 | right) === (j = (yp >= ym) << 1 | (xp >= xm)));
return parent[j] = node, parent[i] = leaf, tree;
}
function addAll(data) {
var d, i, n = data.length,
x,
y,
xz = new Array(n),
yz = new Array(n),
x0 = Infinity,
y0 = Infinity,
x1 = -Infinity,
y1 = -Infinity;
// Compute the points and their extent.
for (i = 0; i < n; ++i) {
if (isNaN(x = +this._x.call(null, d = data[i])) || isNaN(y = +this._y.call(null, d))) continue;
xz[i] = x;
yz[i] = y;
if (x < x0) x0 = x;
if (x > x1) x1 = x;
if (y < y0) y0 = y;
if (y > y1) y1 = y;
}
// If there were no (valid) points, abort.
if (x0 > x1 || y0 > y1) return this;
// Expand the tree to cover the new points.
this.cover(x0, y0).cover(x1, y1);
// Add the new points.
for (i = 0; i < n; ++i) {
add(this, xz[i], yz[i], data[i]);
}
return this;
}
function tree_cover(x, y) {
if (isNaN(x = +x) || isNaN(y = +y)) return this; // ignore invalid points
var x0 = this._x0,
y0 = this._y0,
x1 = this._x1,
y1 = this._y1;
// If the quadtree has no extent, initialize them.
// Integer extent are necessary so that if we later double the extent,
// the existing quadrant boundaries don’t change due to floating point error!
if (isNaN(x0)) {
x1 = (x0 = Math.floor(x)) + 1;
y1 = (y0 = Math.floor(y)) + 1;
}
// Otherwise, double repeatedly to cover.
else {
var z = x1 - x0 || 1,
node = this._root,
parent,
i;
while (x0 > x || x >= x1 || y0 > y || y >= y1) {
i = (y < y0) << 1 | (x < x0);
parent = new Array(4), parent[i] = node, node = parent, z *= 2;
switch (i) {
case 0: x1 = x0 + z, y1 = y0 + z; break;
case 1: x0 = x1 - z, y1 = y0 + z; break;
case 2: x1 = x0 + z, y0 = y1 - z; break;
case 3: x0 = x1 - z, y0 = y1 - z; break;
}
}
if (this._root && this._root.length) this._root = node;
}
this._x0 = x0;
this._y0 = y0;
this._x1 = x1;
this._y1 = y1;
return this;
}
function tree_data() {
var data = [];
this.visit(function(node) {
if (!node.length) do data.push(node.data); while (node = node.next)
});
return data;
}
function tree_extent(_) {
return arguments.length
? this.cover(+_[0][0], +_[0][1]).cover(+_[1][0], +_[1][1])
: isNaN(this._x0) ? undefined : [[this._x0, this._y0], [this._x1, this._y1]];
}
function Quad(node, x0, y0, x1, y1) {
this.node = node;
this.x0 = x0;
this.y0 = y0;
this.x1 = x1;
this.y1 = y1;
}
function tree_find(x, y, radius) {
var data,
x0 = this._x0,
y0 = this._y0,
x1,
y1,
x2,
y2,
x3 = this._x1,
y3 = this._y1,
quads = [],
node = this._root,
q,
i;
if (node) quads.push(new Quad(node, x0, y0, x3, y3));
if (radius == null) radius = Infinity;
else {
x0 = x - radius, y0 = y - radius;
x3 = x + radius, y3 = y + radius;
radius *= radius;
}
while (q = quads.pop()) {
// Stop searching if this quadrant can’t contain a closer node.
if (!(node = q.node)
|| (x1 = q.x0) > x3
|| (y1 = q.y0) > y3
|| (x2 = q.x1) < x0
|| (y2 = q.y1) < y0) continue;
// Bisect the current quadrant.
if (node.length) {
var xm = (x1 + x2) / 2,
ym = (y1 + y2) / 2;
quads.push(
new Quad(node[3], xm, ym, x2, y2),
new Quad(node[2], x1, ym, xm, y2),
new Quad(node[1], xm, y1, x2, ym),
new Quad(node[0], x1, y1, xm, ym)
);
// Visit the closest quadrant first.
if (i = (y >= ym) << 1 | (x >= xm)) {
q = quads[quads.length - 1];
quads[quads.length - 1] = quads[quads.length - 1 - i];
quads[quads.length - 1 - i] = q;
}
}
// Visit this point. (Visiting coincident points isn’t necessary!)
else {
var dx = x - +this._x.call(null, node.data),
dy = y - +this._y.call(null, node.data),
d2 = dx * dx + dy * dy;
if (d2 < radius) {
var d = Math.sqrt(radius = d2);
x0 = x - d, y0 = y - d;
x3 = x + d, y3 = y + d;
data = node.data;
}
}
}
return data;
}
function tree_remove(d) {
if (isNaN(x = +this._x.call(null, d)) || isNaN(y = +this._y.call(null, d))) return this; // ignore invalid points
var parent,
node = this._root,
retainer,
previous,
next,
x0 = this._x0,
y0 = this._y0,
x1 = this._x1,
y1 = this._y1,
x,
y,
xm,
ym,
right,
bottom,
i,
j;
// If the tree is empty, initialize the root as a leaf.
if (!node) return this;
// Find the leaf node for the point.
// While descending, also retain the deepest parent with a non-removed sibling.
if (node.length) while (true) {
if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm;
if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym;
if (!(parent = node, node = node[i = bottom << 1 | right])) return this;
if (!node.length) break;
if (parent[(i + 1) & 3] || parent[(i + 2) & 3] || parent[(i + 3) & 3]) retainer = parent, j = i;
}
// Find the point to remove.
while (node.data !== d) if (!(previous = node, node = node.next)) return this;
if (next = node.next) delete node.next;
// If there are multiple coincident points, remove just the point.
if (previous) return (next ? previous.next = next : delete previous.next), this;
// If this is the root point, remove it.
if (!parent) return this._root = next, this;
// Remove this leaf.
next ? parent[i] = next : delete parent[i];
// If the parent now contains exactly one leaf, collapse superfluous parents.
if ((node = parent[0] || parent[1] || parent[2] || parent[3])
&& node === (parent[3] || parent[2] || parent[1] || parent[0])
&& !node.length) {
if (retainer) retainer[j] = node;
else this._root = node;
}
return this;
}
function removeAll(data) {
for (var i = 0, n = data.length; i < n; ++i) this.remove(data[i]);
return this;
}
function tree_root() {
return this._root;
}
function tree_size() {
var size = 0;
this.visit(function(node) {
if (!node.length) do ++size; while (node = node.next)
});
return size;
}
function tree_visit(callback) {
var quads = [], q, node = this._root, child, x0, y0, x1, y1;
if (node) quads.push(new Quad(node, this._x0, this._y0, this._x1, this._y1));
while (q = quads.pop()) {
if (!callback(node = q.node, x0 = q.x0, y0 = q.y0, x1 = q.x1, y1 = q.y1) && node.length) {
var xm = (x0 + x1) / 2, ym = (y0 + y1) / 2;
if (child = node[3]) quads.push(new Quad(child, xm, ym, x1, y1));
if (child = node[2]) quads.push(new Quad(child, x0, ym, xm, y1));
if (child = node[1]) quads.push(new Quad(child, xm, y0, x1, ym));
if (child = node[0]) quads.push(new Quad(child, x0, y0, xm, ym));
}
}
return this;
}
function tree_visitAfter(callback) {
var quads = [], next = [], q;
if (this._root) quads.push(new Quad(this._root, this._x0, this._y0, this._x1, this._y1));
while (q = quads.pop()) {
var node = q.node;
if (node.length) {
var child, x0 = q.x0, y0 = q.y0, x1 = q.x1, y1 = q.y1, xm = (x0 + x1) / 2, ym = (y0 + y1) / 2;
if (child = node[0]) quads.push(new Quad(child, x0, y0, xm, ym));
if (child = node[1]) quads.push(new Quad(child, xm, y0, x1, ym));
if (child = node[2]) quads.push(new Quad(child, x0, ym, xm, y1));
if (child = node[3]) quads.push(new Quad(child, xm, ym, x1, y1));
}
next.push(q);
}
while (q = next.pop()) {
callback(q.node, q.x0, q.y0, q.x1, q.y1);
}
return this;
}
function defaultX(d) {
return d[0];
}
function tree_x(_) {
return arguments.length ? (this._x = _, this) : this._x;
}
function defaultY(d) {
return d[1];
}
function tree_y(_) {
return arguments.length ? (this._y = _, this) : this._y;
}
function quadtree(nodes, x, y) {
var tree = new Quadtree(x == null ? defaultX : x, y == null ? defaultY : y, NaN, NaN, NaN, NaN);
return nodes == null ? tree : tree.addAll(nodes);
}
function Quadtree(x, y, x0, y0, x1, y1) {
this._x = x;
this._y = y;
this._x0 = x0;
this._y0 = y0;
this._x1 = x1;
this._y1 = y1;
this._root = undefined;
}
function leaf_copy(leaf) {
var copy = {data: leaf.data}, next = copy;
while (leaf = leaf.next) next = next.next = {data: leaf.data};
return copy;
}
var treeProto = quadtree.prototype = Quadtree.prototype;
treeProto.copy = function() {
var copy = new Quadtree(this._x, this._y, this._x0, this._y0, this._x1, this._y1),
node = this._root,
nodes,
child;
if (!node) return copy;
if (!node.length) return copy._root = leaf_copy(node), copy;
nodes = [{source: node, target: copy._root = new Array(4)}];
while (node = nodes.pop()) {
for (var i = 0; i < 4; ++i) {
if (child = node.source[i]) {
if (child.length) nodes.push({source: child, target: node.target[i] = new Array(4)});
else node.target[i] = leaf_copy(child);
}
}
}
return copy;
};
treeProto.add = tree_add;
treeProto.addAll = addAll;
treeProto.cover = tree_cover;
treeProto.data = tree_data;
treeProto.extent = tree_extent;
treeProto.find = tree_find;
treeProto.remove = tree_remove;
treeProto.removeAll = removeAll;
treeProto.root = tree_root;
treeProto.size = tree_size;
treeProto.visit = tree_visit;
treeProto.visitAfter = tree_visitAfter;
treeProto.x = tree_x;
treeProto.y = tree_y;
function constant(x) {
return function() {
return x;
};
}
function jiggle(random) {
return (random() - 0.5) * 1e-6;
}
function x$1(d) {
return d.x + d.vx;
}
function y$1(d) {
return d.y + d.vy;
}
function forceCollide(radius) {
var nodes,
radii,
random,
strength = 1,
iterations = 1;
if (typeof radius !== "function") radius = constant(radius == null ? 1 : +radius);
function force() {
var i, n = nodes.length,
tree,
node,
xi,
yi,
ri,
ri2;
for (var k = 0; k < iterations; ++k) {
tree = quadtree(nodes, x$1, y$1).visitAfter(prepare);
for (i = 0; i < n; ++i) {
node = nodes[i];
ri = radii[node.index], ri2 = ri * ri;
xi = node.x + node.vx;
yi = node.y + node.vy;
tree.visit(apply);
}
}
function apply(quad, x0, y0, x1, y1) {
var data = quad.data, rj = quad.r, r = ri + rj;
if (data) {
if (data.index > node.index) {
var x = xi - data.x - data.vx,
y = yi - data.y - data.vy,
l = x * x + y * y;
if (l < r * r) {
if (x === 0) x = jiggle(random), l += x * x;
if (y === 0) y = jiggle(random), l += y * y;
l = (r - (l = Math.sqrt(l))) / l * strength;
node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj));
node.vy += (y *= l) * r;
data.vx -= x * (r = 1 - r);
data.vy -= y * r;
}
}
return;
}
return x0 > xi + r || x1 < xi - r || y0 > yi + r || y1 < yi - r;
}
}
function prepare(quad) {
if (quad.data) return quad.r = radii[quad.data.index];
for (var i = quad.r = 0; i < 4; ++i) {
if (quad[i] && quad[i].r > quad.r) {
quad.r = quad[i].r;
}
}
}
function initialize() {
if (!nodes) return;
var i, n = nodes.length, node;
radii = new Array(n);
for (i = 0; i < n; ++i) node = nodes[i], radii[node.index] = +radius(node, i, nodes);
}
force.initialize = function(_nodes, _random) {
nodes = _nodes;
random = _random;
initialize();
};
force.iterations = function(_) {
return arguments.length ? (iterations = +_, force) : iterations;
};
force.strength = function(_) {
return arguments.length ? (strength = +_, force) : strength;
};
force.radius = function(_) {
return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius;
};
return force;
}
function index(d) {
return d.index;
}
function find(nodeById, nodeId) {
var node = nodeById.get(nodeId);
if (!node) throw new Error("node not found: " + nodeId);
return node;
}
function forceLink(links) {
var id = index,
strength = defaultStrength,
strengths,
distance = constant(30),
distances,
nodes,
count,
bias,
random,
iterations = 1;
if (links == null) links = [];
function defaultStrength(link) {
return 1 / Math.min(count[link.source.index], count[link.target.index]);
}
function force(alpha) {
for (var k = 0, n = links.length; k < iterations; ++k) {
for (var i = 0, link, source, target, x, y, l, b; i < n; ++i) {
link = links[i], source = link.source, target = link.target;
x = target.x + target.vx - source.x - source.vx || jiggle(random);
y = target.y + target.vy - source.y - source.vy || jiggle(random);
l = Math.sqrt(x * x + y * y);
l = (l - distances[i]) / l * alpha * strengths[i];
x *= l, y *= l;
target.vx -= x * (b = bias[i]);
target.vy -= y * b;
source.vx += x * (b = 1 - b);
source.vy += y * b;
}
}
}
function initialize() {
if (!nodes) return;
var i,
n = nodes.length,
m = links.length,
nodeById = new Map(nodes.map((d, i) => [id(d, i, nodes), d])),
link;
for (i = 0, count = new Array(n); i < m; ++i) {
link = links[i], link.index = i;
if (typeof link.source !== "object") link.source = find(nodeById, link.source);
if (typeof link.target !== "object") link.target = find(nodeById, link.target);
count[link.source.index] = (count[link.source.index] || 0) + 1;
count[link.target.index] = (count[link.target.index] || 0) + 1;
}
for (i = 0, bias = new Array(m); i < m; ++i) {
link = links[i], bias[i] = count[link.source.index] / (count[link.source.index] + count[link.target.index]);
}
strengths = new Array(m), initializeStrength();
distances = new Array(m), initializeDistance();
}
function initializeStrength() {
if (!nodes) return;
for (var i = 0, n = links.length; i < n; ++i) {
strengths[i] = +strength(links[i], i, links);
}
}
function initializeDistance() {
if (!nodes) return;
for (var i = 0, n = links.length; i < n; ++i) {
distances[i] = +distance(links[i], i, links);
}
}
force.initialize = function(_nodes, _random) {
nodes = _nodes;
random = _random;
initialize();
};
force.links = function(_) {
return arguments.length ? (links = _, initialize(), force) : links;
};
force.id = function(_) {
return arguments.length ? (id = _, force) : id;
};
force.iterations = function(_) {
return arguments.length ? (iterations = +_, force) : iterations;
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initializeStrength(), force) : strength;
};
force.distance = function(_) {
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), initializeDistance(), force) : distance;
};
return force;
}
var noop = {value: () => {}};
function dispatch() {
for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) {
if (!(t = arguments[i] + "") || (t in _) || /[\s.]/.test(t)) throw new Error("illegal type: " + t);
_[t] = [];
}
return new Dispatch(_);
}
function Dispatch(_) {
this._ = _;
}
function parseTypenames(typenames, types) {
return typenames.trim().split(/^|\s+/).map(function(t) {
var name = "", i = t.indexOf(".");
if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
if (t && !types.hasOwnProperty(t)) throw new Error("unknown type: " + t);
return {type: t, name: name};
});
}
Dispatch.prototype = dispatch.prototype = {
constructor: Dispatch,
on: function(typename, callback) {
var _ = this._,
T = parseTypenames(typename + "", _),
t,
i = -1,
n = T.length;
// If no callback was specified, return the callback of the given type and name.
if (arguments.length < 2) {
while (++i < n) if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) return t;
return;
}
// If a type was specified, set the callback for the given type and name.
// Otherwise, if a null callback was specified, remove callbacks of the given name.
if (callback != null && typeof callback !== "function") throw new Error("invalid callback: " + callback);
while (++i < n) {
if (t = (typename = T[i]).type) _[t] = set(_[t], typename.name, callback);
else if (callback == null) for (t in _) _[t] = set(_[t], typename.name, null);
}
return this;
},
copy: function() {
var copy = {}, _ = this._;
for (var t in _) copy[t] = _[t].slice();
return new Dispatch(copy);
},
call: function(type, that) {
if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2];
if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
},
apply: function(type, that, args) {
if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
}
};
function get(type, name) {
for (var i = 0, n = type.length, c; i < n; ++i) {
if ((c = type[i]).name === name) {
return c.value;
}
}
}
function set(type, name, callback) {
for (var i = 0, n = type.length; i < n; ++i) {
if (type[i].name === name) {
type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1));
break;
}
}
if (callback != null) type.push({name: name, value: callback});
return type;
}
var frame = 0, // is an animation frame pending?
timeout = 0, // is a timeout pending?
interval = 0, // are any timers active?
pokeDelay = 1000, // how frequently we check for clock skew
taskHead,
taskTail,
clockLast = 0,
clockNow = 0,
clockSkew = 0,
clock = typeof performance === "object" && performance.now ? performance : Date,
setFrame = typeof window === "object" && window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : function(f) { setTimeout(f, 17); };
function now() {
return clockNow || (setFrame(clearNow), clockNow = clock.now() + clockSkew);
}
function clearNow() {
clockNow = 0;
}
function Timer() {
this._call =
this._time =
this._next = null;
}
Timer.prototype = timer.prototype = {
constructor: Timer,
restart: function(callback, delay, time) {
if (typeof callback !== "function") throw new TypeError("callback is not a function");
time = (time == null ? now() : +time) + (delay == null ? 0 : +delay);
if (!this._next && taskTail !== this) {
if (taskTail) taskTail._next = this;
else taskHead = this;
taskTail = this;
}
this._call = callback;
this._time = time;
sleep();
},
stop: function() {
if (this._call) {
this._call = null;
this._time = Infinity;
sleep();
}
}
};
function timer(callback, delay, time) {
var t = new Timer;
t.restart(callback, delay, time);
return t;
}
function timerFlush() {
now(); // Get the current time, if not already set.
++frame; // Pretend we’ve set an alarm, if we haven’t already.
var t = taskHead, e;
while (t) {
if ((e = clockNow - t._time) >= 0) t._call.call(undefined, e);
t = t._next;
}
--frame;
}
function wake() {
clockNow = (clockLast = clock.now()) + clockSkew;
frame = timeout = 0;
try {
timerFlush();
} finally {
frame = 0;
nap();
clockNow = 0;
}
}
function poke() {
var now = clock.now(), delay = now - clockLast;
if (delay > pokeDelay) clockSkew -= delay, clockLast = now;
}
function nap() {
var t0, t1 = taskHead, t2, time = Infinity;
while (t1) {
if (t1._call) {
if (time > t1._time) time = t1._time;
t0 = t1, t1 = t1._next;
} else {
t2 = t1._next, t1._next = null;
t1 = t0 ? t0._next = t2 : taskHead = t2;
}
}
taskTail = t0;
sleep(time);
}
function sleep(time) {
if (frame) return; // Soonest alarm already set, or will be.
if (timeout) timeout = clearTimeout(timeout);
var delay = time - clockNow; // Strictly less than if we recomputed clockNow.
if (delay > 24) {
if (time < Infinity) timeout = set