diff --git a/examples/ipv4-prefix-viz/index.html b/examples/ipv4-prefix-viz/index.html index 32bef26..4dde1ce 100644 --- a/examples/ipv4-prefix-viz/index.html +++ b/examples/ipv4-prefix-viz/index.html @@ -31,14 +31,14 @@ }); } - HilbertChart({ useCanvas: true }) + const chart = HilbertChart({ useCanvas: true }) .hilbertOrder(32 / 2) .data(blockData) .valFormatter(ipFormatter) .rangeLabel(() => '') .rangeColor('color') .rangeTooltipContent(prefixFormatter) - .onRangeClick(console.log) + .onRangeClick(d => chart.focusOn(d.start, d.length, 3000)) (document.getElementById('ipv4-chart')); function ipFormatter(d) { diff --git a/src/hilbert.js b/src/hilbert.js index b157ee0..21a8ade 100644 --- a/src/hilbert.js +++ b/src/hilbert.js @@ -13,6 +13,7 @@ import accessorFn from 'accessor-fn'; import ColorTracker from 'canvas-color-tracker'; const N_TICKS = Math.pow(2, 3); // Force place ticks on bit boundaries +const MAX_OBJECTS_TO_ANIMATE_ZOOM = 400e3; // To prevent blocking interaction in canvas mode export default Kapsule({ props: { @@ -207,13 +208,21 @@ export default Kapsule({ this._refreshAxises(); } else { // canvas // reapply zoom transform on rerender (without recalculating layout) + state.zooming = true; state.skipRelayout = true; requestAnimationFrame(state._rerender); } state.onZoom && state.onZoom({ ...zoomTransform }); }) - .on('end', ev => state.onZoomEnd && state.onZoomEnd({ ...ev.transform })); + .on('end', ev => { + if (useCanvas) { + state.zooming = false; + state.skipRelayout = true; + requestAnimationFrame(state._rerender); + } + state.onZoomEnd && state.onZoomEnd({ ...ev.transform }) + }); let hilbertCanvas; if (!useCanvas) { // svg mode @@ -577,14 +586,25 @@ export default Kapsule({ ctx.scale(zoomTransform.k * pxScale, zoomTransform.k * pxScale); }); + const dataInView = state.data.filter(d => { + if (d.pathVertices.length) return true; // Can't judge multi-cell + + const w = d.cellWidth; + const [x, y] = d.startCell.map(c => c * w); + // 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); + }); + + const n = dataInView.length; + if (state.zooming && n > MAX_OBJECTS_TO_ANIMATE_ZOOM) return; // don't animate zoom for a lot of objects + // indexed blocks for rgb lookup - const n = state.data.length; state.colorTracker = new ColorTracker( n < 250e3 ? 6 : n < 500e3 ? 5 : n < 1e6 ? 4 : n < 2e6 ? 3 : n < 4e6 ? 2 : n < 8e6 ? 1 : 0 ); for (let i = 0; i < n ; i++) { - const d = state.data[i]; + const d = dataInView[i]; const w = d.cellWidth; const scaledW = w * zoomTransform.k; @@ -593,16 +613,12 @@ export default Kapsule({ if (d.pathVertices.length === 0) { // single cell -> draw a square const [x, y] = d.startCell.map(c => c * w); - if (x > viewWindow.x + viewWindow.len || (x + w) < viewWindow.x || y > viewWindow.y + viewWindow.len || (y + w) < viewWindow.y) { - continue; // cell out of view, no need to draw - } - const rectPadding = relPadding * w / 2; const rectW = w * (1 - relPadding); ctx.fillStyle = colorAccessor(d); const ctxs = [ctx]; - if (scaledW >= 1) { // don't bother registering sub-pixel squares on shadow canvas, it kills performance + if (!state.zooming && scaledW >= 1) { // don't bother registering sub-pixel squares on shadow canvas, it kills performance shadowCtx.fillStyle = state.colorTracker.register(d); ctxs.push(shadowCtx); } @@ -634,8 +650,12 @@ export default Kapsule({ })]; ctx.strokeStyle = colorAccessor(d); - shadowCtx.strokeStyle = state.colorTracker.register(d); - [ctx, shadowCtx].forEach(ctx => { + const ctxs = [ctx]; + if (!state.zooming) { + shadowCtx.strokeStyle = state.colorTracker.register(d); + ctxs.push(shadowCtx); + } + ctxs.forEach(ctx => { ctx.lineWidth = w * (1 - relPadding); ctx.lineCap = 'square'; ctx.beginPath();