├── .DS_Store
├── examples
├── vite-example
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── main.tsx
│ │ ├── components
│ │ │ ├── CustomMarkerCluster.css
│ │ │ ├── SimpleExample.tsx
│ │ │ ├── TenThousandMarker.tsx
│ │ │ └── CustomMarkerCluster.tsx
│ │ └── App.tsx
│ ├── tsconfig.node.json
│ ├── vite.config.ts
│ ├── .gitignore
│ ├── index.html
│ ├── .eslintrc.cjs
│ ├── tsconfig.json
│ ├── package.json
│ ├── public
│ │ ├── MarkerCluster.css
│ │ ├── vite.svg
│ │ └── MarkerCluster.Default.css
│ └── vite.svg
└── .DS_Store
├── showcase.gif
├── .prettierrc.yml
├── CONTRIBUTING.md
├── .gitignore
├── tsconfig.json
├── .eslintrc.js
├── dist
├── index.d.ts
├── assets
│ ├── MarkerCluster.css
│ └── MarkerCluster.Default.css
└── index.js
├── index.html
├── src
├── assets
│ ├── MarkerCluster.css
│ └── MarkerCluster.Default.css
└── index.tsx
├── LICENSE
├── package.json
└── README.md
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akursat/react-leaflet-cluster/HEAD/.DS_Store
--------------------------------------------------------------------------------
/examples/vite-example/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/showcase.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akursat/react-leaflet-cluster/HEAD/showcase.gif
--------------------------------------------------------------------------------
/examples/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akursat/react-leaflet-cluster/HEAD/examples/.DS_Store
--------------------------------------------------------------------------------
/.prettierrc.yml:
--------------------------------------------------------------------------------
1 | semi: false
2 | trailingComma: 'all'
3 | singleQuote: true
4 | printWidth: 100
5 | tabWidth: 2
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Prerequisites
2 | Build package locally and test it with example project with `npm link` command.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /demo/dist
3 | /es
4 | /lib
5 | /node_modules
6 | /umd
7 | /examples/custom-marker-cluster/.yalc
8 | /examples/ten-thousand-marker/.yalc
9 | .yalc
10 | npm-debug.log*
11 |
--------------------------------------------------------------------------------
/examples/vite-example/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/examples/vite-example/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 |
5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
6 |
7 |
8 | ,
9 | )
10 |
--------------------------------------------------------------------------------
/examples/vite-example/src/components/CustomMarkerCluster.css:
--------------------------------------------------------------------------------
1 | .custom-marker-cluster {
2 | background: #fff;
3 | border: 3px solid #f00800;
4 | border-radius: 50%;
5 | color: #f00800;
6 | height: 33px;
7 | line-height: 27px;
8 | text-align: center;
9 | width: 33px;
10 | font-weight: bold;
11 | font-size: 14px;
12 | }
--------------------------------------------------------------------------------
/examples/vite-example/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | resolve: {
8 | // Deduplicate dependencies to avoid multiple React instances when using npm link
9 | dedupe: ['@react-leaflet/core'],
10 | },
11 | })
12 |
--------------------------------------------------------------------------------
/examples/vite-example/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ES2020",
5 | "moduleResolution": "node",
6 | "declaration": true,
7 | "outDir": "./dist",
8 | "strict": true,
9 | "jsx": "react-jsx",
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "skipLibCheck": true
13 | },
14 | "include": ["src"],
15 | "exclude": ["node_modules", "**/__tests__/*"]
16 | }
--------------------------------------------------------------------------------
/examples/vite-example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS + Leaflet + Marker Cluster
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/vite-example/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: { browser: true, es2020: true },
3 | extends: [
4 | 'eslint:recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'plugin:react-hooks/recommended',
7 | ],
8 | parser: '@typescript-eslint/parser',
9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
10 | plugins: ['react-refresh'],
11 | rules: {
12 | 'react-refresh/only-export-components': 'warn',
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | ecmaVersion: 2020,
5 | sourceType: 'module',
6 | ecmaFeatures: {
7 | jsx: true,
8 | },
9 | },
10 | settings: {
11 | react: {
12 | version: 'detect',
13 | },
14 | },
15 | extends: [
16 | 'plugin:react/recommended',
17 | 'plugin:@typescript-eslint/recommended',
18 | 'plugin:prettier/recommended',
19 | ],
20 | plugins: ['react-hooks'],
21 | rules: {
22 | 'react-hooks/rules-of-hooks': 'error',
23 | 'react-hooks/exhaustive-deps': 'warn',
24 | },
25 | };
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import L, { LeafletMouseEventHandlerFn } from 'leaflet';
3 |
4 | type ClusterEvents = {
5 | onClick?: LeafletMouseEventHandlerFn;
6 | onDblClick?: LeafletMouseEventHandlerFn;
7 | onMouseDown?: LeafletMouseEventHandlerFn;
8 | onMouseUp?: LeafletMouseEventHandlerFn;
9 | onMouseOver?: LeafletMouseEventHandlerFn;
10 | onMouseOut?: LeafletMouseEventHandlerFn;
11 | onContextMenu?: LeafletMouseEventHandlerFn;
12 | };
13 | declare const MarkerClusterGroup: React.ForwardRefExoticComponent>;
16 |
17 | export { MarkerClusterGroup as default };
18 |
--------------------------------------------------------------------------------
/examples/vite-example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
17 |
18 | birikimplani 🚀🚀🚀
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/examples/vite-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-vite-app",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "leaflet": "^1.9.0",
14 | "react": "^19.0.0",
15 | "react-dom": "^19.0.0",
16 | "react-leaflet": "^5.0.0",
17 | "react-leaflet-cluster": "^4.0.0"
18 | },
19 | "devDependencies": {
20 | "@types/leaflet": "^1.9.3",
21 | "@types/react": "^19.0.0",
22 | "@types/react-dom": "^19.0.0",
23 | "@typescript-eslint/eslint-plugin": "^5.59.0",
24 | "@typescript-eslint/parser": "^5.59.0",
25 | "@vitejs/plugin-react": "^4.0.0",
26 | "eslint": "^8.38.0",
27 | "eslint-plugin-react-hooks": "^4.6.0",
28 | "eslint-plugin-react-refresh": "^0.3.4",
29 | "typescript": "^5.0.2",
30 | "vite": "^4.3.9"
31 | }
32 | }
--------------------------------------------------------------------------------
/dist/assets/MarkerCluster.css:
--------------------------------------------------------------------------------
1 | /* To solve Next.js issues source from https://github.com/Leaflet/Leaflet.markercluster/blob/master/dist/MarkerCluster.css */
2 | .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
3 | -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
4 | -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
5 | -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
6 | transition: transform 0.3s ease-out, opacity 0.3s ease-in;
7 | }
8 |
9 | .leaflet-cluster-spider-leg {
10 | /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
11 | -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
12 | -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
13 | -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
14 | transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
15 | }
--------------------------------------------------------------------------------
/src/assets/MarkerCluster.css:
--------------------------------------------------------------------------------
1 | /* To solve Next.js issues source from https://github.com/Leaflet/Leaflet.markercluster/blob/master/dist/MarkerCluster.css */
2 | .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
3 | -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
4 | -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
5 | -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
6 | transition: transform 0.3s ease-out, opacity 0.3s ease-in;
7 | }
8 |
9 | .leaflet-cluster-spider-leg {
10 | /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
11 | -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
12 | -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
13 | -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
14 | transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
15 | }
--------------------------------------------------------------------------------
/examples/vite-example/public/MarkerCluster.css:
--------------------------------------------------------------------------------
1 | /* To solve Next.js issues source from https://github.com/Leaflet/Leaflet.markercluster/blob/master/dist/MarkerCluster.css */
2 | .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
3 | -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
4 | -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
5 | -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
6 | transition: transform 0.3s ease-out, opacity 0.3s ease-in;
7 | }
8 |
9 | .leaflet-cluster-spider-leg {
10 | /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
11 | -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
12 | -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
13 | -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
14 | transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
15 | }
--------------------------------------------------------------------------------
/examples/vite-example/src/components/SimpleExample.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { MapContainer, Marker, TileLayer } from 'react-leaflet'
3 | import MarkerClusterGroup from 'react-leaflet-cluster'
4 |
5 | export default function SimpleExample() {
6 | const [count, setCount] = useState(0)
7 |
8 | return (
9 |
10 |
Simple Example
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | setCount((count) => count + 1)}>count is {count}
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 A. Kürşat Uzun
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 |
--------------------------------------------------------------------------------
/examples/vite-example/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/vite-example/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/vite-example/src/components/TenThousandMarker.tsx:
--------------------------------------------------------------------------------
1 | import { MapContainer, Marker, TileLayer } from 'react-leaflet'
2 | import MarkerClusterGroup from 'react-leaflet-cluster'
3 |
4 | // Mock data for 10,000 markers
5 | const generateAddressPoints = () => {
6 | const points = []
7 | for (let i = 0; i < 10000; i++) {
8 | points.push([
9 | -41.975762 + (Math.random() - 0.5) * 20, // latitude
10 | 172.934298 + (Math.random() - 0.5) * 20, // longitude
11 | `Marker ${i + 1}`, // title
12 | ])
13 | }
14 | return points
15 | }
16 |
17 | const addressPoints = generateAddressPoints()
18 | type AddressPoint = Array<[number, number, string]>
19 |
20 | export default function TenThousandMarker() {
21 | return (
22 |
23 |
10,000 Markers Example
24 |
30 |
34 |
35 | {(addressPoints as AddressPoint).map((address, index) => (
36 |
37 | ))}
38 |
39 |
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/src/assets/MarkerCluster.Default.css:
--------------------------------------------------------------------------------
1 | /* To solve Next.js issues source from https://github.com/Leaflet/Leaflet.markercluster/blob/master/dist/MarkerCluster.Default.css */
2 | .marker-cluster-small {
3 | background-color: rgba(181, 226, 140, 0.6);
4 | }
5 | .marker-cluster-small div {
6 | background-color: rgba(110, 204, 57, 0.6);
7 | }
8 |
9 | .marker-cluster-medium {
10 | background-color: rgba(241, 211, 87, 0.6);
11 | }
12 | .marker-cluster-medium div {
13 | background-color: rgba(240, 194, 12, 0.6);
14 | }
15 |
16 | .marker-cluster-large {
17 | background-color: rgba(253, 156, 115, 0.6);
18 | }
19 | .marker-cluster-large div {
20 | background-color: rgba(241, 128, 23, 0.6);
21 | }
22 |
23 | /* IE 6-8 fallback colors */
24 | .leaflet-oldie .marker-cluster-small {
25 | background-color: rgb(181, 226, 140);
26 | }
27 | .leaflet-oldie .marker-cluster-small div {
28 | background-color: rgb(110, 204, 57);
29 | }
30 |
31 | .leaflet-oldie .marker-cluster-medium {
32 | background-color: rgb(241, 211, 87);
33 | }
34 | .leaflet-oldie .marker-cluster-medium div {
35 | background-color: rgb(240, 194, 12);
36 | }
37 |
38 | .leaflet-oldie .marker-cluster-large {
39 | background-color: rgb(253, 156, 115);
40 | }
41 | .leaflet-oldie .marker-cluster-large div {
42 | background-color: rgb(241, 128, 23);
43 | }
44 |
45 | .marker-cluster {
46 | background-clip: padding-box;
47 | border-radius: 20px;
48 | }
49 | .marker-cluster div {
50 | width: 30px;
51 | height: 30px;
52 | margin-left: 5px;
53 | margin-top: 5px;
54 |
55 | text-align: center;
56 | border-radius: 15px;
57 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
58 | }
59 | .marker-cluster span {
60 | line-height: 30px;
61 | }
--------------------------------------------------------------------------------
/dist/assets/MarkerCluster.Default.css:
--------------------------------------------------------------------------------
1 | /* To solve Next.js issues source from https://github.com/Leaflet/Leaflet.markercluster/blob/master/dist/MarkerCluster.Default.css */
2 | .marker-cluster-small {
3 | background-color: rgba(181, 226, 140, 0.6);
4 | }
5 | .marker-cluster-small div {
6 | background-color: rgba(110, 204, 57, 0.6);
7 | }
8 |
9 | .marker-cluster-medium {
10 | background-color: rgba(241, 211, 87, 0.6);
11 | }
12 | .marker-cluster-medium div {
13 | background-color: rgba(240, 194, 12, 0.6);
14 | }
15 |
16 | .marker-cluster-large {
17 | background-color: rgba(253, 156, 115, 0.6);
18 | }
19 | .marker-cluster-large div {
20 | background-color: rgba(241, 128, 23, 0.6);
21 | }
22 |
23 | /* IE 6-8 fallback colors */
24 | .leaflet-oldie .marker-cluster-small {
25 | background-color: rgb(181, 226, 140);
26 | }
27 | .leaflet-oldie .marker-cluster-small div {
28 | background-color: rgb(110, 204, 57);
29 | }
30 |
31 | .leaflet-oldie .marker-cluster-medium {
32 | background-color: rgb(241, 211, 87);
33 | }
34 | .leaflet-oldie .marker-cluster-medium div {
35 | background-color: rgb(240, 194, 12);
36 | }
37 |
38 | .leaflet-oldie .marker-cluster-large {
39 | background-color: rgb(253, 156, 115);
40 | }
41 | .leaflet-oldie .marker-cluster-large div {
42 | background-color: rgb(241, 128, 23);
43 | }
44 |
45 | .marker-cluster {
46 | background-clip: padding-box;
47 | border-radius: 20px;
48 | }
49 | .marker-cluster div {
50 | width: 30px;
51 | height: 30px;
52 | margin-left: 5px;
53 | margin-top: 5px;
54 |
55 | text-align: center;
56 | border-radius: 15px;
57 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
58 | }
59 | .marker-cluster span {
60 | line-height: 30px;
61 | }
--------------------------------------------------------------------------------
/examples/vite-example/public/MarkerCluster.Default.css:
--------------------------------------------------------------------------------
1 | /* To solve Next.js issues source from https://github.com/Leaflet/Leaflet.markercluster/blob/master/dist/MarkerCluster.Default.css */
2 | .marker-cluster-small {
3 | background-color: rgba(181, 226, 140, 0.6);
4 | }
5 | .marker-cluster-small div {
6 | background-color: rgba(110, 204, 57, 0.6);
7 | }
8 |
9 | .marker-cluster-medium {
10 | background-color: rgba(241, 211, 87, 0.6);
11 | }
12 | .marker-cluster-medium div {
13 | background-color: rgba(240, 194, 12, 0.6);
14 | }
15 |
16 | .marker-cluster-large {
17 | background-color: rgba(253, 156, 115, 0.6);
18 | }
19 | .marker-cluster-large div {
20 | background-color: rgba(241, 128, 23, 0.6);
21 | }
22 |
23 | /* IE 6-8 fallback colors */
24 | .leaflet-oldie .marker-cluster-small {
25 | background-color: rgb(181, 226, 140);
26 | }
27 | .leaflet-oldie .marker-cluster-small div {
28 | background-color: rgb(110, 204, 57);
29 | }
30 |
31 | .leaflet-oldie .marker-cluster-medium {
32 | background-color: rgb(241, 211, 87);
33 | }
34 | .leaflet-oldie .marker-cluster-medium div {
35 | background-color: rgb(240, 194, 12);
36 | }
37 |
38 | .leaflet-oldie .marker-cluster-large {
39 | background-color: rgb(253, 156, 115);
40 | }
41 | .leaflet-oldie .marker-cluster-large div {
42 | background-color: rgb(241, 128, 23);
43 | }
44 |
45 | .marker-cluster {
46 | background-clip: padding-box;
47 | border-radius: 20px;
48 | }
49 | .marker-cluster div {
50 | width: 30px;
51 | height: 30px;
52 | margin-left: 5px;
53 | margin-top: 5px;
54 |
55 | text-align: center;
56 | border-radius: 15px;
57 | font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
58 | }
59 | .marker-cluster span {
60 | line-height: 30px;
61 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-leaflet-cluster",
3 | "version": "4.0.0",
4 | "description": "React-leaflet-cluster is a plugin for react-leaflet. A wrapper component of Leaflet.markercluster.",
5 | "main": "dist/index.js",
6 | "type": "module",
7 | "types": "dist/index.d.ts",
8 | "files": [
9 | "dist"
10 | ],
11 | "scripts": {
12 | "build": "tsup src/index.tsx --format esm --dts --clean --external @react-leaflet/core && npm run copy:assets",
13 | "format": "prettier --write \"src/**/*.tsx\"",
14 | "lint": "eslint 'src/**/*.{ts,tsx}'",
15 | "prepublishOnly": "npm run lint",
16 | "preversion": "npm run lint",
17 | "version": "npm run format && git add -A src",
18 | "postversion": "git push && git push --tags",
19 | "copy:assets": "cpx 'src/assets/**' 'dist/assets'"
20 | },
21 | "dependencies": {
22 | "leaflet.markercluster": "^1.5.3"
23 | },
24 | "peerDependencies": {
25 | "@react-leaflet/core": "^3.0.0",
26 | "leaflet": "^1.9.0",
27 | "react": "^19.0.0",
28 | "react-dom": "^19.0.0",
29 | "react-leaflet": "^5.0.0"
30 | },
31 | "devDependencies": {
32 | "@types/leaflet": "^1.9.0",
33 | "@types/leaflet.markercluster": "^1.5.4",
34 | "@types/node": "^14.18.21",
35 | "@types/react": "^19.0.0",
36 | "@types/react-dom": "^19.0.0",
37 | "@typescript-eslint/eslint-plugin": "^4.33.0",
38 | "@typescript-eslint/parser": "^4.33.0",
39 | "cpx": "^1.2.1",
40 | "eslint": "^7.32.0",
41 | "eslint-config-prettier": "^10.1.8",
42 | "eslint-plugin-prettier": "^3.4.1",
43 | "eslint-plugin-react": "^7.30.0",
44 | "eslint-plugin-react-hooks": "^4.5.0",
45 | "prettier": "^2.6.2",
46 | "react-leaflet": "^5.0.0",
47 | "tsup": "^8.0.0",
48 | "typescript": "^5.9.3"
49 | },
50 | "author": "akursat",
51 | "homepage": "https://akursat.gitbook.io/marker-cluster/",
52 | "license": "SEE LICENSE IN ",
53 | "repository": "https://github.com/akursat/react-leaflet-cluster",
54 | "keywords": [
55 | "react",
56 | "leaflet",
57 | "marker-cluster",
58 | "cluster",
59 | "map",
60 | "react-leaflet-v4"
61 | ]
62 | }
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | // src/index.tsx
2 | import {
3 | extendContext,
4 | createElementObject,
5 | createPathComponent
6 | } from "@react-leaflet/core";
7 | import L from "leaflet";
8 | import "leaflet.markercluster";
9 | function getPropsAndEvents(props) {
10 | let clusterProps = {};
11 | let clusterEvents = {};
12 | const { children, ...rest } = props;
13 | Object.entries(rest).forEach(([propName, prop]) => {
14 | if (propName.startsWith("on")) {
15 | clusterEvents = { ...clusterEvents, [propName]: prop };
16 | } else {
17 | clusterProps = { ...clusterProps, [propName]: prop };
18 | }
19 | });
20 | return { clusterProps, clusterEvents };
21 | }
22 | function createMarkerClusterGroup(props, context) {
23 | const { clusterProps, clusterEvents } = getPropsAndEvents(props);
24 | const markerClusterGroup = new L.MarkerClusterGroup(clusterProps);
25 | Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => {
26 | const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`;
27 | markerClusterGroup.on(clusterEvent, callback);
28 | });
29 | return createElementObject(
30 | markerClusterGroup,
31 | extendContext(context, { layerContainer: markerClusterGroup })
32 | );
33 | }
34 | var updateMarkerCluster = (instance, props, prevProps) => {
35 | const { clusterProps, clusterEvents } = getPropsAndEvents(props);
36 | const { clusterProps: prevClusterProps, clusterEvents: prevClusterEvents } = getPropsAndEvents(prevProps);
37 | Object.keys(clusterProps).forEach((key) => {
38 | if (clusterProps[key] !== prevClusterProps[key]) {
39 | instance.options[key] = clusterProps[key];
40 | }
41 | });
42 | Object.entries(prevClusterEvents).forEach(([eventAsProp, callback]) => {
43 | const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`;
44 | instance.off(clusterEvent, callback);
45 | });
46 | Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => {
47 | const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`;
48 | instance.on(clusterEvent, callback);
49 | });
50 | };
51 | var MarkerClusterGroup = createPathComponent(
52 | createMarkerClusterGroup,
53 | updateMarkerCluster
54 | );
55 | var index_default = MarkerClusterGroup;
56 | export {
57 | index_default as default
58 | };
59 |
--------------------------------------------------------------------------------
/examples/vite-example/src/components/CustomMarkerCluster.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { MapContainer, Marker, TileLayer } from 'react-leaflet'
3 | import L from 'leaflet'
4 | import MarkerClusterGroup from 'react-leaflet-cluster'
5 | import './CustomMarkerCluster.css'
6 |
7 | const customIcon = new L.Icon({
8 | iconUrl:
9 | 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDciIHZpZXdCb3g9IjAgMCA0MCA0NyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIwIDBDOC45NTQzMSAwIDAgOC45NTQzMSAwIDIwQzAgMzEuMDQ1NyA4Ljk1NDMxIDQwIDIwIDQwQzMxLjA0NTcgNDAgNDAgMzEuMDQ1NyA0MCAyMEM0MCA4Ljk1NDMxIDMxLjA0NTcgMCAyMCAwWiIgZmlsbD0iIzAwNzNGQSIvPgo8cGF0aCBkPSJNMjAgNkMxMi4yNjg5IDYgNiAxMi4yNjg5IDYgMjBDNiAyNy43MzExIDEyLjI2ODkgMzQgMjAgMzRDMjcuNzMxMSAzNCAzNCAyNy43MzExIDM0IDIwQzM0IDEyLjI2ODkgMjcuNzMxMSA2IDIwIDZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K',
10 | iconSize: new L.Point(40, 47),
11 | })
12 |
13 | const createClusterCustomIcon = function (cluster: any) {
14 | return L.divIcon({
15 | html: `${cluster.getChildCount()} `,
16 | className: 'custom-marker-cluster',
17 | iconSize: L.point(33, 33, true),
18 | })
19 | }
20 |
21 | export default function CustomMarkerCluster() {
22 | const [dynamicPosition, setPosition] = useState([41.051687, 28.987261])
23 |
24 | return (
25 |
26 |
Custom Marker Cluster Example
27 | {
29 | setPosition([40.051687, 28.987261])
30 | }}
31 | >
32 | Rerender Marker
33 |
34 |
40 |
44 | console.log('onClick', e)}
46 | iconCreateFunction={createClusterCustomIcon as any}
47 | maxClusterRadius={150}
48 | spiderfyOnMaxZoom={true}
49 | polygonOptions={{
50 | fillColor: '#ffffff',
51 | color: '#f00800',
52 | weight: 5,
53 | opacity: 1,
54 | fillOpacity: 0.8,
55 | }}
56 | showCoverageOnHover={true}
57 | >
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/examples/vite-example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import 'leaflet/dist/leaflet.css'
3 | // Import the required CSS for marker clustering
4 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.css'
5 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css'
6 |
7 | import SimpleExample from './components/SimpleExample'
8 | import TenThousandMarker from './components/TenThousandMarker'
9 | import CustomMarkerCluster from './components/CustomMarkerCluster'
10 | import L from 'leaflet'
11 |
12 | // configure the default icon
13 | delete (L.Icon.Default as any).prototype._getIconUrl
14 | L.Icon.Default.mergeOptions({
15 | iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
16 | iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
17 | shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
18 | })
19 | type ExampleType = 'simple' | 'ten-thousand' | 'custom'
20 |
21 | export default function App() {
22 | const [currentExample, setCurrentExample] = useState('simple')
23 |
24 | const renderExample = () => {
25 | switch (currentExample) {
26 | case 'simple':
27 | return
28 | case 'ten-thousand':
29 | return
30 | case 'custom':
31 | return
32 | default:
33 | return
34 | }
35 | }
36 |
37 | return (
38 |
39 |
React Leaflet Cluster Examples
40 |
41 |
42 | setCurrentExample('simple')}
44 | style={{
45 | marginRight: '10px',
46 | padding: '8px 16px',
47 | backgroundColor: currentExample === 'simple' ? '#007bff' : '#f8f9fa',
48 | color: currentExample === 'simple' ? 'white' : 'black',
49 | border: '1px solid #dee2e6',
50 | borderRadius: '4px',
51 | cursor: 'pointer',
52 | }}
53 | >
54 | Simple Example
55 |
56 | setCurrentExample('ten-thousand')}
58 | style={{
59 | marginRight: '10px',
60 | padding: '8px 16px',
61 | backgroundColor: currentExample === 'ten-thousand' ? '#007bff' : '#f8f9fa',
62 | color: currentExample === 'ten-thousand' ? 'white' : 'black',
63 | border: '1px solid #dee2e6',
64 | borderRadius: '4px',
65 | cursor: 'pointer',
66 | }}
67 | >
68 | 10,000 Markers
69 |
70 | setCurrentExample('custom')}
72 | style={{
73 | padding: '8px 16px',
74 | backgroundColor: currentExample === 'custom' ? '#007bff' : '#f8f9fa',
75 | color: currentExample === 'custom' ? 'white' : 'black',
76 | border: '1px solid #dee2e6',
77 | borderRadius: '4px',
78 | cursor: 'pointer',
79 | }}
80 | >
81 | Custom Cluster
82 |
83 |
84 |
85 | {renderExample()}
86 |
87 | )
88 | }
89 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | extendContext,
4 | createElementObject,
5 | createPathComponent,
6 | LeafletContextInterface,
7 | } from '@react-leaflet/core'
8 | import L, { LeafletMouseEventHandlerFn } from 'leaflet'
9 | import 'leaflet.markercluster'
10 | // CSS imports removed to prevent Next.js issues
11 | // Users should import CSS separately:
12 | // import 'react-leaflet-cluster/dist/assets/MarkerCluster.css'
13 | // import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css'
14 |
15 | // Users should configure their own icon URLs as needed
16 | // delete (L.Icon.Default as any).prototype._getIconUrl
17 | // L.Icon.Default.mergeOptions({
18 | // iconRetinaUrl: new URL('./assets/marker-icon-2x.png', import.meta.url).href,
19 | // iconUrl: new URL('./assets/marker-icon.png', import.meta.url).href,
20 | // shadowUrl: new URL('./assets/marker-shadow.png', import.meta.url).href,
21 | // })
22 |
23 | type ClusterType = { [key in string]: any }
24 |
25 | type ClusterEvents = {
26 | onClick?: LeafletMouseEventHandlerFn
27 | onDblClick?: LeafletMouseEventHandlerFn
28 | onMouseDown?: LeafletMouseEventHandlerFn
29 | onMouseUp?: LeafletMouseEventHandlerFn
30 | onMouseOver?: LeafletMouseEventHandlerFn
31 | onMouseOut?: LeafletMouseEventHandlerFn
32 | onContextMenu?: LeafletMouseEventHandlerFn
33 | }
34 |
35 | type MarkerClusterControl = L.MarkerClusterGroupOptions & {
36 | children: React.ReactNode
37 | } & ClusterEvents
38 |
39 | function getPropsAndEvents(props: MarkerClusterControl) {
40 | let clusterProps: ClusterType = {}
41 | let clusterEvents: ClusterType = {}
42 | const { children, ...rest } = props
43 | // Splitting props and events to different objects
44 | Object.entries(rest).forEach(([propName, prop]) => {
45 | if (propName.startsWith('on')) {
46 | clusterEvents = { ...clusterEvents, [propName]: prop }
47 | } else {
48 | clusterProps = { ...clusterProps, [propName]: prop }
49 | }
50 | })
51 | return { clusterProps, clusterEvents }
52 | }
53 |
54 | function createMarkerClusterGroup(props: MarkerClusterControl, context: LeafletContextInterface) {
55 | const { clusterProps, clusterEvents } = getPropsAndEvents(props)
56 | const markerClusterGroup = new L.MarkerClusterGroup(clusterProps)
57 | Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => {
58 | const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`
59 | markerClusterGroup.on(clusterEvent, callback)
60 | })
61 | return createElementObject(
62 | markerClusterGroup,
63 | extendContext(context, { layerContainer: markerClusterGroup }),
64 | )
65 | }
66 |
67 | const updateMarkerCluster = (
68 | instance: L.MarkerClusterGroup,
69 | props: MarkerClusterControl,
70 | prevProps: MarkerClusterControl,
71 | ) => {
72 | const { clusterProps, clusterEvents } = getPropsAndEvents(props)
73 | const { clusterProps: prevClusterProps, clusterEvents: prevClusterEvents } =
74 | getPropsAndEvents(prevProps)
75 |
76 | // Update Options
77 | Object.keys(clusterProps).forEach((key) => {
78 | if (clusterProps[key] !== prevClusterProps[key]) {
79 | // eslint-disable-next-line
80 | // @ts-ignore
81 | instance.options[key] = clusterProps[key]
82 | }
83 | })
84 |
85 | // Update Events
86 | Object.entries(prevClusterEvents).forEach(([eventAsProp, callback]) => {
87 | const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`
88 | instance.off(clusterEvent, callback)
89 | })
90 |
91 | Object.entries(clusterEvents).forEach(([eventAsProp, callback]) => {
92 | const clusterEvent = `cluster${eventAsProp.substring(2).toLowerCase()}`
93 | instance.on(clusterEvent, callback)
94 | })
95 | }
96 |
97 | const MarkerClusterGroup = createPathComponent(
98 | createMarkerClusterGroup,
99 | updateMarkerCluster,
100 | )
101 |
102 | export default MarkerClusterGroup
103 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-leaflet-cluster [](https://npmjs.com/package/react-leaflet-cluster) [](https://npmjs.com/package/react-leaflet-cluster)
2 |
3 | - [x] React 19 support
4 | - [x] React-leaflet v5 support
5 | - [x] Typescript support
6 | - [x] Next.js compatibility
7 |
8 | ## Breaking Changes in v4.0.0
9 | This release updates key peer dependencies to support React 19 and React-Leaflet 5. Make sure your project is upgraded before installing this version.
10 |
11 | ## Breaking Changes in v3.0.0
12 |
13 | **CSS imports are now required manually** - The package no longer automatically imports CSS files to prevent Next.js build issues. You must now import the CSS files separately:
14 |
15 | ```tsx
16 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.css'
17 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css'
18 | ```
19 |
20 | React-leaflet-cluster is a plugin for react-leaflet. A wrapper component of Leaflet.markercluster. Ready to be integrated into your React.js application to create beautifully animated Marker Clustering functionality.
21 |
22 | 
23 |
24 | ### Examples - Code Sandbox
25 |
26 | - [10.000 marker](https://codesandbox.io/s/hidden-breeze-nrd3e?fontsize=14&hidenavigation=1&theme=dark)
27 | - [Custom marker cluster](https://codesandbox.io/s/beautiful-pike-j2l0w?file=/src/App.tsx)
28 |
29 | ### Installation
30 |
31 | `yarn add react-leaflet-cluster`
32 |
33 | Or with npm:
34 | `npm i react-leaflet-cluster`
35 |
36 | #### Prerequisites
37 |
38 | Make sure that you've installed react-leaflet and leaflet.
39 |
40 | ```json
41 | "react": "19.x",
42 | "react-dom": "19.0.0",
43 | "leaflet": "1.9.x",
44 | "react-leaflet": "5.0.x"
45 | ```
46 |
47 | #### CSS Import
48 |
49 | The package requires CSS files to be imported for proper styling. Add these imports to your main component or entry file:
50 |
51 | ```tsx
52 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.css'
53 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css'
54 | ```
55 |
56 | **Note for Next.js users**: These CSS imports are required and should be added to your component or a global CSS file. The package no longer automatically imports CSS to prevent Next.js build issues.
57 |
58 | #### Icon Configuration
59 |
60 | The package no longer automatically configures Leaflet's default marker icons. If you need to use default markers, you'll need to configure the icon URLs yourself. Add this configuration to your component or entry file:
61 |
62 | ```tsx
63 | import L from 'leaflet'
64 |
65 | // Configure default marker icons
66 | delete (L.Icon.Default as any).prototype._getIconUrl
67 | L.Icon.Default.mergeOptions({
68 | iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
69 | iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
70 | shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
71 | })
72 | ```
73 |
74 | Alternatively, you can use your own custom icons for markers:
75 |
76 | ```tsx
77 | import L from 'leaflet'
78 |
79 | const customIcon = new L.Icon({
80 | iconUrl: '/path/to/your/marker-icon.png',
81 | iconSize: [25, 41],
82 | iconAnchor: [12, 41],
83 | popupAnchor: [1, -34],
84 | shadowUrl: '/path/to/your/marker-shadow.png',
85 | shadowSize: [41, 41],
86 | })
87 | ```
88 |
89 | #### Migration from v2.x
90 |
91 | If you're upgrading from v2.x, you need to add the CSS imports manually. The package will work without them, but the clustering won't be styled properly.
92 |
93 | **Before (v2.x):**
94 |
95 | ```tsx
96 | import MarkerClusterGroup from 'react-leaflet-cluster'
97 | // CSS was automatically imported
98 | ```
99 |
100 | **After (v3.1.0):**
101 |
102 | ```tsx
103 | import MarkerClusterGroup from 'react-leaflet-cluster'
104 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.css'
105 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css'
106 | ```
107 |
108 | #### API
109 |
110 | For more detailed guide and API see:
111 | https://akursat.gitbook.io/marker-cluster/api
112 |
113 | #### Usage
114 |
115 | ```tsx
116 | import MarkerClusterGroup from 'react-leaflet-cluster'
117 | import { MapContainer, Marker } from 'react-leaflet'
118 | import 'leaflet/dist/leaflet.css'
119 | // Import the required CSS for marker clustering
120 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.css'
121 | import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css'
122 | import { addressPoints } from './realworld'
123 |
124 | const Demo = () => {
125 | return (
126 |
132 |
133 | {(addressPoints as AdressPoint).map((address, index) => (
134 |
140 | ))}
141 |
142 |
143 | )
144 | }
145 | ```
146 |
--------------------------------------------------------------------------------