// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ Copyright (c) 2017, the Perspective Authors. ┃ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ // ┃ This file is part of the Perspective library, distributed under the terms ┃ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import * as d3 from "d3"; import { getOrCreateElement } from "../utils/utils"; import template from "../../html/zoom-controls.html"; export default () => { let chart = null; let settings = null; let xScale = null; let xCopy = null; let yScale = null; let yCopy = null; let bound = false; let canvas = false; let onChange: (args: any) => void = () => {}; function zoomableChart(selection) { const chartPlotArea = `d3fc-${canvas ? "canvas" : "svg"}.plot-area`; if (xScale || yScale) { const dateAxis = xCopy && xCopy.domain()[0] instanceof Date; const zoom = d3.zoom().on("zoom", (event) => { const { transform } = event; settings.zoom = { k: transform.k, x: transform.x, y: transform.y, }; applyTransform(transform); selection.call(chart); const noZoom = transform.k === 1 && transform.x === 0 && transform.y === 0; const zoomControls = getZoomControls(selection).style( "display", noZoom ? "none" : "", ); zoomControls .select("#zoom-reset") .on("click", () => selection .select(chartPlotArea) .call(zoom.transform, d3.zoomIdentity), ); const oneYear = zoomControls .select("#one-year") .style("display", dateAxis ? "" : "none"); const sixMonths = zoomControls .select("#six-months") .style("display", dateAxis ? "" : "none"); const oneMonth = zoomControls .select("#one-month") .style("display", dateAxis ? "" : "none"); if (dateAxis) { const dateClick = (endCalculation) => () => { const start = new Date(xScale.domain()[0]); const end = new Date(start); endCalculation(start, end); const xRange = xCopy.range(); const k = (xRange[1] - xRange[0]) / (xCopy(end) - xCopy(start)); const x = -xCopy(start) * k; let y = 0; if (yScale) { const yMiddle = yScale.domain().reduce((a, b) => a + b) / 2; y = -yCopy(yMiddle) * k + yScale(yMiddle); } selection .select(chartPlotArea) .call( zoom.transform, d3.zoomIdentity.translate(x, y).scale(k), ); }; oneYear.on( "click", dateClick((start, end) => end.setYear(start.getFullYear() + 1), ), ); sixMonths.on( "click", dateClick((start, end) => end.setMonth(start.getMonth() + 6), ), ); oneMonth.on( "click", dateClick((start, end) => end.setMonth(start.getMonth() + 1), ), ); } }); const oldDecorate = chart.decorate(); chart.decorate((sel, data) => { oldDecorate(sel, data); if (!bound) { bound = true; // add the zoom interaction on the enter selection const plotArea = sel.select(chartPlotArea); const device_pixel_factor = canvas ? window.devicePixelRatio : 1; plotArea .on("measure.zoom-range", (event) => { if (xCopy) xCopy.range([ 0, event.detail.width / device_pixel_factor, ]); if (yCopy) yCopy.range([ 0, event.detail.height / device_pixel_factor, ]); if (settings.zoom) { const initialTransform = d3.zoomIdentity .translate(settings.zoom.x, settings.zoom.y) .scale(settings.zoom.k); plotArea.call(zoom.transform, initialTransform); } }) .call(zoom); } }); } selection.call(chart); } zoomableChart.chart = (...args) => { if (!args.length) { return chart; } chart = args[0]; return zoomableChart; }; zoomableChart.settings = (...args) => { if (!args.length) { return settings; } settings = args[0]; return zoomableChart; }; zoomableChart.xScale = (...args) => { if (!args.length) { return xScale; } xScale = zoomableScale(args[0]); xCopy = xScale ? xScale.copy() : null; return zoomableChart; }; zoomableChart.yScale = (...args) => { if (!args.length) { return yScale; } yScale = zoomableScale(args[0]); yCopy = yScale ? yScale.copy() : null; if (yCopy) { const yDomain = yCopy.domain(); yCopy.domain([yDomain[1], yDomain[0]]); } return zoomableChart; }; zoomableChart.canvas = (...args) => { if (!args.length) { return canvas; } canvas = args[0]; return zoomableChart; }; zoomableChart.onChange = (...args) => { if (!args.length) { return onChange; } onChange = args[0]; return zoomableChart; }; const applyTransform = (transform) => { const changeArgs = { ...transform }; if (xScale) { xScale.domain(transform.rescaleX(xCopy).domain()); changeArgs.xDomain = xScale.domain(); } if (yScale) { const yZoomDomain = transform.rescaleY(yCopy).domain(); yScale.domain([yZoomDomain[1], yZoomDomain[0]]); changeArgs.yDomain = yScale.domain(); } onChange(changeArgs); }; const getZoomControls = (container) => getOrCreateElement(container, ".zoom-controls", () => container .append("div") .attr("class", "zoom-controls") .style("display", "none") .html(template), ); const zoomableScale = (scale) => { if (scale && scale.nice) { return scale; } return null; }; return zoomableChart; };