hilbert-chart
Version:
A hilbert space-filling curve D3 chart for representing one-dimensional lengths on a two-dimensional space.
1,008 lines (964 loc) • 39.6 kB
JavaScript
import { select, create, pointer } from 'd3-selection';
import { scaleLinear, scaleOrdinal } from 'd3-scale';
import { schemePaired } from 'd3-scale-chromatic';
import { axisLeft, axisRight, axisTop, axisBottom } from 'd3-axis';
import { zoomTransform, zoom, ZoomTransform } from 'd3-zoom';
import d3Hilbert from 'd3-hilbert';
import d3Tip from 'd3-tip';
import gsap from 'gsap';
import heatmap from 'heatmap.js';
import Kapsule from 'kapsule';
import accessorFn from 'accessor-fn';
import Tooltip from 'float-tooltip';
import { IntervalTree } from 'node-interval-tree';
import ScrollZoomClamp from 'scroll-zoom-clamp';
function styleInject(css, ref) {
if (ref === void 0) ref = {};
var insertAt = ref.insertAt;
if (typeof document === 'undefined') {
return;
}
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css_248z = ".hilbert-chart {\n position: relative;\n font-family: sans-serif;\n}\n\n.hilbert-chart .hilbert-segment path {\n fill: none;\n stroke-linecap: square;\n\n opacity: 1;\n transition: opacity 0.4s;\n}\n\n.hilbert-chart .hilbert-segment path:hover {\n opacity: 0.8;\n transition: opacity 0.2s;\n}\n\n.hilbert-chart .hilbert-segment text {\n pointer-events: none;\n}\n\n.hilbert-chart .hilbert-heatmap {\n position: absolute;\n pointer-events: none;\n}\n\n.hilbert-chart .hilbert-tooltip {\n color: #eee;\n background: rgba(0, 0, 0, 0.6);\n padding: 5px;\n border-radius: 3px;\n font: 11px sans-serif;\n pointer-events: none;\n}\n";
styleInject(css_248z);
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function _arrayWithHoles(r) {
if (Array.isArray(r)) return r;
}
function _arrayWithoutHoles(r) {
if (Array.isArray(r)) return _arrayLikeToArray(r);
}
function _defineProperty(e, r, t) {
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: true,
configurable: true,
writable: true
}) : e[r] = t, e;
}
function _iterableToArray(r) {
if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
}
function _iterableToArrayLimit(r, l) {
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (null != t) {
var e,
n,
i,
u,
a = [],
f = true,
o = false;
try {
if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
} catch (r) {
o = true, n = r;
} finally {
try {
if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
} finally {
if (o) throw n;
}
}
return a;
}
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function ownKeys(e, r) {
var t = Object.keys(e);
if (Object.getOwnPropertySymbols) {
var o = Object.getOwnPropertySymbols(e);
r && (o = o.filter(function (r) {
return Object.getOwnPropertyDescriptor(e, r).enumerable;
})), t.push.apply(t, o);
}
return t;
}
function _objectSpread2(e) {
for (var r = 1; r < arguments.length; r++) {
var t = null != arguments[r] ? arguments[r] : {};
r % 2 ? ownKeys(Object(t), true).forEach(function (r) {
_defineProperty(e, r, t[r]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
});
}
return e;
}
function _slicedToArray(r, e) {
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
}
function _toConsumableArray(r) {
return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread();
}
function _toPrimitive(t, r) {
if ("object" != typeof t || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r);
if ("object" != typeof i) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == typeof i ? i : i + "";
}
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
/** Render text along a path in a Canvas
* Adds extra functionality to the CanvasRenderingContext2D by extending its prototype.
* Extent the global object with options:
* - textOverflow {undefined|visible|ellipsis|string} the text to use on overflow, default "" (hidden)
* - textJustify {undefined|boolean} used to justify text (otherwise use textAlign), default false
* - textStrokeMin {undefined|number} the min length (in pixel) for the support path to draw the text upon, default 0
*
* @param {string} text the text to render
* @param {Array<Number>} path an array of coordinates as support for the text (ie. [x1,y1,x2,y2,...]
*/
(function () {
/* Usefull function */
function dist2D(x1, y1, x2, y2) {
var dx = x2 - x1;
var dy = y2 - y1;
return Math.sqrt(dx * dx + dy * dy);
}
/* Add new properties on CanvasRenderingContext2D */
CanvasRenderingContext2D.prototype.textOverflow = "";
CanvasRenderingContext2D.prototype.textJustify = false;
CanvasRenderingContext2D.prototype.textStrokeMin = 0;
var state = [];
var save = CanvasRenderingContext2D.prototype.save;
CanvasRenderingContext2D.prototype.save = function () {
state.push({
textOverflow: this.textOverflow,
textJustify: this.textJustify,
textStrokeMin: this.textStrokeMin
});
save.call(this);
};
var restore = CanvasRenderingContext2D.prototype.restore;
CanvasRenderingContext2D.prototype.restore = function () {
restore.call(this);
var s = state.pop();
this.textOverflow = s.textOverflow;
this.textJustify = s.textJustify;
this.textStrokeMin = s.textStrokeMin;
};
/* textPath function */
CanvasRenderingContext2D.prototype.textPath = function (text, path) {
// Helper to get a point on the path, starting at dl
// (return x, y and the angle on the path)
var di,
dpos = 0;
var pos = 2;
function pointAt(dl) {
if (!di || dpos + di < dl) {
for (; pos < path.length;) {
di = dist2D(path[pos - 2], path[pos - 1], path[pos], path[pos + 1]);
if (dpos + di > dl) break;
pos += 2;
if (pos >= path.length) break;
dpos += di;
}
}
var x,
y,
dt = dl - dpos;
if (pos >= path.length) {
pos = path.length - 2;
}
if (!dt) {
x = path[pos - 2];
y = path[pos - 1];
} else {
x = path[pos - 2] + (path[pos] - path[pos - 2]) * dt / di;
y = path[pos - 1] + (path[pos + 1] - path[pos - 1]) * dt / di;
}
return [x, y, Math.atan2(path[pos + 1] - path[pos - 1], path[pos] - path[pos - 2])];
}
var letterPadding = this.measureText(" ").width * 0.25;
// Calculate length
var d = 0;
for (var i = 2; i < path.length; i += 2) {
d += dist2D(path[i - 2], path[i - 1], path[i], path[i + 1]);
}
if (d < this.minWidth) return;
var nbspace = text.split(" ").length - 1;
// Remove char for overflow
if (this.textOverflow != "visible") {
if (d < this.measureText(text).width + (text.length - 1 + nbspace) * letterPadding) {
var overflow = this.textOverflow == "ellipsis" ? "\u2026" : this.textOverflow || "";
var dt = overflow.length - 1;
do {
if (text[text.length - 1] === " ") nbspace--;
text = text.slice(0, -1);
} while (text && d < this.measureText(text + overflow).width + (text.length + dt + nbspace) * letterPadding);
text += overflow;
}
}
// Calculate start point
var start = 0;
switch (this.textJustify || this.textAlign) {
case true: // justify
case "center":
case "end":
case "right":
{
// Justify
if (this.textJustify) {
start = 0;
letterPadding = (d - this.measureText(text).width) / (text.length - 1 + nbspace);
}
// Text align
else {
start = d - this.measureText(text).width - (text.length + nbspace) * letterPadding;
if (this.textAlign == "center") start /= 2;
}
break;
}
}
// Do rendering
for (var t = 0; t < text.length; t++) {
var letter = text[t];
var wl = this.measureText(letter).width;
var p = pointAt(start + wl / 2);
this.save();
this.textAlign = "center";
this.translate(p[0], p[1]);
this.rotate(p[2]);
if (this.lineWidth > 0.1) this.strokeText(letter, 0, 0);
this.fillText(letter, 0, 0);
this.restore();
start += wl + letterPadding * (letter == " " ? 2 : 1);
}
};
})();
var N_TICKS = Math.pow(2, 3); // Force place ticks on bit boundaries
var MAX_OBJECTS_TO_ANIMATE_ZOOM = 90e3; // To prevent blocking interaction in canvas mode
var hilbert = Kapsule({
props: {
width: {},
margin: {
"default": 90
},
hilbertOrder: {
"default": 4
},
// 0-255 default
data: {
"default": []
},
rangeLabel: {
"default": 'name'
},
rangeLabelColor: {
"default": function _default() {
return 'black';
}
},
rangeColor: {},
rangePadding: {
"default": 0
},
rangePaddingAbsolute: {
"default": 0
},
valFormatter: {
"default": function _default(d) {
return d;
}
},
showValTooltip: {
"default": true,
triggerUpdate: false
},
showRangeTooltip: {
"default": true,
triggerUpdate: false
},
rangeTooltipContent: {
triggerUpdate: false
},
enableZoom: {
"default": true,
triggerUpdate: false
},
onRangeClick: {
triggerUpdate: false
},
onRangeHover: {
triggerUpdate: false
},
onPointerMove: {
triggerUpdate: false
},
onZoom: {
triggerUpdate: false
},
onZoomEnd: {
triggerUpdate: false
}
},
methods: {
focusOn: function focusOn(state, pos, length, transitionDuration) {
setTimeout(function () {
// async so that it runs after initialization
var N_SAMPLES = Math.pow(4, 2) + 1; // +1 to sample outside of bit boundaries
var pnts = [{
start: pos,
length: 1
}].concat(_toConsumableArray(_toConsumableArray(Array(N_SAMPLES).keys()).map(function (n) {
return {
start: pos + Math.round(length * (n + 1) / N_SAMPLES),
length: 1
};
})));
pnts.forEach(state.hilbert.layout);
// Figure out bounding box (in side bit units)
var tl = [Math.min.apply(Math, _toConsumableArray(pnts.map(function (p) {
return p.startCell[0];
}))), Math.min.apply(Math, _toConsumableArray(pnts.map(function (p) {
return p.startCell[1];
})))];
var br = [Math.max.apply(Math, _toConsumableArray(pnts.map(function (p) {
return p.startCell[0];
}))), Math.max.apply(Math, _toConsumableArray(pnts.map(function (p) {
return p.startCell[1];
})))];
var side = Math.max(br[0] - tl[0], br[1] - tl[1]);
var destination = {
x: -tl[0] * state.canvasWidth / side,
y: -tl[1] * state.canvasWidth / side,
k: Math.pow(2, state.hilbertOrder) / side
};
if (!transitionDuration) {
// no animation
state.zoom.transform(state.zoom.__baseElem, new ZoomTransform(destination.k, destination.x, destination.y));
} else {
var t = _objectSpread2({}, zoomTransform(state.zoom.__baseElem.node())); // { k, x, y }
gsap.to(t, Object.assign({
duration: transitionDuration / 1000,
ease: 'power1.inOut',
onUpdate: function onUpdate() {
state.zooming = true;
state.zoom.transform(state.zoom.__baseElem, new ZoomTransform(t.k, t.x, t.y));
},
onComplete: function onComplete() {
return state.zooming = false;
}
}, destination));
}
});
return this;
},
addMarker: function addMarker(state, pos, markerUrl, width, height, tooltipFormatter) {
if (state.useCanvas) return this; // not supported in canvas mode
tooltipFormatter = tooltipFormatter || function (d) {
return state.valFormatter(d.start);
};
var range = {
start: pos,
length: 1
};
state.hilbert.layout(range);
var marker = state.svg.select('.markers-canvas').append('svg:image').attr('xlink:href', markerUrl).attr('width', width).attr('height', height).attr('x', range.startCell[0] * range.cellWidth - width / 2).attr('y', range.startCell[1] * range.cellWidth - height / 2);
// Tooltip
var markerTooltip = d3Tip().attr('class', 'hilbert-tooltip').offset([-15, 0]).html(tooltipFormatter);
state.svg.call(markerTooltip);
marker.on('mouseover', function (ev, d) {
return markerTooltip.show(d);
});
marker.on('mouseout', function (ev, d) {
return markerTooltip.hide(d);
});
return this;
},
addHeatmap: function addHeatmap(pnts) {
if (state.useCanvas) return this; // not supported in canvas mode
var hmData = pnts.map(function (pnt) {
var hPnt = {
start: pnt,
length: 1
};
state.hilbert.layout(hPnt);
return {
x: Math.round(hPnt.startCell[0] * hPnt.cellWidth),
y: Math.round(hPnt.startCell[1] * hPnt.cellWidth),
value: 1
};
});
var svgBox = state.svg.node().getBoundingClientRect();
var hmElem = select(state.nodeElem).append('div').attr('class', 'hilbert-heatmap').style('top', svgBox.top + state.margin + 'px').style('left', svgBox.left + state.margin + 'px').append('div').style('width', state.canvasWidth + 'px').style('height', state.canvasWidth + 'px');
heatmap.create({
container: hmElem.node()
}).setData({
max: 100,
data: hmData
});
return this;
},
_refreshAxises: function _refreshAxises(state) {
// Adjust axises
var axises = state.axises;
var axisScaleX = state.zoomedAxisScaleX || state.axisScaleX;
var axisScaleY = state.zoomedAxisScaleY || state.axisScaleY;
axisScaleX.range([0, state.canvasWidth]);
axisScaleY.range([0, state.canvasWidth]);
state.axisScaleX.range([0, state.canvasWidth]);
state.axisScaleY.range([0, state.canvasWidth]);
axises.select('.axis-left').call(state.axisLeft.scale(axisScaleY));
axises.select('.axis-right').call(state.axisRight.scale(axisScaleY));
axises.select('.axis-top').call(state.axisTop.scale(axisScaleX)).selectAll('text').attr('x', 9).attr('dy', '.35em').attr('transform', 'rotate(-45)').style('text-anchor', 'start');
axises.select('.axis-bottom').call(state.axisBottom.scale(axisScaleX)).selectAll('text').attr('x', -9).attr('dy', '.35em').attr('transform', 'rotate(-45)').style('text-anchor', 'end');
return this;
}
},
stateInit: function stateInit() {
return {
hilbert: d3Hilbert().simplifyCurves(true),
defaultColorScale: scaleOrdinal(schemePaired),
zoomBox: [[0, 0], [N_TICKS, N_TICKS]],
axisScaleX: scaleLinear().domain([0, N_TICKS]),
axisScaleY: scaleLinear().domain([0, N_TICKS])
};
},
init: function init(el, state, _ref) {
var _this = this;
var _ref$useCanvas = _ref.useCanvas,
useCanvas = _ref$useCanvas === void 0 ? false : _ref$useCanvas,
_ref$zoomWithModKey = _ref.zoomWithModKey,
zoomWithModKey = _ref$zoomWithModKey === void 0 ? false : _ref$zoomWithModKey;
var isD3Selection = !!el && _typeof(el) === 'object' && !!el.node && typeof el.node === 'function';
var d3El = select(isD3Selection ? el.node() : el);
d3El.html(null); // Wipe DOM
d3El.attr('class', 'hilbert-chart');
// Dom
state.nodeElem = d3El.node();
state.useCanvas = useCanvas;
var svg = state.svg = create('svg').style('display', 'block');
d3El.node().appendChild(useCanvas || !zoomWithModKey ? svg.node() : new ScrollZoomClamp(svg.node()).node);
state.canvasWidth = state.width || Math.min(window.innerWidth, window.innerHeight) - state.margin * 2;
// zoom interaction
state.zoom = zoom().on('zoom', function (ev) {
if (!state.enableZoom && ev.sourceEvent) return;
var zoomTransform = ev.transform;
ev.sourceEvent && (state.zooming = true);
// Adjust axes
var xScale = state.zoomedAxisScaleX = zoomTransform.rescaleX(state.axisScaleX);
var yScale = state.zoomedAxisScaleY = zoomTransform.rescaleY(state.axisScaleY);
state.zoomBox[0] = [xScale.domain()[0], yScale.domain()[0]];
state.zoomBox[1] = [xScale.domain()[1], yScale.domain()[1]];
// Apply transform to chart
if (!useCanvas) {
// svg
state.hilbertCanvas.attr('transform', zoomTransform);
_this._refreshAxises();
} else {
// canvas
// reapply zoom transform on rerender (without recalculating layout)
state.skipRelayout = true;
requestAnimationFrame(state._rerender);
}
state.onZoom && state.onZoom(_objectSpread2({}, zoomTransform));
}).on('end', function (ev) {
if (!state.enableZoom && ev.sourceEvent) return;
if (ev.sourceEvent && ev.sourceEvent.type === 'mouseup' && !state.zooming) return; // ignore clicks without drag
ev.sourceEvent && (state.zooming = false); // end of interactive zoom
if (useCanvas) {
state.skipRelayout = true;
requestAnimationFrame(state._rerender);
}
state.onZoomEnd && state.onZoomEnd(_objectSpread2({}, ev.transform));
});
var hilbertCanvas;
if (!useCanvas) {
// svg mode
var defs = state.defs = svg.append('defs');
var zoomCanvas = state.zoomCanvas = svg.append('g');
hilbertCanvas = state.hilbertCanvas = zoomCanvas.append('g').attr('class', 'hilbert-canvas');
hilbertCanvas.append('rect').attr('class', 'zoom-trap').attr('x', 0).attr('y', 0).attr('opacity', 0);
hilbertCanvas.append('g').attr('class', 'ranges-canvas');
hilbertCanvas.append('g').attr('class', 'markers-canvas');
// Zoom binding
zoomCanvas.call(state.zoom).on("dblclick.zoom", null);
state.zoom.__baseElem = zoomCanvas; // Attach controlling elem for easy access
defs.append('clipPath').attr('id', 'canvas-cp').append('rect').attr('x', 0).attr('y', 0);
zoomCanvas.attr('clip-path', 'url(#canvas-cp)');
} else {
// Canvas mode
d3El.style('position', 'relative');
hilbertCanvas = state.hilbertCanvas = create('canvas').attr('class', 'hilbert-canvas').style('display', 'block');
var canvasWrapper = state.hilbertCanvasWrapper = zoomWithModKey ? select(new ScrollZoomClamp(hilbertCanvas.node()).node) : hilbertCanvas;
canvasWrapper.style('position', 'absolute');
d3El.node().appendChild(canvasWrapper.node());
// Zoom binding
hilbertCanvas.call(state.zoom).on("dblclick.zoom", null);
state.zoom.__baseElem = hilbertCanvas; // Attach controlling elem for easy access
}
// Tooltips
state.rangeTooltip = new Tooltip(d3El).offsetX(0).offsetY(-12);
var valTooltip = new Tooltip(d3El).offsetX('-10px').offsetY(20);
hilbertCanvas.on('mouseout', function () {
if (state.useCanvas && state.hoverD) {
state.hoverD = null;
state.onRangeHover && state.onRangeHover(null);
}
});
hilbertCanvas.on('click', function () {
return state.useCanvas && state.onRangeClick && state.hoverD && state.onRangeClick(state.hoverD);
});
hilbertCanvas.on('mousemove', function (ev) {
var _state$hilbert;
var coords = pointer(ev);
var c = coords.slice();
if (state.useCanvas) {
// Need to consider zoom on canvas
var zoomTransform$1 = zoomTransform(state.zoom.__baseElem.node());
c[0] -= zoomTransform$1.x;
c[0] /= zoomTransform$1.k;
c[1] -= zoomTransform$1.y;
c[1] /= zoomTransform$1.k;
}
var val = (_state$hilbert = state.hilbert).getValAtXY.apply(_state$hilbert, _toConsumableArray(c));
// Hover detection based on interval tree
if (state.useCanvas && (state.onRangeHover || state.showRangeTooltip)) {
var hoverDs = !state.intervalTree ? [] : state.intervalTree.search(val, val).map(function (d) {
return d.data;
}).sort(function (a, b) {
return a.length - b.length;
}); // prefer smaller cells
var hoverD = hoverDs.length ? hoverDs[0] : null;
if (hoverD !== state.hoverD) {
state.hoverD = hoverD;
if (hoverD && state.showRangeTooltip) {
var d = hoverD;
var tooltipContent = state.rangeTooltipContent ? accessorFn(state.rangeTooltipContent)(d)
// default tooltip
: "<b>".concat(accessorFn(state.rangeLabel)(d), "</b>: ").concat(state.valFormatter(d.start) + (d.length > 1 ? ' - ' + state.valFormatter(d.start + d.length - 1) : ''));
state.rangeTooltip.content(tooltipContent);
} else {
state.rangeTooltip.content(false);
}
hilbertCanvas.style('cursor', hoverD && state.onRangeClick ? 'pointer' : null);
state.onRangeHover && state.onRangeHover(hoverD || null);
}
}
if (state.showValTooltip || state.onPointerMove) {
state.showValTooltip && valTooltip.content(state.valFormatter(val));
state.onPointerMove && state.onPointerMove(val, ev);
}
});
// Setup axises
state.axisLeft = axisLeft().tickFormat(getTickFormatter(0));
state.axisRight = axisRight().tickFormat(getTickFormatter(1));
state.axisTop = axisTop().tickFormat(getTickFormatter(null, 0));
state.axisBottom = axisBottom().tickFormat(getTickFormatter(null, 1));
state.axises = state.svg.append('g').attr('class', 'hilbert-axises');
state.axises.append('g').attr('class', 'axis-left');
state.axises.append('g').attr('class', 'axis-right');
state.axises.append('g').attr('class', 'axis-top');
state.axises.append('g').attr('class', 'axis-bottom');
//
function getTickFormatter(xZoomBoxIdx, yZoomBoxIdx) {
return function (d) {
// Convert to canvas coordinates
var relD = d * state.canvasWidth / N_TICKS;
var zoomBox = state.zoomBox;
var nCells = Math.pow(2, state.hilbertOrder);
var xy = [xZoomBoxIdx != null ? state.axisScaleX(zoomBox[xZoomBoxIdx][0]) : relD, yZoomBoxIdx != null ? state.axisScaleY(zoomBox[yZoomBoxIdx][1]) : relD].map(function (coord) {
return (
// Prevent going off canvas
Math.min(coord, state.canvasWidth * (1 - 1 / nCells))
);
});
return state.valFormatter(state.hilbert.getValAtXY(xy[0], xy[1]));
};
}
},
update: function update(state, changedProps) {
var canvasWidth = state.canvasWidth = state.width || Math.min(window.innerWidth, window.innerHeight) - state.margin * 2;
var labelAcessor = accessorFn(state.rangeLabel);
var labelColorAccessor = accessorFn(state.rangeLabelColor);
var colorAccessor = state.rangeColor ? accessorFn(state.rangeColor) : function (d) {
return state.defaultColorScale(labelAcessor(d));
};
var _paddingAccessorFn = accessorFn(state.rangePadding);
var paddingAccessor = function paddingAccessor(d) {
return Math.max(0, Math.min(1, _paddingAccessorFn(d)));
}; // limit to [0, 1] range
var paddingAbsAccessor = accessorFn(state.rangePaddingAbsolute);
state.hilbert.order(state.hilbertOrder).canvasWidth(canvasWidth);
// resizing
state.svg.attr('width', canvasWidth + state.margin * 2).attr('height', canvasWidth + state.margin * 2);
state.zoom.scaleExtent([1, Math.pow(2, state.hilbertOrder)]).translateExtent([[0, 0], [canvasWidth, canvasWidth].map(function (w) {
return w + (state.useCanvas ? 0 : state.margin * 2);
})]); // fix margin glitch on svg
state.axises.attr('transform', "translate(".concat(state.margin, ", ").concat(state.margin, ")"));
state.axises.select('.axis-right').attr('transform', "translate(".concat(canvasWidth, ",0)"));
state.axises.select('.axis-bottom').attr('transform', "translate(0,".concat(canvasWidth, ")"));
this._refreshAxises();
if (!state.skipRelayout) {
// compute layout
state.data.forEach(state.hilbert.layout);
} else {
state.skipRelayout = false;
}
state.useCanvas ? canvasUpdate() : svgUpdate();
if (state.useCanvas && changedProps.hasOwnProperty('data')) {
// re-index interval tree when data changes for fast lookups
state.intervalTree = new IntervalTree();
(state.data || []).forEach(function (d) {
return state.intervalTree.insert({
low: d.start,
high: d.start + d.length,
data: d
});
});
}
//
function svgUpdate() {
// chart resizing
state.zoomCanvas.attr('transform', "translate(".concat(state.margin, ", ").concat(state.margin, ")"));
state.hilbertCanvas.select('.zoom-trap').attr('width', canvasWidth).attr('height', canvasWidth);
state.defs.select('#canvas-cp rect').attr('width', state.canvasWidth).attr('height', state.canvasWidth);
// D3 digest
var rangePaths = state.svg.select('.ranges-canvas').selectAll('.hilbert-segment').data(state.data.slice());
rangePaths.exit().remove();
var newPaths = rangePaths.enter().append('g').attr('class', 'hilbert-segment').attr('fill', labelColorAccessor).on('click', function (ev, d) {
return state.onRangeClick && state.onRangeClick(d);
}).on('mouseover', function (ev, d) {
if (state.showRangeTooltip) {
var tooltipContent = state.rangeTooltipContent ? accessorFn(state.rangeTooltipContent)(d)
// default tooltip
: "<b>".concat(accessorFn(state.rangeLabel)(d), "</b>: ").concat(state.valFormatter(d.start) + (d.length > 1 ? ' - ' + state.valFormatter(d.start + d.length - 1) : ''));
state.rangeTooltip.content(tooltipContent);
} else {
state.rangeTooltip.content(false);
}
state.onRangeHover && state.onRangeHover(d);
}).on('mouseout', function () {
state.onRangeHover && state.onRangeHover(null);
});
newPaths.append('path');
newPaths.append('text').attr('dy', 0.035).append('textPath')
// Label that follows the path contour
.attr('xlink:href', function (d) {
var id = 'textPath-' + Math.round(Math.random() * 1e10);
state.defs.append('path').attr('id', id).attr('d', getHilbertPath(d.pathVertices));
return '#' + id;
}).text(function (d) {
var MAX_TEXT_COMPRESSION = 8;
var name = labelAcessor(d);
return !d.pathVertices.length || name.length / (d.pathVertices.length + 1) > MAX_TEXT_COMPRESSION ? '' : name;
}).attr('textLength', function (d) {
var MAX_TEXT_EXPANSION = 0.4;
return Math.min(d.pathVertices.length, labelAcessor(d).length * MAX_TEXT_EXPANSION);
}).attr('startOffset', function (d) {
if (!d.pathVertices.length) return '0';
return (1 - select(this).attr('textLength') / d.pathVertices.length) / 2 * 100 + '%';
});
// Ensure propagation of data binding into sub-elements
rangePaths.select('path');
rangePaths = rangePaths.merge(newPaths);
rangePaths.selectAll('path') //.transition()
.attr('d', function (d) {
return getHilbertPath(d.pathVertices);
}).style('stroke', colorAccessor).style('stroke-width', function (d) {
return Math.max(0, 1 - paddingAccessor(d) - paddingAbsAccessor(d) / d.cellWidth);
}).style('cursor', state.onRangeClick ? 'pointer' : null);
rangePaths.attr('transform', function (d) {
return "scale(".concat(d.cellWidth, ") translate(").concat(d.startCell[0] + .5, ",").concat(d.startCell[1] + .5, ")");
});
rangePaths.selectAll('text').attr('font-size', function (d) {
return Math.max(0, Math.min.apply(Math, [0.25,
// Max 25% of path height
(d.pathVertices.length + 1 - paddingAccessor(d) - paddingAbsAccessor(d) / d.cellWidth) * 0.25,
// Max 25% path length
canvasWidth / d.cellWidth * 0.03 // Max 3% of canvas size
]));
}).attr('textLength', function (d) {
var MAX_TEXT_EXPANSION;
var name = labelAcessor(d);
if (d.pathVertices.length) {
// Include it on text element for Firefox support
MAX_TEXT_EXPANSION = 0.4;
return Math.min(d.pathVertices.length, name.length * MAX_TEXT_EXPANSION);
} else {
MAX_TEXT_EXPANSION = 0.15;
return Math.max(0, Math.min(0.95 * (1 - paddingAccessor(d) - paddingAbsAccessor(d) / d.cellWidth), name.length * MAX_TEXT_EXPANSION));
}
}).filter(function (d) {
return !d.pathVertices.length;
})
// Those with no path (plain square)
.text(function (d) {
var MAX_TEXT_COMPRESSION = 10;
var name = labelAcessor(d);
return name.length > MAX_TEXT_COMPRESSION ? '' : name;
}).attr('text-anchor', 'middle');
//
function getHilbertPath(vertices) {
if (!vertices.every(function (v) {
return v === 'L';
})) {
var path = 'M0 0L0 0';
vertices.forEach(function (vert) {
switch (vert) {
case 'U':
path += 'v-1';
break;
case 'D':
path += 'v1';
break;
case 'L':
path += 'h-1';
break;
case 'R':
path += 'h1';
break;
}
});
return path;
} else {
// reverse path (to prevent upside down text)
var _path = '';
var lastPos = [0, 0];
vertices.slice().reverse().forEach(function (vert) {
switch (vert) {
case 'U':
_path += 'v1';
lastPos[1] -= 1;
break;
case 'D':
_path += 'v-1';
lastPos[1] += 1;
break;
case 'L':
_path += 'h1';
lastPos[0] -= 1;
break;
case 'R':
_path += 'h-1';
lastPos[0] += 1;
break;
}
});
return "M".concat(lastPos.join(' '), "L").concat(lastPos.join(' ')).concat(_path);
}
}
}
function canvasUpdate() {
var pxScale = window.devicePixelRatio; // 2 on retina displays
// canvas resize (and clear)
state.hilbertCanvasWrapper.style('top', "".concat(state.margin, "px")).style('left', "".concat(state.margin, "px"));
state.hilbertCanvas.style('width', "".concat(canvasWidth, "px")).style('height', "".concat(canvasWidth, "px"));
[state.hilbertCanvas.node()].forEach(function (canvasEl) {
// Memory size (scaled to avoid blurriness)
canvasEl.width = state.canvasWidth * pxScale;
canvasEl.height = state.canvasWidth * pxScale;
});
var zoomTransform$1 = zoomTransform(state.zoom.__baseElem.node());
var viewWindow = {
// in px
x: -zoomTransform$1.x / zoomTransform$1.k,
y: -zoomTransform$1.y / zoomTransform$1.k,
len: canvasWidth / zoomTransform$1.k
};
var ctx = state.hilbertCanvas.node().getContext('2d');
ctx.clearRect(0, 0, canvasWidth, canvasWidth);
// Apply zoom transform (respecting pxScale)
ctx.translate(zoomTransform$1.x * pxScale, zoomTransform$1.y * pxScale);
ctx.scale(zoomTransform$1.k * pxScale, zoomTransform$1.k * pxScale);
var dataInView = state.data.filter(function (d) {
if (d.pathVertices.length) return true; // Can't judge multi-cell
var w = d.cellWidth;
var _d$startCell$map = d.startCell.map(function (c) {
return c * w;
}),
_d$startCell$map2 = _slicedToArray(_d$startCell$map, 2),
x = _d$startCell$map2[0],
y = _d$startCell$map2[1];
// cell out of view, no need to draw
return !(x > viewWindow.x + viewWindow.len || x + w < viewWindow.x || y > viewWindow.y + viewWindow.len || y + w < viewWindow.y);
});
if (state.zooming && dataInView.length > MAX_OBJECTS_TO_ANIMATE_ZOOM) {
var getBitBoundary = function getBitBoundary(n) {
if (!n) return 0;
var cnt = 0;
while (!(n % 1)) {
n /= 2;
cnt++;
}
return cnt;
};
// prefer larger objects on a bit boundary
var keepIdxs = new Set(dataInView.map(function (d, idx) {
return {
idx: idx,
size: d.cellWidth,
bitBoundary: getBitBoundary(d.start)
};
}).sort(function (a, b) {
return b.size - a.size || b.bitBoundary - a.bitBoundary;
}).slice(0, MAX_OBJECTS_TO_ANIMATE_ZOOM).map(function (_ref2) {
var idx = _ref2.idx;
return idx;
}));
dataInView = dataInView.filter(function (_, idx) {
return keepIdxs.has(idx);
});
}
var _loop = function _loop() {
var d = dataInView[i];
var w = d.cellWidth;
var scaledW = w * zoomTransform$1.k;
var relPadding = paddingAccessor(d);
var absPadding = paddingAbsAccessor(d);
var padding = Math.min(w * 0.5, relPadding * w + absPadding / zoomTransform$1.k);
if (d.pathVertices.length === 0) {
// single cell -> draw a square
var _d$startCell$map3 = d.startCell.map(function (c) {
return c * w;
}),
_d$startCell$map4 = _slicedToArray(_d$startCell$map3, 2),
x = _d$startCell$map4[0],
y = _d$startCell$map4[1];
var rectW = w - padding;
ctx.fillStyle = colorAccessor(d);
var ctxs = [ctx];
ctxs.forEach(function (ctx) {
return ctx.fillRect(x + padding / 2, y + padding / 2, rectW, rectW);
});
if (scaledW > 12) {
// Hide labels on small square cells
var name = labelAcessor(d);
var fontSize = Math.min(20,
// absolute
scaledW * 0.25,
// Max 25% of cell height
(scaledW - padding) / name.length * 1.5 // Fit text length
) / zoomTransform$1.k;
ctx.font = "".concat(fontSize, "px Sans-Serif");
ctx.fillStyle = labelColorAccessor(d);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText.apply(ctx, [name].concat(_toConsumableArray([x, y].map(function (c) {
return c + w / 2;
}))));
}
} else {
var _ref5;
// draw path (with textpath labels)
var _d$startCell$map5 = d.startCell.map(function (c) {
return c * w + w / 2;
}),
_d$startCell$map6 = _slicedToArray(_d$startCell$map5, 2),
_x = _d$startCell$map6[0],
_y = _d$startCell$map6[1];
var path = [[_x, _y]].concat(_toConsumableArray(d.pathVertices.map(function (vert) {
switch (vert) {
case 'U':
_y -= w;
break;
case 'D':
_y += w;
break;
case 'L':
_x -= w;
break;
case 'R':
_x += w;
break;
}
return [_x, _y];
})));
ctx.strokeStyle = colorAccessor(d);
var _ctxs = [ctx];
_ctxs.forEach(function (ctx) {
ctx.lineWidth = w - padding;
ctx.lineCap = 'square';
ctx.beginPath();
ctx.moveTo.apply(ctx, _toConsumableArray(path[0]));
path.slice(1).forEach(function (_ref3) {
var _ref4 = _slicedToArray(_ref3, 2),
x = _ref4[0],
y = _ref4[1];
return ctx.lineTo(x, y);
});
ctx.stroke();
});
// extend path extremities to cell edges for textpath
var pathStart = path[0].map(function (c, idx) {
return c - (path[1][idx] - c) / 2;
});
var pathEnd = path[path.length - 1].map(function (c, idx) {
return c - (path[path.length - 2][idx] - c) / 2;
});
path[0] = pathStart;
path[path.length - 1] = pathEnd;
var _name = labelAcessor(d);
var _fontSize = Math.min(20,
// absolute
scaledW * 0.4,
// Max 40% of cell height
(scaledW * path.length - padding) / _name.length * 1.2 // Fit text length
) / zoomTransform$1.k;
ctx.font = "".concat(_fontSize, "px Sans-Serif");
ctx.fillStyle = labelColorAccessor(d);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.lineWidth = 0.01; // no stroke outline
// flip text if upside down (left oriented paths)
ctx.textPath(_name, (_ref5 = []).concat.apply(_ref5, _toConsumableArray(d.pathVertices.every(function (v) {
return v === 'L';
}) ? path.slice().reverse() : path)));
}
};
for (var i = 0, n = dataInView.length; i < n; i++) {
_loop();
}
}
}
});
export { hilbert as default };