From 5398d35a1e449cea33ce9a12ff9b64cc60d5c67e Mon Sep 17 00:00:00 2001 From: David Boyd Date: Sat, 9 May 2020 23:34:31 -0700 Subject: [PATCH 1/7] - Added ZoomableGeo component --- src/components/ZoomableGeo.js | 56 ++++++++++++++++++++ src/components/useZoomGeo.js | 98 +++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 src/components/ZoomableGeo.js create mode 100644 src/components/useZoomGeo.js diff --git a/src/components/ZoomableGeo.js b/src/components/ZoomableGeo.js new file mode 100644 index 0000000..ac0e298 --- /dev/null +++ b/src/components/ZoomableGeo.js @@ -0,0 +1,56 @@ + +import React, { useContext } from "react" +import PropTypes from "prop-types" + +import { MapContext } from "./MapProvider" +import useZoomGeo from "./useZoomGeo" + +const ZoomableGeo = ({ + center = null, + fitMargin = 0.1, + duration = 750, + minZoom = 1, + maxZoom = 8, + onMoveStart, + onMove, + onMoveEnd, + className, + ...restProps +}) => { + const { width, height } = useContext(MapContext) + + const { + mapRef, + transformString, + style + } = useZoomGeo({ + center, + fitMargin, + duration, + onMoveStart, + onMove, + onMoveEnd, + scaleExtent: [minZoom, maxZoom], + }); + + return ( + + + + + ) +} + +ZoomableGeo.propTypes = { + center: PropTypes.object, + fitMargin: PropTypes.number, + duration: PropTypes.number, + minZoom: PropTypes.number, + maxZoom: PropTypes.number, + onMoveStart: PropTypes.func, + onMove: PropTypes.func, + onMoveEnd: PropTypes.func, + className: PropTypes.string, +} + +export default ZoomableGeo diff --git a/src/components/useZoomGeo.js b/src/components/useZoomGeo.js new file mode 100644 index 0000000..d753a81 --- /dev/null +++ b/src/components/useZoomGeo.js @@ -0,0 +1,98 @@ + +import { useEffect, useRef, useState, useContext } from "react" +import { zoom as d3Zoom, zoomIdentity } from "d3-zoom" +import { select, event as d3Event } from "d3-selection" + +import { MapContext } from "./MapProvider" +import { getCoords } from "../utils" + +export default function useZoomGeo({ + center, + fitMargin, + duration, + onMoveStart, + onMove, + onMoveEnd, + scaleExtent = [1, 8], +}) { + const { width, height, projection, path } = useContext(MapContext) + + const [position, setPosition] = useState({ x: 0, y: 0, k: 1 }) + const mapRef = useRef() + const zoomRef = useRef() + const transformRef = useRef() + + const [a, b] = [[-Infinity, -Infinity], [Infinity, Infinity]]; + const [a1, a2] = a + const [b1, b2] = b + const [minZoom, maxZoom] = scaleExtent + + useEffect(() => { + const svg = select(mapRef.current) + + function handleZoomStart() { + if (!onMoveStart) return + onMoveStart({ coordinates: projection.invert(getCoords(width, height, d3Event.transform)), zoom: d3Event.transform.k }, d3Event) + } + + function handleZoom() { + const {transform, sourceEvent} = d3Event + setPosition({ x: transform.x, y: transform.y, k: transform.k, dragging: sourceEvent }) + if (!onMove) return + onMove({ x: transform.x, y: transform.y, k: transform.k, dragging: sourceEvent }, d3Event) + } + + function handleZoomEnd() { + transformRef.current = d3Event.transform; + const [x, y] = projection.invert(getCoords(width, height, d3Event.transform)) + if (!onMoveEnd) return + onMoveEnd({ coordinates: [x, y], zoom: d3Event.transform.k }, d3Event) + } + + const zoom = d3Zoom() + .scaleExtent([minZoom, maxZoom]) + .translateExtent([[a1, a1], [b1, b2]]) + .on("start", handleZoomStart) + .on("zoom", handleZoom) + .on("end", handleZoomEnd) + + zoomRef.current = zoom + + // Prevent the default zooming behaviors + svg.call(zoom) + .on("mousedown.zoom", null) + .on("dblclick.zoom", null) + .on("wheel.zoom", null) + + }, [width, height, a1, a2, b1, b2, minZoom, maxZoom, projection, onMoveStart, onMove, onMoveEnd]) + + // Zoom to the specfied geometry so that it's centered and perfectly bound + useEffect(() => { + const svg = select(mapRef.current) + const transform = zoomRef.current.transform + + if (center) { + const [[x0, y0], [x1, y1]] = path.bounds(center); + svg.transition().duration(duration).call( + transform, + zoomIdentity + .translate(width / 2, height / 2) + .scale(Math.min(maxZoom, (1 - fitMargin) / Math.max((x1 - x0) / width, (y1 - y0) / height))) + .translate(-(x0 + x1) / 2, -(y0 + y1) / 2), + ); + } else { + svg.transition().duration(duration).call( + transform, + zoomIdentity, + transformRef.current ? transformRef.current.invert([width / 2, height / 2]) : null + ); + } + }, [center]) + + return { + mapRef, + position, + transformString: `translate(${position.x} ${position.y}) scale(${position.k})`, + style: { strokeWidth: 1 / position.k }, + } +} From 888273628860d685edc6828bf8fd58ec5fff7572 Mon Sep 17 00:00:00 2001 From: David Boyd Date: Sun, 10 May 2020 17:08:57 -0700 Subject: [PATCH 2/7] - Added export for components - Renamed properties --- src/components/ZoomableGeo.js | 12 ++++++------ src/components/useZoomGeo.js | 15 ++++++++------- src/index.js | 2 ++ 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/components/ZoomableGeo.js b/src/components/ZoomableGeo.js index ac0e298..c879a24 100644 --- a/src/components/ZoomableGeo.js +++ b/src/components/ZoomableGeo.js @@ -6,8 +6,8 @@ import { MapContext } from "./MapProvider" import useZoomGeo from "./useZoomGeo" const ZoomableGeo = ({ - center = null, - fitMargin = 0.1, + bounds = null, + boundsMargin = 0.1, duration = 750, minZoom = 1, maxZoom = 8, @@ -24,8 +24,8 @@ const ZoomableGeo = ({ transformString, style } = useZoomGeo({ - center, - fitMargin, + bounds, + boundsMargin, duration, onMoveStart, onMove, @@ -42,8 +42,8 @@ const ZoomableGeo = ({ } ZoomableGeo.propTypes = { - center: PropTypes.object, - fitMargin: PropTypes.number, + bounds: PropTypes.object, + boundsMargin: PropTypes.number, duration: PropTypes.number, minZoom: PropTypes.number, maxZoom: PropTypes.number, diff --git a/src/components/useZoomGeo.js b/src/components/useZoomGeo.js index d753a81..29a845b 100644 --- a/src/components/useZoomGeo.js +++ b/src/components/useZoomGeo.js @@ -1,4 +1,5 @@ - +// Converted from this D3 demo: +// https://observablehq.com/@d3/zoom-to-bounding-box import { useEffect, useRef, useState, useContext } from "react" import { zoom as d3Zoom, zoomIdentity } from "d3-zoom" import { select, event as d3Event } from "d3-selection" @@ -7,8 +8,8 @@ import { MapContext } from "./MapProvider" import { getCoords } from "../utils" export default function useZoomGeo({ - center, - fitMargin, + bounds, + boundsMargin, duration, onMoveStart, onMove, @@ -71,13 +72,13 @@ export default function useZoomGeo({ const svg = select(mapRef.current) const transform = zoomRef.current.transform - if (center) { - const [[x0, y0], [x1, y1]] = path.bounds(center); + if (bounds) { + const [[x0, y0], [x1, y1]] = path.bounds(bounds); svg.transition().duration(duration).call( transform, zoomIdentity .translate(width / 2, height / 2) - .scale(Math.min(maxZoom, (1 - fitMargin) / Math.max((x1 - x0) / width, (y1 - y0) / height))) + .scale(Math.min(maxZoom, (1 - boundsMargin) / Math.max((x1 - x0) / width, (y1 - y0) / height))) .translate(-(x0 + x1) / 2, -(y0 + y1) / 2), ); } else { @@ -87,7 +88,7 @@ export default function useZoomGeo({ transformRef.current ? transformRef.current.invert([width / 2, height / 2]) : null ); } - }, [center]) + }, [bounds]) return { mapRef, diff --git a/src/index.js b/src/index.js index 4554dd7..2ba314b 100644 --- a/src/index.js +++ b/src/index.js @@ -3,10 +3,12 @@ export { default as ComposableMap } from "./components/ComposableMap" export { default as Geographies } from "./components/Geographies" export { default as Geography } from "./components/Geography" export { default as Graticule } from "./components/Graticule" +export { default as ZoomableGeo } from "./components/ZoomableGeo" export { default as ZoomableGroup } from "./components/ZoomableGroup" export { default as Sphere } from "./components/Sphere" export { default as Marker } from "./components/Marker" export { default as Line } from "./components/Line" export { default as Annotation } from "./components/Annotation" export { default as useGeographies } from "./components/useGeographies" +export { default as useZoomGeo } from "./components/useZoomGeo" export { default as useZoomPan } from "./components/useZoomPan" From 0116b5d8e3d854ef04ccbd2b83f4df34a7bb7107 Mon Sep 17 00:00:00 2001 From: David Boyd Date: Sun, 10 May 2020 22:38:42 -0700 Subject: [PATCH 3/7] Update README.md --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 288d278..e1fc6f0 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,34 @@ Create beautiful SVG maps in react with d3-geo and topojson using a declarative Read the [docs](https://www.react-simple-maps.io/docs/getting-started/), or check out the [examples](https://www.react-simple-maps.io/examples/). +This fork contains an additional component. + +##### +ZoomableGeo is the result of porting this [example](https://observablehq.com/@d3/zoom-to-bounding-box) to `react-simple-maps`. Given a GeoJSON feature, it will zoom to fill the screen with the bounding box of that feature. + +##### Props + `bounds` GeoJSON feature object or null, for entire geography + `boundsMargin` How much margin around feature bounds (0.05 is 5%) + `duration` How long to animate between features + `minZoom` Minimum zoom level + `maxZoom` Maximum zoom level + `onZoomStart` + `onZoom` + `onZoomEnd` + +#### Example +```jsx + + + ... + + +``` + ### Why `React-simple-maps` aims to make working with svg maps in react easier. It handles tasks such as panning, zooming and simple rendering optimization, and takes advantage of parts of [d3-geo](https://github.com/d3/d3-geo) and topojson-client instead of relying on the entire d3 library. From 2324a10f08dc7ca5a8b0751bf1aa640b731cf2bf Mon Sep 17 00:00:00 2001 From: David Boyd Date: Sun, 10 May 2020 22:49:16 -0700 Subject: [PATCH 4/7] Update README.md --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e1fc6f0..a8a0c79 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,14 @@ This fork contains an additional component. ZoomableGeo is the result of porting this [example](https://observablehq.com/@d3/zoom-to-bounding-box) to `react-simple-maps`. Given a GeoJSON feature, it will zoom to fill the screen with the bounding box of that feature. ##### Props - `bounds` GeoJSON feature object or null, for entire geography - `boundsMargin` How much margin around feature bounds (0.05 is 5%) - `duration` How long to animate between features - `minZoom` Minimum zoom level - `maxZoom` Maximum zoom level - `onZoomStart` - `onZoom` - `onZoomEnd` +* `bounds` GeoJSON feature object or null, for entire geography +* `boundsMargin` How much margin around feature bounds (0.05 is 5%) +* `duration` How long to animate between features +* `minZoom` Minimum zoom level +* `maxZoom` Maximum zoom level +* `onZoomStart` +* `onZoom` +* `onZoomEnd` #### Example ```jsx From 8160799f9f6732b70219a54fdc4354eca98eb61c Mon Sep 17 00:00:00 2001 From: David Boyd Date: Sun, 10 May 2020 22:50:47 -0700 Subject: [PATCH 5/7] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a8a0c79..cf5f0b9 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Read the [docs](https://www.react-simple-maps.io/docs/getting-started/), or chec This fork contains an additional component. -##### -ZoomableGeo is the result of porting this [example](https://observablehq.com/@d3/zoom-to-bounding-box) to `react-simple-maps`. Given a GeoJSON feature, it will zoom to fill the screen with the bounding box of that feature. +##### `` +`ZoomableGeo` is the result of porting this [example](https://observablehq.com/@d3/zoom-to-bounding-box) to `react-simple-maps`. Given a GeoJSON feature, it will zoom to fill the screen with the bounding box of that feature. ##### Props * `bounds` GeoJSON feature object or null, for entire geography From 0e2c75860f3a0c484c1a8aa7de34018eb8f89850 Mon Sep 17 00:00:00 2001 From: David Boyd Date: Sun, 10 May 2020 23:02:36 -0700 Subject: [PATCH 6/7] - Back to normal readme --- README.md | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/README.md b/README.md index cf5f0b9..288d278 100644 --- a/README.md +++ b/README.md @@ -6,34 +6,6 @@ Create beautiful SVG maps in react with d3-geo and topojson using a declarative Read the [docs](https://www.react-simple-maps.io/docs/getting-started/), or check out the [examples](https://www.react-simple-maps.io/examples/). -This fork contains an additional component. - -##### `` -`ZoomableGeo` is the result of porting this [example](https://observablehq.com/@d3/zoom-to-bounding-box) to `react-simple-maps`. Given a GeoJSON feature, it will zoom to fill the screen with the bounding box of that feature. - -##### Props -* `bounds` GeoJSON feature object or null, for entire geography -* `boundsMargin` How much margin around feature bounds (0.05 is 5%) -* `duration` How long to animate between features -* `minZoom` Minimum zoom level -* `maxZoom` Maximum zoom level -* `onZoomStart` -* `onZoom` -* `onZoomEnd` - -#### Example -```jsx - - - ... - - -``` - ### Why `React-simple-maps` aims to make working with svg maps in react easier. It handles tasks such as panning, zooming and simple rendering optimization, and takes advantage of parts of [d3-geo](https://github.com/d3/d3-geo) and topojson-client instead of relying on the entire d3 library. From 0bb0411c26e17d2e5523faabf739b69206c58fd3 Mon Sep 17 00:00:00 2001 From: David Boyd Date: Wed, 13 May 2020 17:02:48 -0700 Subject: [PATCH 7/7] - Fixed React lint warning --- src/components/useZoomGeo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/useZoomGeo.js b/src/components/useZoomGeo.js index 29a845b..2635333 100644 --- a/src/components/useZoomGeo.js +++ b/src/components/useZoomGeo.js @@ -88,7 +88,7 @@ export default function useZoomGeo({ transformRef.current ? transformRef.current.invert([width / 2, height / 2]) : null ); } - }, [bounds]) + }, [bounds, boundsMargin, duration, height, maxZoom, path, width]); return { mapRef,