Openlayers popup in React doesn't follow the map zoom - reactjs

I'm trying to code a modified version of this example.
Using vanilla Javascript all is working fine, but now I'm trying to move it to React and the popup doesn't follow the map when I zoom in or zoom out the map. I suppose that the popup is not linked to the map overlay, but it's totally disconnected from it and this should be the problem, but I don't know how to fix it:
This is my code:
import 'ol/ol.css';
import React, { Component } from "react";
import ReactDOM from 'react-dom';
import Map from 'ol/Map';
import View from 'ol/View';
import Overlay from 'ol/Overlay';
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer';
import { OSM, Vector as VectorSource } from 'ol/source';
import { Circle as CircleStyle, Icon, Fill, Stroke, Style, Text } from 'ol/style';
import GeoJSON from 'ol/format/GeoJSON';
import * as olExtent from 'ol/extent';
import $ from 'jquery';
import 'bootstrap';
import markerLogo from './marker.png';
class PublicMap extends Component {
constructor(props) {
super(props);
var shapeDisciplinare = this.props.geoJson;
var iconStyle = new Style({
image: new Icon({
anchor: [0.5, 46],
anchorXUnits: 'fraction',
anchorYUnits: 'pixels',
src: markerLogo
}),
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({ color: '#000' }),
stroke: new Stroke({
color: '#fff', width: 2
})
})
});
var getStyles = function (feature) {
var myStyle = {
'Point': iconStyle,
'Polygon': new Style({
stroke: new Stroke({
color: 'blue',
width: 3
}),
fill: new Fill({
color: 'rgba(0, 0, 255, 0.1)'
}),
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({ color: '#000' }),
stroke: new Stroke({
color: '#fff', width: 2
}),
text: feature.getProperties().denominazione
})
})
};
return [myStyle[feature.getGeometry().getType()]];
};
var raster = new TileLayer({
source: new OSM()
});
var source = new VectorSource();
var vector = new VectorLayer({
source: source,
style: new Style({
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)'
}),
stroke: new Stroke({
color: '#ffcc33',
width: 2
}),
image: new CircleStyle({
radius: 7,
fill: new Fill({
color: '#ffcc33'
})
})
})
});
vector.setZIndex(1);
this.olmap = new Map({
layers: [raster, vector],
target: null,
view: new View({
center: [-11000000, 4600000],
zoom: 4
})
});
var reader = new GeoJSON({
defaultDataProjection: 'EPSG:3857',
Projection: 'EPSG:3857'
});
var projector = {
dataProjection: 'EPSG:4326',
featureProjection: 'EPSG:3857'
};
let shapeDisciplinareJson = JSON.parse(shapeDisciplinare);
var vectorSource = new VectorSource({
features: reader.readFeatures(shapeDisciplinareJson, projector)
});
var vectorLayer = new VectorLayer({
source: vectorSource,
style: getStyles
});
this.state = { vs: vectorSource, vl: vectorLayer };
}
componentDidMount() {
this.olmap.setTarget("map");
var extent = olExtent.createEmpty();
extent = olExtent.extend(extent, this.state.vs.getExtent());
this.olmap.addLayer(this.state.vl);
this.olmap.getView().fit(extent, this.olmap.getSize());
this.olmap.getView().setZoom(8);
var element = document.getElementById('popup');
this.popup = new Overlay({
element: ReactDOM.findDOMNode(this).querySelector('#popup'),
positioning: 'bottom-center',
stopEvent: false,
offset: [0, -50]
});
// display popup on click
this.olmap.on('click', (evt) => {
var feature = this.olmap.forEachFeatureAtPixel(evt.pixel,
(feature) => {
return feature;
});
if (feature) {
var coordinates = feature.getGeometry().getCoordinates();
//if lenght is 2, then is a gps location, otherwise a shape
if (coordinates.length === 2) {
this.popup.setOffset([0, -50]);
this.popup.setPosition(coordinates);
} else {
this.popup.setOffset([0, 0]);
this.popup.setPosition(evt.coordinate);
}
this.olmap.addOverlay(this.popup);
$(element).popover({
'placement': 'top',
'html': true,
'content': feature.getProperties().denominazione
});
$(element).attr('data-content', feature.getProperties().denominazione);
$(element).popover('show');
} else {
$(element).popover('hide');
}
});
// change mouse cursor when over marker
this.olmap.on('pointermove', (e) => {
if (e.dragging) {
$(element).popover('hide');
return;
}
var pixel = this.olmap.getEventPixel(e.originalEvent);
var hit = this.olmap.hasFeatureAtPixel(pixel);
this.olmap.getTargetElement().style.cursor = hit ? 'pointer' : '';
});
}
componentWillUnmount() {
this.map.setTarget(null);
}
render() {
return (
<div id="map" style={{ width: "100%", height: "360px" }}>
<div id="popup"></div>
</div>
);
}
}
export default PublicMap;

Related

How to make a realtime chart with Highcharts, Firebase, ReactJS with Typescript?

I need to create a graph in real time getting data from Firebase. I'm using ReactJs with typescript along with Highcharts.
I've already managed to simulate Highcharts in real time, but now I'm wondering how I can connect it to get the data that will be added to Firebase.
Global Graph Component:
import * as Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import HighchartsMore from 'highcharts/highcharts-more';
import HighchartsAccessibility from 'highcharts/modules/accessibility';
import HighchartsData from 'highcharts/modules/data';
import HighchartsExporting from 'highcharts/modules/exporting';
import HighchartsHeatmap from 'highcharts/modules/heatmap';
import HighchartsTreeChart from 'highcharts/modules/treemap';
import { defaultTheme } from '../../../styles/themes';
HighchartsAccessibility(Highcharts);
HighchartsMore(Highcharts);
HighchartsData(Highcharts);
HighchartsHeatmap(Highcharts);
HighchartsTreeChart(Highcharts);
HighchartsExporting(Highcharts);
interface IChartProps {
options: Highcharts.Options;
}
export const labelsStyle = (
fontSize = 20,
color = defaultTheme.palette.grey[900]
) => ({
color,
fontSize: `${fontSize / 16}rem`,
fontFamily: 'Poppins',
margin: '0px',
fontWeight: 400,
lineHeight: `${(fontSize + 10) / 16}rem`,
});
export const Chart: React.FC<IChartProps> = ({ options }) => {
Highcharts.setOptions({
// rangeSelector: {
// enabled: false,
// },
// navigator: {
// enabled: false,
// },
xAxis: {
labels: {
style: labelsStyle(),
},
},
yAxis: {
labels: {
style: labelsStyle(18, defaultTheme.palette.grey[200]),
},
},
credits: {
enabled: false,
},
exporting: {
enabled: false,
},
});
return <HighchartsReact highcharts={Highcharts} options={options} />;
};
Realtime Graph Component:
import { TooltipFormatterContextObject } from 'highcharts';
import {
Chart,
labelsStyle,
} from '../../../../shared/components/DataDisplay/Chart';
import { defaultTheme } from '../../../../shared/styles/themes';
import { formatDate } from '../../../../shared/utils/date';
import { getRandomIntInclusive } from '../../../../shared/utils/math';
export const GraphHeartRateMonitor: React.FC = () => {
const analyticsDataState = {
dates: [],
data: [],
};
const options: Highcharts.Options = {
chart: {
borderWidth: 0,
height: '176px',
plotBackgroundColor: defaultTheme.palette.background.default,
type: 'line',
marginRight: 10,
events: {
load() {
const series = this.series[0];
setInterval(function () {
const x = new Date().getTime(), // current time
y = getRandomIntInclusive(80, 120);
series.addPoint([x, y], true, true);
}, 1000);
},
},
},
title: {
text: '',
},
xAxis: {
type: 'datetime',
tickPixelInterval: 300,
labels: {
enabled: false,
},
},
yAxis: {
title: {
text: '',
},
gridLineColor: defaultTheme.palette.background.default,
tickInterval: 10,
labels: {
enabled: false,
},
plotLines: [
{
value: 120,
color: defaultTheme.palette.red[300],
width: 2,
label: {
text: '120',
useHTML: true,
verticalAlign: 'middle',
align: 'left',
style: {
background: defaultTheme.palette.red[300],
borderRadius: '24px',
height: '18px',
padding: '2px 12px',
color: defaultTheme.palette.common.white,
fontFamily: 'Roboto Mono',
fontSize: '12px',
fontWeight: '400',
},
},
},
],
// lineColor: defaultTheme.palette.background.default,
},
legend: {
enabled: false,
},
exporting: {
enabled: false,
},
series: [
{
name: 'Random data',
color: defaultTheme.palette.grey[900],
data: (function () {
// generate an array of random data
let data = [],
time = new Date().getTime(),
i;
for (i = -19; i <= 0; i++) {
data.push({
x: time + i * 1000,
y: getRandomIntInclusive(80, 120),
});
}
return data;
})(),
},
],
tooltip: {
useHTML: true,
backgroundColor: defaultTheme.palette.background.paper,
borderRadius: 4,
shadow: {
color: defaultTheme.palette.common.black,
},
padding: 12,
style: labelsStyle(20, defaultTheme.palette.grey[300]),
formatter() {
const self: TooltipFormatterContextObject = this;
return `
<span> Time:</span>
<strong>${formatDate(new Date(self.x), 'HH:mm:ss')}</strong>
</br>
<span>Heart Rate:</span>
<strong >${self.y} bpm</strong>
`;
},
},
};
return <Chart options={options} />;
};
To call the component just call as a normal component <GraphHeartRateMonitor /> .

Customize Images on Amcharts 5 force directed graph

I am trying to make a force directed graph with amcharts 5 where the nodes are images.
I was able to make images as nodes but was not really able to customize it. I want it to be rounded and has an onClick handler, which returns the node which is being clicked.
import React, { useLayoutEffect } from "react";
import "./App.css";
import * as am5 from "#amcharts/amcharts5";
import * as am5hierarchy from "#amcharts/amcharts5/hierarchy";
import am5themes_Animated from "#amcharts/amcharts5/themes/Animated";
function App(props) {
useLayoutEffect(() => {
let root = am5.Root.new("chartdiv");
root.setThemes([am5themes_Animated.new(root)]);
let chart = root.container.children.push(
am5.Container.new(root, {
width: am5.percent(100),
height: am5.percent(100),
layout: root.verticalLayout,
})
);
let series = chart.children.push(
am5hierarchy.ForceDirected.new(root, {
downDepth: 1,
initialDepth: 1,
topDepth: 0,
valueField: "value",
categoryField: "name",
childDataField: "children",
xField: "x",
yField: "y",
minRadius: 30,
manyBodyStrength: -40,
})
);
// series.circles.template.set("forceHidden", true);
// series.outerCircles.template.set("forceHidden", true);
series.circles.template.events.on("click", function (ev) {
console.log(ev);
console.log("Clicked on");
});
// Use template.setup function to prep up node with an image
series.nodes.template.setup = function (target) {
target.events.on("dataitemchanged", function (ev) {
target.children.push(
am5.Picture.new(root, {
width: 90,
height: 90,
centerX: am5.percent(50),
centerY: am5.percent(50),
src: ev.target.dataItem.dataContext.image,
})
);
});
};
series.bullets.push(function (root) {
return am5.Bullet.new(root, {
sprite: am5.Picture.new(root, {
radius: 4,
fill: series.get("fill"),
}),
});
});
series.data.setAll([
{
name: "Chrome",
value: 1,
image: "https://picsum.photos/202",
children: [
{ name: "Google", value: 1, image: "https://picsum.photos/203" },
{
name: "Firefox",
value: 1,
image: "https://picsum.photos/204",
},
{
name: "IE",
value: 1,
image: "https://picsum.photos/203",
},
{
name: "Safari",
value: 1,
image: "https://picsum.photos/205",
},
{
name: "Opera",
value: 1,
image: "https://picsum.photos/206",
},
],
},
]);
series.set("selectedDataItem", series.dataItems[0]);
return () => {
root.dispose();
};
}, []);
return <div id="chartdiv" style={{ width: "100%", height: "500px" }}></div>;
}
export default App;
I was able to find some workaround using pinBullets in amhcarts 4 but I'm trying to get it working on amcharts 5.

How to render custom svg in IconLayer of Deck.gl

i am trying to render a custom svg in iconLayer when a particular condition is met, from this example, https://deck.gl/gallery/icon-layer my getIcon option is
getIcon: (d) => ({
url: './glow.svg',
width: 128,
height: 128
}),
I think the url is meant to be the path to the image i want to render
However, nothing is been shown on the map.
the full component is
import { IconLayer } from "#deck.gl/layers";
import icons from "../assets/images/monsoon_icons.png";
const COLORS = {
...colors
};
const ICON_MAPPING = {
circle: { x: 0, y: 0, width: 70, height: 70, mask: true },
rectangle: { x: 70, y: 0, width: 70, height: 70, mask: true },
triangle: { x: 140, y: 0, width: 70, height: 70, mask: true },
star: { x: 210, y: 0, width: 70, height: 70, mask: true },
};
export const RenderLayers = (props) => {
let { data } = props;
if(props.isSaved) {
return [
new IconLayer({
...other icon props
// iconMapping: ICON_MAPPING,
getIcon: (d) => ({
url: './glow.svg',
width: 128,
height: 128
}),
...other icon props,
}),
new IconLayer({
...other icon props
// iconMapping: ICON_MAPPING,
getIcon: (d) => ({
url: './glow.svg',
width: 128,
height: 128
}),
...other icon props,
}),
];
};
if (!data?.length) {
console.log("NO DATA TO MAP");
return;
};
return [
new IconLayer({
...normal icon layer props
})
];
};
You would need to convert your svg to data URLs.
Following the official deck.gl example:
import yourSvg from 'whatever/path';
function svgToDataURL(svg) {
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
}
new IconLayer({
getIcon: () => ({
url: svgToDataURL(yourSvg),
width: 24,
height: 24
}),
})
Also, you can check a full working example here.
Cheers!

React, ApexCharts, Radial Bar series value from data

I'm new to React and trying to learn. My personal project is homebrewing display site.
I want to show some of the details in radial bar. I followed tutorial and made array data file, and index file where details show. Now I'm stuck getting single values to display in radial bar.
Also values need to be calculated from percent to numeric. I got that working. I tried some push functions but did not get it to work way I wanted. My coding skills are beginner level.
display example
data.js
import product1 from '../../images/product-1.png';
import product2 from '../../images/product-1.png';
export const productData = [
{
id: 1,
img: product1,
alt: 'Beer',
name: 'Lager',
desc: 'Saaz',
ibu: '35',
ebc: '40',
abv: '8.5',
},
{
id: 2,
img: product2,
alt: 'Beer',
name: 'IPA',
desc: 'Mango, Citrus',
ibu: '85',
ebc: '25',
abv: '5.5',
},
];
index.js
import React, { Component } from 'react';
import Chart from 'react-apexcharts';
import {
ProductsContainer,
ProductWrapper,
ProductsHeading,
ProductTitle,
ProductCard,
ProductImg,
ProductInfo,
ProductDesc,
ProductIbu,
ProductEbc,
ProductAbv,
} from './ProductsElements';
const max = 119;
function valueToPercent(value) {
return (value * 100) / max;
}
class IBU extends Component {
constructor(props) {
super(props);
this.state = {
series: [valueToPercent(15)],
options: {
plotOptions: {
radialBar: {
startAngle: -90,
endAngle: 90,
hollow: {
margin: 10,
size: '70%',
background: '#222222',
image: undefined,
imageOffsetX: 0,
imageOffsetY: 0,
position: 'front',
dropShadow: {
enabled: true,
top: 5,
left: 0,
blur: 4,
opacity: 0.9,
},
},
track: {
background: '#d42d2d',
strokeWidth: '67%',
margin: 0, // margin is in pixels
dropShadow: {
enabled: true,
top: 0,
left: 0,
blur: 4,
color: '#000',
opacity: 0.6,
},
},
dataLabels: {
show: true,
name: {
offsetY: -10,
show: true,
color: '#fff',
fontSize: '17px',
},
value: {
formatter: function (value) {
return (parseFloat(value) * max) / 100;
},
color: '#dadada',
fontSize: '36px',
show: true,
},
},
},
},
fill: {
colors: [
function ({ value, seriesIndex, w }) {
if (value < 55) {
return '#7E36AF';
} else if (value >= 55 && value < 80) {
return '#164666';
} else {
return '#D9534F';
}
},
],
},
stroke: {
lineCap: 'round',
},
labels: ['IBU'],
},
};
}
render() {
return <Chart options={this.state.options} series={this.state.series} type="radialBar" height={250} />;
}
}
const Products = ({ data }) => {
return (
<ProductsContainer>
<ProductsHeading>heading</ProductsHeading>
<ProductWrapper>
{data.map((product, index) => {
return (
<ProductCard key={index}>
<ProductImg src={product.img} alt={product.alt} />
<ProductInfo>
<ProductTitle>{product.name}</ProductTitle>
<ProductDesc>{product.desc}</ProductDesc>
<ProductIbu>{product.ibu}</ProductIbu>
<IBU></IBU>
<ProductEbc>{product.ebc}</ProductEbc>
<ProductAbv>{product.abv}</ProductAbv>
</ProductInfo>
</ProductCard>
);
})}
</ProductWrapper>
</ProductsContainer>
);
};
export default Products;

using react-chartjs-2 , How can I save my chart as png using a download button

I'm trying to download my chart.js charts as png using a button Onclick, but I have no idea how I'm going to achieve this , I've went through this answer React-chartjs-2 Doughnut chart export to png but it wasn't quite clear enough for me as I'm quite new in chart.js don't know how I'm going to connect those variables with my button.
import React from 'react';
import { Component, useRef } from 'react';
import { Bar } from 'react-chartjs-2';
import 'chartjs-plugin-datalabels';
const data = {
labels: ['Finance & Business', 'Mining', 'Community Services', 'Electricity', 'Agriculture', 'Construction', 'Manufacture', "Trade & Tourism", "Transport & Logistics"],
datasets: [
{
label: 'My First dataset',
backgroundColor: ["#3283FC", "", "", "#00C0C8", "#C0BD00", "#3A46B1", "#00A150", "#FEB200", "#9302a1"],
borderWidth: 1,
hoverBackgroundColor: 'rgba(255,99,132,0.4)',
hoverBorderColor: 'rgba(255,99,132,1)',
data: [0.6, 0.0, 0.0, -0.1, -0.1, -0.3, -0.3, -0.6, -1.0],
}
]
};
class StackedBar extends Component {
render() {
return (
<div>
<h2>Bar Example (custom size)</h2>
<Bar
data={data}
options={{
plugins: {
datalabels: {
display: true,
color: '#fff'
}
},
title: {
display: true,
text: 'Contribution Percentage',
position: 'left'
},
maintainAspectRatio: true,
scales: {
xAxes: [{
stacked: true,
gridLines: {
borderDash: [2, 6],
color: "black"
},
scales: {
}
}],
yAxes: [{
ticks: {
beginAtZero: true,
steps: 0.5,
stepSize: 0.5,
max: 1.5,
min: -1.0
},
}]
},
}}
/>
</div>
);
}
}
export default StackedBar;
So I installed a plugin called FileSave.js //
npm install
npm i file-saver
import the plugin
import { saveAs } from 'file-saver';
than just write this blob function
class StackedBar extends Component {
saveCanvas() {
//save to png
const canvasSave = document.getElementById('stackD');
canvasSave.toBlob(function (blob) {
saveAs(blob, "testing.png")
})
}
render() {
return (
<div>
<a onClick={this.saveCanvas}>Download as PNG</a>
<Bar id="stackD" data={data} options={options} />
</div>
);
}
}
export default StackedBar;
another option is to use the chart ref, and then use the chart.js toBase64Image function. save this out as base64, convert to blob and save as a file using the file-saver package.
import { saveAs } from 'file-saver';
/**
* #param b64Data
* #param contentType
* #param sliceSize
* #returns {Blob}
* #link https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript
*/
const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => {
const byteCharacters = atob(b64Data);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
// eslint-disable-next-line no-plusplus
for (let i = 0; i < slice.length; i += 1) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: contentType });
};
... in component
const chartRef = useRef(null);
... in jsx...
<Button
onClick={() => {
const b64 = chartRef.current.toBase64Image().replace('data:image/png;base64,', '');
const content = b64toBlob(b64);
const file = new File([content], 'Revenue_chart.png', { type: 'image/png' });
saveAs(file);
}}
>
img
</Button>
<Line data={data} options={options} ref={chartRef} />

Resources