UNPKG

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
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 };