Skip to content

Instantly share code, notes, and snippets.

@iam-rohid
Created May 10, 2023 14:20
Show Gist options
  • Save iam-rohid/aebde17198bda9d6509bfaa881f8a88f to your computer and use it in GitHub Desktop.
Save iam-rohid/aebde17198bda9d6509bfaa881f8a88f to your computer and use it in GitHub Desktop.
Leaflet Seller Map Component with NextJS 13.4
.leaflet-container {
outline: none;
}
.seller-marker-popup .seller-marker-popup-name {
font-weight: 600;
margin: 4px 0;
}
.seller-marker-popup .seller-marker-popup-sales {
margin: 0;
}
.seller-marker-icon {
background-color: rgba(251, 191, 36, 0.3);
border: 1px solid rgb(251, 191, 36);
border-radius: 9999px;
}
.cluster-marker-icon {
background-color: #4f7bff;
border-radius: 9999px;
display: flex !important;
align-items: center;
justify-content: center;
color: #fff;
text-shadow: 0 1px 0px rgba(0, 0, 0, 0.4);
font-weight: 500;
text-align: center;
font-size: 16px;
}
.spider-leg-polyline {
stroke-width: 1px;
stroke: rgba(0, 0, 0, 0.2);
transition: all ease-in;
}
.leaflet-left .leaflet-control {
@apply ml-4 mt-4 overflow-hidden rounded-lg border border-slate-200 shadow-xl;
}
.leaflet-touch .leaflet-bar a {
@apply flex h-10 w-10 items-center justify-center bg-white text-2xl hover:bg-slate-100;
}
.leaflet-touch .leaflet-bar a.leaflet-disabled {
@apply bg-white;
}
.leaflet-touch .leaflet-bar a span {
@apply leading-4;
}
.leaflet-popup-content-wrapper {
@apply rounded-lg border border-slate-200 bg-white shadow-xl;
}
"use client";
import L from "leaflet";
import "leaflet.markercluster";
import { useEffect, useRef } from "react";
import { range } from "@/utils/range";
import "leaflet.markercluster/dist/MarkerCluster.Default.css";
import "leaflet.markercluster/dist/MarkerCluster.css";
import "leaflet/dist/leaflet.css";
import "./seller-map.css";
export type Seller = {
id: number;
name: string;
estimate_sales: number;
latitude: number;
longitude: number;
};
export type SellerMapProps = {
sellers: Seller[];
onItemClick?: (seller: Seller) => void;
};
export default function SellerMap({ sellers, onItemClick }: SellerMapProps) {
const mapDivRef = useRef<HTMLDivElement | null>(null);
const mapRef = useRef<L.Map | null>(null);
useEffect(() => {
if (typeof window === "undefined") return;
if (!mapDivRef.current) return;
mapRef.current = L.map(mapDivRef.current).setView([51.505, -0.09], 4);
L.tileLayer(
"https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
{
attribution:
'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, ' +
'<a href="https://carto.com/">CARTO</a>',
minZoom: 3,
maxZoom: 19,
}
).addTo(mapRef.current);
return () => {
mapRef.current?.remove();
};
}, [mapDivRef, mapRef]);
useEffect(() => {
if (!mapRef.current) return;
if (!sellers?.length) return;
const sortedData = sellers.sort((a, b) =>
(a.estimate_sales || 0) > (b.estimate_sales || 0) ? 1 : -1
);
let sum = 0;
sortedData.forEach((seller) => {
sum += seller.estimate_sales || 0;
});
const avg = sum / sellers.length;
const markers = L.markerClusterGroup({
animate: true,
animateAddingMarkers: true,
maxClusterRadius: 100,
disableClusteringAtZoom: 8,
spiderfyOnMaxZoom: true,
spiderLegPolylineOptions: {
className: "spider-leg-polyline",
},
iconCreateFunction: (cluster) => {
const count = cluster.getChildCount();
const iconSize = range(0, sortedData.length, 30, 100, count);
return L.divIcon({
html: count.toLocaleString(),
className: "cluster-marker-icon",
iconSize: [iconSize, iconSize],
});
},
});
sortedData.forEach((seller) => {
const point = L.latLng(seller.latitude || 0, seller.longitude || 0);
const iconSize = Math.max(
10,
Math.min(60, Math.round(((seller.estimate_sales || 0) * 40) / avg))
);
const marker = L.marker(point, {
icon: L.divIcon({
className: "seller-marker-icon",
iconSize: [iconSize, iconSize],
}),
});
const popupContent = `<div class='seller-marker-popup'>
<p class='seller-marker-popup-name'>
${seller.name}
</p>
<p class='seller-marker-popup-sales'>
$${(seller.estimate_sales || 0).toLocaleString()}
</p>
</div>`;
marker.bindPopup(popupContent, {
closeButton: false,
});
marker.on("mouseover", () => marker.openPopup());
marker.on("mouseout", () => marker.closePopup());
marker.on("click", () => onItemClick && onItemClick(seller));
markers.addLayer(marker);
});
mapRef.current.addLayer(markers);
return () => {
mapRef.current?.removeLayer(markers);
};
}, [mapRef, sellers, onItemClick]);
return <div ref={mapDivRef} className="z-0 h-full w-full" />;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment