Multiple Doughnut chart as markers over Openstreetmap - charts

I'm in trouble while implementing the doughnut chart over OpenStreetMap. I'm using react-chartjs2 for the doughnut chart and react-leaflet for Openstreetmap. Like we use the location icon on different coordinates over the map but here I want to use a Doughnut graph over the map instead of the location icon.
I want to achieve something like this
As per the react-leaflet documentation, the Marker icon property accepts two types of icons that is icon strings like image URL and divIcon which can be some HTML elements but while I'm rendering react component it does not accept and not showing it.
Here you can check in codesandbox I have added code to make it easy to try
https://codesandbox.io/s/doughnut-chart-over-osm-map-1indvl?file=/src/App.js

For what I know marker Icons can only be static, I use a function to create my only markers based on icons and plain html. Will be hard to do that with a component in your case.
My icon render function
import { divIcon } from "leaflet";
import { ReactElement } from "react";
import { renderToString } from "react-dom/server";
export const createLeafletIcon = (
icon: ReactElement,
size: number,
className?: string,
width: number = size,
height: number = size
) => {
return divIcon({
html: renderToString(icon),
iconSize: [width, height],
iconAnchor: [width / 2, height],
popupAnchor: [0, -height],
className: className ? className : "",
});
};
In your case I would try to cheese it and create blank markers and show the graph in popups instead and just force the popups to alway stay open.
EDIT: Added my custom Marker code below that have some nice options.
You can just use the defaultOpen option, and add the graph as a child component to the marker and it will show up in the popup. You can the change the styling of you liking to make it look like the graph is the marker.
import { LatLngLiteral } from "leaflet";
import React, { Children, ReactElement, useEffect, useRef } from "react";
import { Marker, Popup, useMap } from "react-leaflet";
import { MapPin } from "tabler-icons-react";
import { createLeafletIcon } from "./utils";
export interface LeafletMarkerProps {
position: LatLngLiteral;
flyToPosition?: boolean;
size?: number;
color?: string;
icon?: ReactElement;
defaultOpen?: boolean;
onOpen?: () => void;
children?: React.ReactNode;
markerType?: string;
zIndexOffset?: number;
}
const LeafletMarker: React.FC<LeafletMarkerProps> = ({
position,
flyToPosition = false,
children,
size = 30,
color,
defaultOpen = false,
onOpen,
icon = <MapPin size={size} color={color} />,
markerType,
zIndexOffset,
}) => {
const map = useMap();
const markerRef = useRef(null);
position && flyToPosition && map.flyTo(position);
const markerIcon = createLeafletIcon(icon, size, markerType); // Important to not get default styling
useEffect(() => {
if (defaultOpen) {
try {
// #ts-ignore
if (markerRef.current !== null && !markerRef.current.isPopupOpen()) {
// #ts-ignore
markerRef.current.openPopup();
}
} catch (error) {}
}
}, [defaultOpen, position.lat, position.lng]);
return (
<Marker
eventHandlers={{
popupopen: () => onOpen && onOpen(),
}}
ref={markerRef}
icon={markerIcon}
position={position}
zIndexOffset={zIndexOffset}
>
{/* autoPan important to not have jittering */}
<Popup autoPan={false}>{children}</Popup>
</Marker>
);
};
export default LeafletMarker;

Related

How to Stop Markers Moving on Zoom in Mapbox GL JS

I have tried previous answers with no success.
On initial load custom markers are in the correct position, though I do have to manually shift them to the right by 220px to achieve this. There is no zoom level set due to the fitBounds we use, described in the next paragraph.
On zooming in or out, the markers lose their position - their position is only correct for a certain zoom level. I am using a Turf.js bbox to calculate the bounding box we can use in the fitBounds function (though this is slightly out with me having to shift the markers to the right 220px).
In my _app.js (Next.js) I import the css
import 'mapbox-gl/dist/mapbox-gl.css'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
This is my Map component
import React, { useRef, useEffect } from 'react'
import mapboxgl from 'mapbox-gl'
import * as turf from '#turf/turf'
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN
export default function App() {
const mapContainer = useRef(null)
const map = useRef(null)
var geojson = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: { price: 399, beds: 2 },
geometry: { type: 'Point', coordinates: [115.1848543, -8.721661] },
},
{
type: 'Feature',
properties: { price: 450, beds: 3 },
geometry: { type: 'Point', coordinates: [115.1676773, -8.72259] },
},
],
}
var bbox = turf.bbox(geojson)
useEffect(() => {
if (map.current) return // initialize map only once
map.current = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/streets-v11',
})
for (const marker of geojson.features) {
// Create a DOM element for each marker.
const el = document.createElement('div')
//? WE USE ml-[220px] AS THE MARKERS APPEAR 220PX TO THE LEFT OF WHERE THEY SHOULD BE
el.className =
'flex items-center justify-center h-[30px] px-3 tracking-wide font-extrabold rounded-full bg-zinc-100 border border-zinc-300 shadow-md text-[14px] text-black ml-[220px]'
el.innerText = '$' + marker.properties.price
// Add markers to the map.
new mapboxgl.Marker(el).setLngLat(marker.geometry.coordinates).addTo(map.current)
}
map.current.fitBounds(bbox, { padding: 100 })
})
return <div ref={mapContainer} className='w-full h-screen' />
An example can be found here
Any help would be very much appreciated.
On initial load custom markers are in the correct position, though I do have to manually shift them to the right by 220px to achieve this.
This doesn't make sense. If you have to move them 220px, they're clearly not in the right position. Presumably, the longitude of the point is wrong, and by offsetting it 220px it temporarily looks right - but as soon as the zoom changes, it will be wrong by a different amount.
You need to:
get the right lat/longs for your markers
make sure your marker images don't contain extra white space
set icon-anchor correctly for your image design

Change the center of React-leaflet

I am trying to dynamically change the center of a map-container with data provided externally. I get the data as a string, and then parse it to get it as numbers instead. But when I enter lat to the const center, I get a NaN when trying use it.
import React from 'react'
import { useCasparData } from 'caspar-graphics'
import { useTimeline } from '#nxtedition/graphics-kit'
import './style.css'
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet'
import './leaflet.css'
export default function Lowerthird () {
const { text01, text02, text03, text04 } = useCasparData()
const lat = parseFloat(text01)
const zoom = 15
const center = [lat, 13.440222]
function onLoad(timeline) {
timeline
.add('start')
.from('.name', { x: -2000 }, 'start')
.from('.titel', { x: -1000 }, 'start')
}
function onStop(timeline) {
timeline
.reverse()
}
//useTimeline(onLoad, onStop)
return (
<MapContainer center={center} zoom={zoom} zoomControl={false}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
</MapContainer>
)
}
export const previewData = {
text01: '59.392133',
text02: '13.440222',
text03: '15',
text04: '[59.392133, 13.440222]'
}
I have looked through several threads here, but I have not found a answer that solves this for me... I do realize that the map-container is immutable - I just can't seem to figure out how to update it or set a new center...
(Oh... I am a total noob to react/leaflet, I am just trying to find a simple way to use Openstreetmap as a overlay in our broadcasts (tv))
You can use useMap() from a child component.
And with map, you can use functions such as flyTo to move around on the map.
Sadly useMap needs to be used from the child scope of MapContainer, however, you can just create an invisible component and add as a child and provide it with the new position when you want it to move.
So something like this (I have not tested this code but I have done similar stuff with markers):
interface ChangeCenterProps {
position: { lat: number; lng: number };
}
const ChangeCenter: React.FC<ChangeCenterProps> = ({ position }) => {
const map = useMap();
map.flyTo(position);
return <></>;
};

Map Size when Testing React Leaflet

I'm trying to test a custom react-leaflet component. It grabs the map element and uses a filter to get the number of elements visible at the current zoom level:
const visible = allPoints.filter(p => map.getBounds().contains(p));
This code works fine in the application, but when I try to test with Jest & Enzyme, the filter always returns 0 elements, regardless of zoom level. Poking at it a bit, I discovered that map.getSize() always returns 0x0 and map.getBounds() returns the same point for the southwest and northeast corners, which explains why it can't really contain any other points.
Is this just a bug in Enzyme or Leaflet or is there something that I need to do to set the map size explicitly?
import React from "react";
import { mount } from "enzyme";
import { act } from "react-dom/test-utils";
import { MapContext } from "../../../context";
import MarkersShown from "../markers-shown";
import { Map } from "react-leaflet";
import { mockParsed, mockCenter, mockData } from "./__data__";
describe("MarkersShown Tests", () => {
it("renders the total asset count", async () => {
const parsed = mockParsed;
const center = mockCenter;
let wrapper;
await act(async () => {
wrapper = mount(
<MapContext.Provider value={mockData}>
<Map zoom={parsed.z} center={center} style={{ height: "100px", width: "100px" }}>
<MarkersShown />
</Map>
</MapContext.Provider>
);
});
wrapper.update();
expect(wrapper.html()).toEqual(
expect.stringMatching("Markers Shown of 3")
);
});
});

Render mapbox vector tiles inside react-leaflet?

Is there a way to use vector tiles from react-leaflet?
I am aware of Leaflet.VectorGrid, but it is not written for react-leaflet?
For react-leaflet v2, export the MapBoxGLLayer component wrapped with HOC withLeaflet() to get it working.
Steps:
1.Install mapbox-gl-leaflet.
npm i mapbox-gl-leaflet
2.Add mapbox-gl js and css to index.html
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.51.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.51.0/mapbox-gl.css' rel='stylesheet' />
3.Add this component.
import L from "leaflet";
import {} from "mapbox-gl-leaflet";
import PropTypes from "prop-types";
import { GridLayer, withLeaflet } from "react-leaflet";
class MapBoxGLLayer extends GridLayer {
createLeafletElement(props) {
return L.mapboxGL(props);
}
}
/*
* Props are the options supported by Mapbox Map object
* Find options here:https://www.mapbox.com/mapbox-gl-js/api/#new-mapboxgl-map-options-
*/
MapBoxGLLayer.propTypes = {
accessToken: PropTypes.string.isRequired,
style: PropTypes.string
};
MapBoxGLLayer.defaultProps = {
style: "mapbox://styles/mapbox/streets-v9"
};
export default withLeaflet(MapBoxGLLayer);
4.Use the MapBoxGLLayer component.
class App extends Component {
state = {
center: [51.505, -0.091],
zoom: 13
};
render() {
return (
<div>
<Map center={this.state.center} zoom={this.state.zoom}>
<MapBoxGLLayer
accessToken={MAPBOX_ACCESS_TOKEN}
style="mapbox://styles/mapbox/streets-v9"
/>
</Map>
</div>
);
}
}
Find the working code here (Add your own mapbox token): https://codesandbox.io/s/ooypokn26y
There are some really nice vector tiles examples in this react-leaflet issue (mapbox-gl example reproduced below).
// #flow
import L from 'leaflet'
import {} from 'mapbox-gl-leaflet'
import {PropTypes} from 'react'
import { GridLayer } from 'react-leaflet'
export default class MapBoxGLLayer extends GridLayer {
static propTypes = {
opacity: PropTypes.number,
accessToken: PropTypes.string.isRequired,
style: PropTypes.string,
zIndex: PropTypes.number,
}
createLeafletElement(props: Object): Object {
return L.mapboxGL(props)
}
}
and the usage of the above component:
<Map>
<MapBoxGLLayer
url={url}
accessToken={MAPBOX_ACCESS_TOKEN}
style='https://style.example.com/style.json'
/>
</Map>
NOTE: you may also need to npm install mapbox-gl and import that library and assign into to the global window.mapboxgl = mapboxgl to avoid issues with mapboxgl being undefined.
You can create a custom component by extending the MapLayer component. You can see an example of how this is done in react-leaflet 1.0 in a project I contributed to here.
In case anyone finds this question and is wondering how to do this with MapLibre GL JS (FOSS fork of Mapbox GL JS) as the backend renderer, you can do this but it's not immediately obvious. The MapLibre equivalent plugin is actively maintained now while the Mapbox one is not.
Here is the component code (in TypeScript) for a MapLibre tile layer that you can use instead of TileLayer in your React Leaflet MapContainer:
import {
type LayerProps,
createElementObject,
createTileLayerComponent,
updateGridLayer,
withPane,
} from '#react-leaflet/core'
import L from 'leaflet'
import '#maplibre/maplibre-gl-leaflet'
export interface MapLibreTileLayerProps extends L.LeafletMaplibreGLOptions, LayerProps {
url: string,
attribution: string,
}
export const MapLibreTileLayer = createTileLayerComponent<
L.MaplibreGL,
MapLibreTileLayerProps
>(
function createTileLayer({ url, attribution, ...options }, context) {
const layer = L.maplibreGL({style: url, attribution: attribution, noWrap: true}, withPane(options, context))
return createElementObject(layer, context)
},
function updateTileLayer(layer, props, prevProps) {
updateGridLayer(layer, props, prevProps)
const { url, attribution } = props
if (url != null && url !== prevProps.url) {
layer.getMaplibreMap().setStyle(url)
}
if (attribution != null && attribution !== prevProps.attribution) {
layer.options.attribution = attribution
}
},
)
Full sample code lives in this repo on GitHub: https://github.com/stadiamaps/react-leaflet-demo

Popup always open in the marker

Is there any way the popup always stays open and does not need to click on it to open?
Expected behaviour
Actual behavior
With the introduction of react-leaflet version 2 which brings breaking changes in regard of creating custom components, it is no longer supported to extend components via inheritance (refer this thread for a more details)
In fact React official documentation also recommends to use composition instead of inheritance:
At Facebook, we use React in thousands of components, and we haven’t
found any use cases where we would recommend creating component
inheritance hierarchies.
Props and composition give you all the flexibility you need to
customize a component’s look and behavior in an explicit and safe way.
Remember that components may accept arbitrary props, including
primitive values, React elements, or functions.
The following example demonstrates how to extend marker component in order to keep popup open once the marker is displayed:
const MyMarker = props => {
const initMarker = ref => {
if (ref) {
ref.leafletElement.openPopup()
}
}
return <Marker ref={initMarker} {...props}/>
}
Explanation:
get access to native leaflet marker object (leafletElement) and open popup via Marker.openPopup method
Here is a demo
What you can do is to make your own Marker class from the react-leaflet marker, and then call the leaflet function openPopup() on the leaflet object after it has been mounted.
// Create your own class, extending from the Marker class.
class ExtendedMarker extends Marker {
componentDidMount() {
// Call the Marker class componentDidMount (to make sure everything behaves as normal)
super.componentDidMount();
// Access the marker element and open the popup.
this.leafletElement.openPopup();
}
}
This will make the popup open once the component has been mounted, and will also behave like a normal popup afterwards, ie. on close/open.
I threw together this fiddle that shows the same code together with the basic example.
You can use permanent tooltips, or React provides refs for this type of thing... you can do this:
https://jsfiddle.net/jrcoq72t/121/
const React = window.React
const { Map, TileLayer, Marker, Popup } = window.ReactLeaflet
class SimpleExample extends React.Component {
constructor () {
super()
this.state = {
lat: 51.505,
lng: -0.09,
zoom: 13
}
}
openPopup (marker) {
if (marker && marker.leafletElement) {
window.setTimeout(() => {
marker.leafletElement.openPopup()
})
}
}
render () {
const position = [this.state.lat, this.state.lng]
return (
<Map center={position} zoom={this.state.zoom}>
<TileLayer attribution='© OpenStreetMap contributors' url="http://{s}.tile.osm.org/{z}/{x}/{y}.png" />
<Marker position={position} ref={this.openPopup}>
<Popup>
<span>
A pretty CSS3 popup. <br /> Easily customizable.
</span>
</Popup>
</Marker>
</Map>
)
}
}
window.ReactDOM.render(<SimpleExample />, document.getElementById('container'))
References:
https://reactjs.org/docs/refs-and-the-dom.html
React.js - access to component methods
Auto open markers popup on react-leaflet map
The above no longer works with react-leaflet version 3. In your custom marker component, to get a reference to the leaflet element you should now use useRef() and then open up the popup in useEffect() once the component is mounted.
const MyMarker = (props) => {
const leafletRef = useRef();
useEffect(() => {
leafletRef.current.openPopup();
},[])
return <Marker ref={leafletRef} {...props} />
}
For the new react-leaflet v4 you will need to do some changes
const CustomMarker = ({ isActive, data, map }) => {
const [refReady, setRefReady] = useState(false);
let popupRef = useRef();
useEffect(() => {
if (refReady && isActive) {
map.openPopup(popupRef);
}
}, [isActive, refReady, map]);
return (
<Marker position={data.position}>
<Popup
ref={(r) => {
popupRef = r;
setRefReady(true);
}}
>
{data.title}
</Popup>
</Marker>
);
};
And then use MapContainer like this
const MapComponent = () => {
const [map, setMap] = useState(null);
return (
<div>
<MapContainer
ref={setMap}
center={[45.34416, 15.49005]}
zoom={15}
scrollWheelZoom={true}
>
<CustomMarker
isActive
map={map}
data={{
position: [45.34416, 15.49005],
title: "Text displayed in popup",
}}
/>
</MapContainer>
</div>
);
};
Edited
Notice that my old solution would try to open the popup every time it renders.
Found another solution that fit my needs to open it when the position changed. Notice that I look at position.lat, position.lng since it will think it always changes if you pass on the object.
And yes it is not perfect typescript but it is the best solution I could come up with.
const CustomMarker: React.FC<CustomMarkerProps> = ({ position, children }) => {
const map = useMap();
const markerRef = useRef(null);
useEffect(() => {
try {
// #ts-ignore
if (markerRef.current !== null && !markerRef.current.isPopupOpen()) {
// #ts-ignore
markerRef.current.openPopup();
}
} catch (error) {}
}, [position.lat, position.lng]);
return (
<Marker ref={markerRef} position={position}>
<Popup>{children}</Popup>
</Marker>
);
};
export default CustomMarker;
Old solution
Could not get it to work using useRef and useEffect. However, got it to work with calling openPopup() directly from the ref.
import { LatLngLiteral, Marker as LMarker } from "leaflet";
import React from "react";
import { Marker } from "react-leaflet";
export interface CustomMarkerProps {
position: LatLngLiteral;
open?: boolean;
}
const CustomMarker: React.FC<CustomMarkerProps> = ({
position,
open = false,
children,
}) => {
const initMarker = (ref: LMarker<any> | null) => {
if (ref && open) {
ref.openPopup();
}
};
return (
<Marker ref={initMarker} position={position}>
{children}
</Marker>
);
};
export default CustomMarker;