├── .babelrc
├── .gitignore
├── .npmignore
├── .prettierrc.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── components
│ ├── Annotation.js
│ ├── ComposableMap.js
│ ├── Geographies.js
│ ├── Geography.js
│ ├── Graticule.js
│ ├── Line.js
│ ├── MapProvider.js
│ ├── Marker.js
│ ├── Sphere.js
│ ├── ZoomPanProvider.js
│ ├── ZoomableGroup.js
│ ├── useGeographies.js
│ └── useZoomPan.js
├── index.js
└── utils.js
└── tests
└── utils.spec.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"],
3 | "plugins": ["@babel/transform-react-jsx", "@babel/plugin-proposal-object-rest-spread"]
4 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .next
3 | .DS_Store
4 | node_modules
5 | dist
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 |
2 | .next
3 | .DS_Store
4 | node_modules
5 | examples
6 | src
7 | topojson-maps
8 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "bracketSpacing": true,
4 | "embeddedLanguageFormatting": "auto",
5 | "htmlWhitespaceSensitivity": "css",
6 | "insertPragma": false,
7 | "jsxBracketSameLine": false,
8 | "jsxSingleQuote": false,
9 | "printWidth": 80,
10 | "proseWrap": "preserve",
11 | "quoteProps": "preserve",
12 | "requirePragma": false,
13 | "semi": false,
14 | "singleQuote": false,
15 | "tabWidth": 2,
16 | "trailingComma": "es5",
17 | "useTabs": false,
18 | "vueIndentScriptAndStyle": false
19 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # Changelog
3 |
4 | ## v3.0.0 2022-07-25
5 |
6 | - Added `forwardRef` to mapping components
7 | - Added `ZoomPanContext` and `ZoomPanProvider`
8 | - Added `useZoomPanContext` and `useMapContext` hooks
9 | - Added support for React 18
10 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at richard@zcreativelabs.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Richard Zimerman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # react-simple-maps
4 |
5 | Create beautiful SVG maps in react with d3-geo and topojson using a declarative api.
6 |
7 | Read the [docs](https://www.react-simple-maps.io/docs/getting-started/), or check out the [examples](https://www.react-simple-maps.io/examples/).
8 |
9 | ### Why
10 |
11 | `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.
12 |
13 | Since `react-simple-maps` leaves DOM work to react, it can also easily be used with other libraries, such as [react-spring](https://github.com/react-spring/react-spring) and [react-annotation](https://github.com/susielu/react-annotation/).
14 |
15 | ### Install
16 |
17 | To install `react-simple-maps`
18 |
19 | ```bash
20 | $ npm install react-simple-maps
21 | ```
22 |
23 | ...or if you use yarn:
24 |
25 | ```bash
26 | $ yarn add react-simple-maps
27 | ```
28 |
29 | ### Usage
30 |
31 | `React-simple-maps` exposes a set of components that can be combined to create svg maps with markers and annotations. In order to render a map you have to provide a reference to a valid topojson file. You can find example topojson files on [here](https://github.com/topojson/world-atlas) or [here](https://github.com/deldersveld/topojson). To learn how to make your own topojson maps from shapefiles, please read ["How to convert and prepare TopoJSON files for interactive mapping with d3"](https://hackernoon.com/how-to-convert-and-prepare-topojson-files-for-interactive-mapping-with-d3-499cf0ced5f) on medium.
32 |
33 | ```jsx
34 | import React from "react";
35 | import ReactDOM from "react-dom";
36 | import { ComposableMap, Geographies, Geography } from "react-simple-maps";
37 |
38 | // url to a valid topojson file
39 | const geoUrl =
40 | "https://raw.githubusercontent.com/deldersveld/topojson/master/world-countries.json";
41 |
42 | const App = () => {
43 | return (
44 |
45 |
46 |
47 | {({ geographies }) =>
48 | geographies.map((geo) => (
49 |
50 | ))
51 | }
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | document.addEventListener("DOMContentLoaded", () => {
59 | ReactDOM.render( , document.getElementById("app"));
60 | });
61 | ```
62 |
63 | Check out the [live example](https://codesandbox.io/s/basic-map-wvlol)
64 |
65 | The above will render a world map using the [equal earth projection](https://observablehq.com/@d3/equal-earth). You can read more about this projection on [Shaded Relief](http://shadedrelief.com/ee_proj/) and on [Wikipedia](https://en.wikipedia.org/wiki/Equal_Earth_projection).
66 |
67 | For other examples and components, check out the [documentation](https://www.react-simple-maps.io/docs/getting-started).
68 |
69 | ### Map files
70 |
71 | React-simple-maps does not restrict you to one specific map and relies on custom map files that you can modify in any way necessary for the project. This means that you can visualise countries, regions, and continents in various resolutions, as long as they can be represented using geojson/topojson.
72 |
73 | In order for this to work properly, you will however need to provide these valid map files to react-simple-maps yourself. Luckily, there are decent sources for map files on github and elsewhere. Here are some you can check out:
74 |
75 | * [Natural Earth](https://github.com/nvkelso/natural-earth-vector)
76 | * [Topojson maps by @deldersveld](https://github.com/deldersveld/topojson)
77 | * [Topojson world atlas](https://github.com/topojson/world-atlas)
78 |
79 | ### License
80 |
81 | MIT licensed. Copyright (c) Richard Zimerman 2017. See [LICENSE.md](https://github.com/zcreativelabs/react-simple-maps/blob/master/LICENSE) for more details.
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-simple-maps",
3 | "version": "3.0.0",
4 | "description": "An svg map chart component built with and for React",
5 | "main": "dist/index.js",
6 | "module": "dist/index.es.js",
7 | "browser": "dist/index.umd.js",
8 | "files": [
9 | "dist"
10 | ],
11 | "scripts": {
12 | "build": "rollup -c",
13 | "watch": "rollup -cw",
14 | "prepare": "rollup -c",
15 | "test": "mocha './tests/**/*.spec.js' --compilers js:@babel/register"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/zcreativelabs/react-simple-maps.git"
20 | },
21 | "keywords": [
22 | "react",
23 | "maps",
24 | "charts",
25 | "worldmap",
26 | "usa",
27 | "d3-geo"
28 | ],
29 | "author": "Richard Zimerman (https://github.com/zimrick)",
30 | "license": "MIT",
31 | "bugs": {
32 | "url": "https://github.com/zcreativelabs/react-simple-maps/issues"
33 | },
34 | "homepage": "https://github.com/zcreativelabs/react-simple-maps#readme",
35 | "devDependencies": {
36 | "@babel/core": "^7.18.6",
37 | "@babel/plugin-external-helpers": "^7.18.6",
38 | "@babel/plugin-proposal-object-rest-spread": "^7.18.6",
39 | "@babel/plugin-transform-react-jsx": "^7.18.6",
40 | "@babel/preset-env": "^7.18.6",
41 | "@babel/register": "^7.18.6",
42 | "@rollup/plugin-babel": "^5.3.1",
43 | "@rollup/plugin-commonjs": "^22.0.1",
44 | "@rollup/plugin-node-resolve": "^13.3.0",
45 | "expect": "^23.5.0",
46 | "mocha": "^5.2.0",
47 | "prop-types": "^15.7.2",
48 | "react": "^17.0.1",
49 | "react-dom": "^17.0.1",
50 | "rollup": "^2.75.7",
51 | "rollup-plugin-terser": "^7.0.2"
52 | },
53 | "peerDependencies": {
54 | "prop-types": "^15.7.2",
55 | "react": "^16.8.0 || 17.x || 18.x",
56 | "react-dom": "^16.8.0 || 17.x || 18.x"
57 | },
58 | "dependencies": {
59 | "d3-color": "^3.1.0",
60 | "d3-geo": "^3.1.0",
61 | "d3-interpolate": "^3.0.1",
62 | "d3-selection": "^3.0.0",
63 | "d3-zoom": "^3.0.0",
64 | "topojson-client": "^3.1.0"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import { babel } from "@rollup/plugin-babel";
2 | import resolve from "@rollup/plugin-node-resolve";
3 | import commonjs from "@rollup/plugin-commonjs";
4 | import { terser } from "rollup-plugin-terser";
5 |
6 | import pkg from "./package.json";
7 |
8 | const external = [
9 | ...Object.keys(pkg.dependencies || {}),
10 | ...Object.keys(pkg.peerDependencies || {}),
11 | ];
12 |
13 | export default [
14 | {
15 | input: "src/index.js",
16 | external,
17 | output: {
18 | name: "reactSimpleMaps",
19 | file: pkg.browser,
20 | format: "umd",
21 | extend: true,
22 | globals: {
23 | react: "React",
24 | "react-dom": "ReactDOM",
25 | "d3-geo": "d3",
26 | "d3-zoom": "d3",
27 | "d3-selection": "d3",
28 | "topojson-client": "topojson",
29 | "prop-types": "PropTypes",
30 | },
31 | },
32 | plugins: [
33 | babel({ babelHelpers: "bundled" }),
34 | resolve(),
35 | commonjs(),
36 | terser(),
37 | ],
38 | },
39 | {
40 | input: "src/index.js",
41 | external,
42 | output: [
43 | {
44 | file: pkg.main,
45 | format: "cjs",
46 | },
47 | {
48 | file: pkg.module,
49 | format: "es",
50 | },
51 | ],
52 | plugins: [babel({ babelHelpers: "bundled" })],
53 | },
54 | ];
55 |
--------------------------------------------------------------------------------
/src/components/Annotation.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, forwardRef } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | import { MapContext } from "./MapProvider"
5 | import { createConnectorPath } from "../utils"
6 |
7 | const Annotation = forwardRef(
8 | (
9 | {
10 | subject,
11 | children,
12 | connectorProps,
13 | dx = 30,
14 | dy = 30,
15 | curve = 0,
16 | className = "",
17 | ...restProps
18 | },
19 | ref
20 | ) => {
21 | const { projection } = useContext(MapContext)
22 | const [x, y] = projection(subject)
23 | const connectorPath = createConnectorPath(dx, dy, curve)
24 |
25 | return (
26 |
32 |
38 | {children}
39 |
40 | )
41 | }
42 | )
43 |
44 | Annotation.displayName = "Annotation"
45 |
46 | Annotation.propTypes = {
47 | subject: PropTypes.array,
48 | children: PropTypes.oneOfType([
49 | PropTypes.node,
50 | PropTypes.arrayOf(PropTypes.node),
51 | ]),
52 | dx: PropTypes.number,
53 | dy: PropTypes.number,
54 | curve: PropTypes.number,
55 | connectorProps: PropTypes.object,
56 | className: PropTypes.string,
57 | }
58 |
59 | export default Annotation
60 |
--------------------------------------------------------------------------------
/src/components/ComposableMap.js:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | import { MapProvider } from "./MapProvider"
5 |
6 | const ComposableMap = forwardRef(
7 | (
8 | {
9 | width = 800,
10 | height = 600,
11 | projection = "geoEqualEarth",
12 | projectionConfig = {},
13 | className = "",
14 | ...restProps
15 | },
16 | ref
17 | ) => {
18 | return (
19 |
25 |
31 |
32 | )
33 | }
34 | )
35 |
36 | ComposableMap.displayName = "ComposableMap"
37 |
38 | ComposableMap.propTypes = {
39 | width: PropTypes.number,
40 | height: PropTypes.number,
41 | projection: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
42 | projectionConfig: PropTypes.object,
43 | className: PropTypes.string,
44 | }
45 |
46 | export default ComposableMap
47 |
--------------------------------------------------------------------------------
/src/components/Geographies.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, forwardRef } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | import { MapContext } from "./MapProvider"
5 | import useGeographies from "./useGeographies"
6 |
7 | const Geographies = forwardRef(
8 | (
9 | { geography, children, parseGeographies, className = "", ...restProps },
10 | ref
11 | ) => {
12 | const { path, projection } = useContext(MapContext)
13 | const { geographies, outline, borders } = useGeographies({
14 | geography,
15 | parseGeographies,
16 | })
17 |
18 | return (
19 |
20 | {geographies &&
21 | geographies.length > 0 &&
22 | children({ geographies, outline, borders, path, projection })}
23 |
24 | )
25 | }
26 | )
27 |
28 | Geographies.displayName = "Geographies"
29 |
30 | Geographies.propTypes = {
31 | geography: PropTypes.oneOfType([
32 | PropTypes.string,
33 | PropTypes.object,
34 | PropTypes.array,
35 | ]),
36 | children: PropTypes.func,
37 | parseGeographies: PropTypes.func,
38 | className: PropTypes.string,
39 | }
40 |
41 | export default Geographies
42 |
--------------------------------------------------------------------------------
/src/components/Geography.js:
--------------------------------------------------------------------------------
1 | import React, { useState, memo, forwardRef } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | const Geography = forwardRef(
5 | (
6 | {
7 | geography,
8 | onMouseEnter,
9 | onMouseLeave,
10 | onMouseDown,
11 | onMouseUp,
12 | onFocus,
13 | onBlur,
14 | style = {},
15 | className = "",
16 | ...restProps
17 | },
18 | ref
19 | ) => {
20 | const [isPressed, setPressed] = useState(false)
21 | const [isFocused, setFocus] = useState(false)
22 |
23 | function handleMouseEnter(evt) {
24 | setFocus(true)
25 | if (onMouseEnter) onMouseEnter(evt)
26 | }
27 |
28 | function handleMouseLeave(evt) {
29 | setFocus(false)
30 | if (isPressed) setPressed(false)
31 | if (onMouseLeave) onMouseLeave(evt)
32 | }
33 |
34 | function handleFocus(evt) {
35 | setFocus(true)
36 | if (onFocus) onFocus(evt)
37 | }
38 |
39 | function handleBlur(evt) {
40 | setFocus(false)
41 | if (isPressed) setPressed(false)
42 | if (onBlur) onBlur(evt)
43 | }
44 |
45 | function handleMouseDown(evt) {
46 | setPressed(true)
47 | if (onMouseDown) onMouseDown(evt)
48 | }
49 |
50 | function handleMouseUp(evt) {
51 | setPressed(false)
52 | if (onMouseUp) onMouseUp(evt)
53 | }
54 |
55 | return (
56 |
78 | )
79 | }
80 | )
81 |
82 | Geography.displayName = "Geography"
83 |
84 | Geography.propTypes = {
85 | geography: PropTypes.object,
86 | onMouseEnter: PropTypes.func,
87 | onMouseLeave: PropTypes.func,
88 | onMouseDown: PropTypes.func,
89 | onMouseUp: PropTypes.func,
90 | onFocus: PropTypes.func,
91 | onBlur: PropTypes.func,
92 | style: PropTypes.object,
93 | className: PropTypes.string,
94 | }
95 |
96 | export default memo(Geography)
97 |
--------------------------------------------------------------------------------
/src/components/Graticule.js:
--------------------------------------------------------------------------------
1 | import React, { memo, useContext, forwardRef } from "react"
2 | import PropTypes from "prop-types"
3 | import { geoGraticule } from "d3-geo"
4 |
5 | import { MapContext } from "./MapProvider"
6 |
7 | const Graticule = forwardRef(
8 | (
9 | {
10 | fill = "transparent",
11 | stroke = "currentcolor",
12 | step = [10, 10],
13 | className = "",
14 | ...restProps
15 | },
16 | ref
17 | ) => {
18 | const { path } = useContext(MapContext)
19 | return (
20 |
28 | )
29 | }
30 | )
31 |
32 | Graticule.displayName = "Graticule"
33 |
34 | Graticule.propTypes = {
35 | fill: PropTypes.string,
36 | stroke: PropTypes.string,
37 | step: PropTypes.array,
38 | className: PropTypes.string,
39 | }
40 |
41 | export default memo(Graticule)
42 |
--------------------------------------------------------------------------------
/src/components/Line.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, forwardRef } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | import { MapContext } from "./MapProvider"
5 |
6 | const Line = forwardRef(
7 | (
8 | {
9 | from = [0, 0],
10 | to = [0, 0],
11 | coordinates,
12 | stroke = "currentcolor",
13 | strokeWidth = 3,
14 | fill = "transparent",
15 | className = "",
16 | ...restProps
17 | },
18 | ref
19 | ) => {
20 | const { path } = useContext(MapContext)
21 |
22 | const lineData = {
23 | type: "LineString",
24 | coordinates: coordinates || [from, to],
25 | }
26 |
27 | return (
28 |
37 | )
38 | }
39 | )
40 |
41 | Line.displayName = "Line"
42 |
43 | Line.propTypes = {
44 | from: PropTypes.array,
45 | to: PropTypes.array,
46 | coordinates: PropTypes.array,
47 | stroke: PropTypes.string,
48 | strokeWidth: PropTypes.number,
49 | fill: PropTypes.string,
50 | className: PropTypes.string,
51 | }
52 |
53 | export default Line
54 |
--------------------------------------------------------------------------------
/src/components/MapProvider.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useMemo, useCallback, useContext } from "react"
2 | import PropTypes from "prop-types"
3 | import * as d3Geo from "d3-geo"
4 |
5 | const { geoPath, ...projections } = d3Geo
6 |
7 | const MapContext = createContext()
8 |
9 | const makeProjection = ({
10 | projectionConfig = {},
11 | projection = "geoEqualEarth",
12 | width = 800,
13 | height = 600,
14 | }) => {
15 | const isFunc = typeof projection === "function"
16 |
17 | if (isFunc) return projection
18 |
19 | let proj = projections[projection]().translate([width / 2, height / 2])
20 |
21 | const supported = [
22 | proj.center ? "center" : null,
23 | proj.rotate ? "rotate" : null,
24 | proj.scale ? "scale" : null,
25 | proj.parallels ? "parallels" : null,
26 | ]
27 |
28 | supported.forEach((d) => {
29 | if (!d) return
30 | proj = proj[d](projectionConfig[d] || proj[d]())
31 | })
32 |
33 | return proj
34 | }
35 |
36 | const MapProvider = ({
37 | width,
38 | height,
39 | projection,
40 | projectionConfig,
41 | ...restProps
42 | }) => {
43 | const [cx, cy] = projectionConfig.center || []
44 | const [rx, ry, rz] = projectionConfig.rotate || []
45 | const [p1, p2] = projectionConfig.parallels || []
46 | const s = projectionConfig.scale || null
47 |
48 | const projMemo = useMemo(() => {
49 | return makeProjection({
50 | projectionConfig: {
51 | center: cx || cx === 0 || cy || cy === 0 ? [cx, cy] : null,
52 | rotate: rx || rx === 0 || ry || ry === 0 ? [rx, ry, rz] : null,
53 | parallels: p1 || p1 === 0 || p2 || p2 === 0 ? [p1, p2] : null,
54 | scale: s,
55 | },
56 | projection,
57 | width,
58 | height,
59 | })
60 | }, [width, height, projection, cx, cy, rx, ry, rz, p1, p2, s])
61 |
62 | const proj = useCallback(projMemo, [projMemo])
63 |
64 | const value = useMemo(() => {
65 | return {
66 | width,
67 | height,
68 | projection: proj,
69 | path: geoPath().projection(proj),
70 | }
71 | }, [width, height, proj])
72 |
73 | return
74 | }
75 |
76 | MapProvider.propTypes = {
77 | width: PropTypes.number,
78 | height: PropTypes.number,
79 | projection: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
80 | projectionConfig: PropTypes.object,
81 | }
82 |
83 | const useMapContext = () => {
84 | return useContext(MapContext)
85 | }
86 |
87 | export { MapProvider, MapContext, useMapContext }
88 |
--------------------------------------------------------------------------------
/src/components/Marker.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, useState, forwardRef } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | import { MapContext } from "./MapProvider"
5 |
6 | const Marker = forwardRef(
7 | (
8 | {
9 | coordinates,
10 | children,
11 | onMouseEnter,
12 | onMouseLeave,
13 | onMouseDown,
14 | onMouseUp,
15 | onFocus,
16 | onBlur,
17 | style = {},
18 | className = "",
19 | ...restProps
20 | },
21 | ref
22 | ) => {
23 | const { projection } = useContext(MapContext)
24 | const [isPressed, setPressed] = useState(false)
25 | const [isFocused, setFocus] = useState(false)
26 |
27 | const [x, y] = projection(coordinates)
28 |
29 | function handleMouseEnter(evt) {
30 | setFocus(true)
31 | if (onMouseEnter) onMouseEnter(evt)
32 | }
33 |
34 | function handleMouseLeave(evt) {
35 | setFocus(false)
36 | if (isPressed) setPressed(false)
37 | if (onMouseLeave) onMouseLeave(evt)
38 | }
39 |
40 | function handleFocus(evt) {
41 | setFocus(true)
42 | if (onFocus) onFocus(evt)
43 | }
44 |
45 | function handleBlur(evt) {
46 | setFocus(false)
47 | if (isPressed) setPressed(false)
48 | if (onBlur) onBlur(evt)
49 | }
50 |
51 | function handleMouseDown(evt) {
52 | setPressed(true)
53 | if (onMouseDown) onMouseDown(evt)
54 | }
55 |
56 | function handleMouseUp(evt) {
57 | setPressed(false)
58 | if (onMouseUp) onMouseUp(evt)
59 | }
60 |
61 | return (
62 |
83 | {children}
84 |
85 | )
86 | }
87 | )
88 |
89 | Marker.displayName = "Marker"
90 |
91 | Marker.propTypes = {
92 | coordinates: PropTypes.array,
93 | children: PropTypes.oneOfType([
94 | PropTypes.node,
95 | PropTypes.arrayOf(PropTypes.node),
96 | ]),
97 | onMouseEnter: PropTypes.func,
98 | onMouseLeave: PropTypes.func,
99 | onMouseDown: PropTypes.func,
100 | onMouseUp: PropTypes.func,
101 | onFocus: PropTypes.func,
102 | onBlur: PropTypes.func,
103 | style: PropTypes.object,
104 | className: PropTypes.string,
105 | }
106 |
107 | export default Marker
108 |
--------------------------------------------------------------------------------
/src/components/Sphere.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, memo, useMemo, useContext, forwardRef } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | import { MapContext } from "./MapProvider"
5 |
6 | const Sphere = forwardRef(
7 | (
8 | {
9 | id = "rsm-sphere",
10 | fill = "transparent",
11 | stroke = "currentcolor",
12 | strokeWidth = 0.5,
13 | className = "",
14 | ...restProps
15 | },
16 | ref
17 | ) => {
18 | const { path } = useContext(MapContext)
19 | const spherePath = useMemo(() => path({ type: "Sphere" }), [path])
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
37 |
38 | )
39 | }
40 | )
41 |
42 | Sphere.displayName = "Sphere"
43 |
44 | Sphere.propTypes = {
45 | id: PropTypes.string,
46 | fill: PropTypes.string,
47 | stroke: PropTypes.string,
48 | strokeWidth: PropTypes.number,
49 | className: PropTypes.string,
50 | }
51 |
52 | export default memo(Sphere)
53 |
--------------------------------------------------------------------------------
/src/components/ZoomPanProvider.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | const ZoomPanContext = createContext()
5 |
6 | const defaultValue = {
7 | x: 0,
8 | y: 0,
9 | k: 1,
10 | transformString: "translate(0 0) scale(1)",
11 | }
12 |
13 | const ZoomPanProvider = ({ value = defaultValue, ...restProps }) => {
14 | return
15 | }
16 |
17 | ZoomPanProvider.propTypes = {
18 | x: PropTypes.number,
19 | y: PropTypes.number,
20 | k: PropTypes.number,
21 | transformString: PropTypes.string,
22 | }
23 |
24 | const useZoomPanContext = () => {
25 | return useContext(ZoomPanContext)
26 | }
27 |
28 | export { ZoomPanContext, ZoomPanProvider, useZoomPanContext }
29 |
--------------------------------------------------------------------------------
/src/components/ZoomableGroup.js:
--------------------------------------------------------------------------------
1 | import React, { useContext, forwardRef } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | import { MapContext } from "./MapProvider"
5 | import { ZoomPanProvider } from "./ZoomPanProvider"
6 | import useZoomPan from "./useZoomPan"
7 |
8 | const ZoomableGroup = forwardRef(
9 | (
10 | {
11 | center = [0, 0],
12 | zoom = 1,
13 | minZoom = 1,
14 | maxZoom = 8,
15 | translateExtent,
16 | filterZoomEvent,
17 | onMoveStart,
18 | onMove,
19 | onMoveEnd,
20 | className,
21 | ...restProps
22 | },
23 | ref
24 | ) => {
25 | const { width, height } = useContext(MapContext)
26 |
27 | const { mapRef, transformString, position } = useZoomPan({
28 | center,
29 | filterZoomEvent,
30 | onMoveStart,
31 | onMove,
32 | onMoveEnd,
33 | scaleExtent: [minZoom, maxZoom],
34 | translateExtent,
35 | zoom,
36 | })
37 |
38 | return (
39 |
42 |
43 |
44 |
50 |
51 |
52 | )
53 | }
54 | )
55 |
56 | ZoomableGroup.displayName = "ZoomableGroup"
57 |
58 | ZoomableGroup.propTypes = {
59 | center: PropTypes.array,
60 | zoom: PropTypes.number,
61 | minZoom: PropTypes.number,
62 | maxZoom: PropTypes.number,
63 | translateExtent: PropTypes.arrayOf(PropTypes.array),
64 | onMoveStart: PropTypes.func,
65 | onMove: PropTypes.func,
66 | onMoveEnd: PropTypes.func,
67 | className: PropTypes.string,
68 | }
69 |
70 | export default ZoomableGroup
71 |
--------------------------------------------------------------------------------
/src/components/useGeographies.js:
--------------------------------------------------------------------------------
1 | import { useMemo, useState, useEffect, useContext } from "react"
2 | import { MapContext } from "./MapProvider"
3 |
4 | import {
5 | fetchGeographies,
6 | getFeatures,
7 | getMesh,
8 | prepareFeatures,
9 | isString,
10 | prepareMesh,
11 | } from "../utils"
12 |
13 | export default function useGeographies({ geography, parseGeographies }) {
14 | const { path } = useContext(MapContext)
15 | const [output, setOutput] = useState({})
16 |
17 | useEffect(() => {
18 | if (typeof window === `undefined`) return
19 |
20 | if (!geography) return
21 |
22 | if (isString(geography)) {
23 | fetchGeographies(geography).then((geos) => {
24 | if (geos) {
25 | setOutput({
26 | geographies: getFeatures(geos, parseGeographies),
27 | mesh: getMesh(geos),
28 | })
29 | }
30 | })
31 | } else {
32 | setOutput({
33 | geographies: getFeatures(geography, parseGeographies),
34 | mesh: getMesh(geography),
35 | })
36 | }
37 | }, [geography, parseGeographies])
38 |
39 | const { geographies, outline, borders } = useMemo(() => {
40 | const mesh = output.mesh || {}
41 | const preparedMesh = prepareMesh(mesh.outline, mesh.borders, path)
42 | return {
43 | geographies: prepareFeatures(output.geographies, path),
44 | outline: preparedMesh.outline,
45 | borders: preparedMesh.borders,
46 | }
47 | }, [output, path])
48 |
49 | return { geographies, outline, borders }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/useZoomPan.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState, useContext } from "react"
2 | import { zoom as d3Zoom, zoomIdentity as d3ZoomIdentity } from "d3-zoom"
3 | import { select as d3Select } from "d3-selection"
4 |
5 | import { MapContext } from "./MapProvider"
6 | import { getCoords } from "../utils"
7 |
8 | export default function useZoomPan({
9 | center,
10 | filterZoomEvent,
11 | onMoveStart,
12 | onMoveEnd,
13 | onMove,
14 | translateExtent = [
15 | [-Infinity, -Infinity],
16 | [Infinity, Infinity],
17 | ],
18 | scaleExtent = [1, 8],
19 | zoom = 1,
20 | }) {
21 | const { width, height, projection } = useContext(MapContext)
22 |
23 | const [lon, lat] = center
24 | const [position, setPosition] = useState({ x: 0, y: 0, k: 1 })
25 | const lastPosition = useRef({ x: 0, y: 0, k: 1 })
26 | const mapRef = useRef()
27 | const zoomRef = useRef()
28 | const bypassEvents = useRef(false)
29 |
30 | const [a, b] = translateExtent
31 | const [a1, a2] = a
32 | const [b1, b2] = b
33 | const [minZoom, maxZoom] = scaleExtent
34 |
35 | useEffect(() => {
36 | const svg = d3Select(mapRef.current)
37 |
38 | function handleZoomStart(d3Event) {
39 | if (!onMoveStart || bypassEvents.current) return
40 | onMoveStart(
41 | {
42 | coordinates: projection.invert(
43 | getCoords(width, height, d3Event.transform)
44 | ),
45 | zoom: d3Event.transform.k,
46 | },
47 | d3Event
48 | )
49 | }
50 |
51 | function handleZoom(d3Event) {
52 | if (bypassEvents.current) return
53 | const { transform, sourceEvent } = d3Event
54 | setPosition({
55 | x: transform.x,
56 | y: transform.y,
57 | k: transform.k,
58 | dragging: sourceEvent,
59 | })
60 | if (!onMove) return
61 | onMove(
62 | {
63 | x: transform.x,
64 | y: transform.y,
65 | zoom: transform.k,
66 | dragging: sourceEvent,
67 | },
68 | d3Event
69 | )
70 | }
71 |
72 | function handleZoomEnd(d3Event) {
73 | if (bypassEvents.current) {
74 | bypassEvents.current = false
75 | return
76 | }
77 | const [x, y] = projection.invert(
78 | getCoords(width, height, d3Event.transform)
79 | )
80 | lastPosition.current = { x, y, k: d3Event.transform.k }
81 | if (!onMoveEnd) return
82 | onMoveEnd({ coordinates: [x, y], zoom: d3Event.transform.k }, d3Event)
83 | }
84 |
85 | function filterFunc(d3Event) {
86 | if (filterZoomEvent) {
87 | return filterZoomEvent(d3Event)
88 | }
89 | return d3Event ? !d3Event.ctrlKey && !d3Event.button : false
90 | }
91 |
92 | const zoom = d3Zoom()
93 | .filter(filterFunc)
94 | .scaleExtent([minZoom, maxZoom])
95 | .translateExtent([
96 | [a1, a2],
97 | [b1, b2],
98 | ])
99 | .on("start", handleZoomStart)
100 | .on("zoom", handleZoom)
101 | .on("end", handleZoomEnd)
102 |
103 | zoomRef.current = zoom
104 | svg.call(zoom)
105 | }, [
106 | width,
107 | height,
108 | a1,
109 | a2,
110 | b1,
111 | b2,
112 | minZoom,
113 | maxZoom,
114 | projection,
115 | onMoveStart,
116 | onMove,
117 | onMoveEnd,
118 | filterZoomEvent,
119 | ])
120 |
121 | useEffect(() => {
122 | if (
123 | lon === lastPosition.current.x &&
124 | lat === lastPosition.current.y &&
125 | zoom === lastPosition.current.k
126 | )
127 | return
128 |
129 | const coords = projection([lon, lat])
130 | const x = coords[0] * zoom
131 | const y = coords[1] * zoom
132 | const svg = d3Select(mapRef.current)
133 |
134 | bypassEvents.current = true
135 |
136 | svg.call(
137 | zoomRef.current.transform,
138 | d3ZoomIdentity.translate(width / 2 - x, height / 2 - y).scale(zoom)
139 | )
140 | setPosition({ x: width / 2 - x, y: height / 2 - y, k: zoom })
141 |
142 | lastPosition.current = { x: lon, y: lat, k: zoom }
143 | }, [lon, lat, zoom, width, height, projection])
144 |
145 | return {
146 | mapRef,
147 | position,
148 | transformString: `translate(${position.x} ${position.y}) scale(${position.k})`,
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as ComposableMap } from "./components/ComposableMap"
2 | export { default as Geographies } from "./components/Geographies"
3 | export { default as Geography } from "./components/Geography"
4 | export { default as Graticule } from "./components/Graticule"
5 | export { default as ZoomableGroup } from "./components/ZoomableGroup"
6 | export { default as Sphere } from "./components/Sphere"
7 | export { default as Marker } from "./components/Marker"
8 | export { default as Line } from "./components/Line"
9 | export { default as Annotation } from "./components/Annotation"
10 | export {
11 | MapProvider,
12 | MapContext,
13 | useMapContext,
14 | } from "./components/MapProvider"
15 | export {
16 | ZoomPanProvider,
17 | ZoomPanContext,
18 | useZoomPanContext,
19 | } from "./components/ZoomPanProvider"
20 | export { default as useGeographies } from "./components/useGeographies"
21 | export { default as useZoomPan } from "./components/useZoomPan"
22 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import { feature, mesh } from "topojson-client"
2 |
3 | export function getCoords(w, h, t) {
4 | const xOffset = (w * t.k - w) / 2
5 | const yOffset = (h * t.k - h) / 2
6 | return [w / 2 - (xOffset + t.x) / t.k, h / 2 - (yOffset + t.y) / t.k]
7 | }
8 |
9 | export function fetchGeographies(url) {
10 | return fetch(url)
11 | .then((res) => {
12 | if (!res.ok) {
13 | throw Error(res.statusText)
14 | }
15 | return res.json()
16 | })
17 | .catch((error) => {
18 | console.log("There was a problem when fetching the data: ", error)
19 | })
20 | }
21 |
22 | export function getFeatures(geographies, parseGeographies) {
23 | const isTopojson = geographies.type === "Topology"
24 | if (!isTopojson) {
25 | return parseGeographies
26 | ? parseGeographies(geographies.features || geographies)
27 | : geographies.features || geographies
28 | }
29 | const feats = feature(
30 | geographies,
31 | geographies.objects[Object.keys(geographies.objects)[0]]
32 | ).features
33 | return parseGeographies ? parseGeographies(feats) : feats
34 | }
35 |
36 | export function getMesh(geographies) {
37 | const isTopojson = geographies.type === "Topology"
38 | if (!isTopojson) return null
39 | const outline = mesh(
40 | geographies,
41 | geographies.objects[Object.keys(geographies.objects)[0]],
42 | (a, b) => a === b
43 | )
44 | const borders = mesh(
45 | geographies,
46 | geographies.objects[Object.keys(geographies.objects)[0]],
47 | (a, b) => a !== b
48 | )
49 | return { outline, borders }
50 | }
51 |
52 | export function prepareMesh(outline, borders, path) {
53 | return outline && borders
54 | ? {
55 | outline: { ...outline, rsmKey: "outline", svgPath: path(outline) },
56 | borders: { ...borders, rsmKey: "borders", svgPath: path(borders) },
57 | }
58 | : {}
59 | }
60 |
61 | export function prepareFeatures(geographies, path) {
62 | return geographies
63 | ? geographies.map((d, i) => {
64 | return {
65 | ...d,
66 | rsmKey: `geo-${i}`,
67 | svgPath: path(d),
68 | }
69 | })
70 | : []
71 | }
72 |
73 | export function createConnectorPath(dx = 30, dy = 30, curve = 0.5) {
74 | const curvature = Array.isArray(curve) ? curve : [curve, curve]
75 | const curveX = (dx / 2) * curvature[0]
76 | const curveY = (dy / 2) * curvature[1]
77 | return `M${0},${0} Q${-dx / 2 - curveX},${-dy / 2 + curveY} ${-dx},${-dy}`
78 | }
79 |
80 | export function isString(geo) {
81 | return typeof geo === "string"
82 | }
83 |
--------------------------------------------------------------------------------
/tests/utils.spec.js:
--------------------------------------------------------------------------------
1 |
2 | import expect from "expect"
3 |
4 | describe("sampleTest", () => {
5 | it("should exist", () => {
6 | expect(true).toEqual(true)
7 | })
8 | })
9 |
--------------------------------------------------------------------------------