Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heatmap, color customisation and marker aggregation #7

Merged
merged 16 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Geo Location Map Visualization is a dynamic web application designed to visually
![Visualization Overview with status](./docs/status-screenshot.jpg)
![Visualisation showing Regions only](./docs/screenshot_regions.jpg)
![Visualisation showing regions and markers together](./docs/screenshot_regionsandmarkers.jpg)
![Visualisation showing heatmap onUS region](./docs/heatmap-usa.jpg)

## Features

Expand All @@ -16,7 +17,8 @@ Geo Location Map Visualization is a dynamic web application designed to visually
- **Interactive Map Clusters:** Locations are clustered on the map for a cleaner visual representation. Clicking on a cluster zooms into that area, revealing individual markers. Clusters also provide a summary representation of the underlying marker status.
- **Real-time Data Updates:** The application periodically fetches and updates data, ensuring the information displayed is current and accurate.
- **Flexible Configuration:** Easily configure via configuration options and query values.
- **Country regions:** Display whole countries with threshold colourings.
- **Country Regions:** Display whole countries with threshold colourings.
- **Regional Heatmaps:** Dynamicaly shade regions to generate heat maps based on value.

## Prerequisites

Expand Down Expand Up @@ -69,6 +71,8 @@ The following options can be configured using the visualization configuration pa

- **Account ID:** Choose the account you wish the query to work against. (The custom visualization needs to be deplopyed to all accounts that you require data from.)
- **Markers query:** This is an NRQL query that returns the markers to render on the map. You must supply a longitude and latitude value for each location, along with the data to render. See below for more details on the query structure.
- **Markers colors**: Allows you to override the colours used for markers states. Specify hex colors in a comma seperated list in the order: cluster,no-status,ok,warning,critical
- **Regions query:** This is an NRQL query that returns the regions to render on the map. You must supply a valid region field. See below for more details on the query structure.
- **Default Since/Until:** The since.until clause to use when no picker value (i.e. default) is selected.
- **Ignore time picker:** If checked changes to the time picker will not be applied to the maerk query.
- **Default zoom:** This allows you to select how zoomed in the map is when it first loads.
Expand Down Expand Up @@ -96,17 +100,21 @@ Regions can be rendered as an alternative or in additon to markers. Use the same
- **`geoUSState`:** A US state 2 letter code, number or name
- **`geoUKRegion`:** A Uk Region name
- **`tooltip_header`**: By default the country name is displayed as tool tip header. You can override by supplying a value here. Specify empty string or NONE to remove the header entirely.
- **`custom_color`:** Provide a hex color code for this region (overrides all other colors)

More details regarding region setup can be found [here](./visualizations/store-map-viz/geo/readme.md).

#### Precision, prefix and suffix

Its possible to specify the precision of numbers and add prefix/suffix to values. These adjustments can be made to the `icon_label`` and `tooltip_xxx`` fields by providing extra fields:
Its possible to specify the precision of numbers and add prefix/suffix to values. These adjustments can be made to the `icon_label` and `tooltip_xxx` fields by providing extra fields:

- **`_precision`:** Sets the number of decimal places to display. e.g. `select ... 2 as 'icon_label_precision'...`
- **`_prefix`:** Adds a prefix to the value. e.g. `select ... '$' as 'tooltip_sales_prefix' ...`
- **`_suffix`:** Adds a suffix to the value. e.g. `select ... 'rpm' as 'tooltip_thoughput_suffix' ...`

You may also override the default precision (2) of the icon value by setting `value_precision` if you are not providing an icon label.


#### Example Query

Here is a basic example query, that utilises a lookup table (containing storeID/lat/lng/city) to supply the latitude, longitude to the data:
Expand Down
Binary file added docs/heatmap-usa.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 26 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"private": true,
"name": "location-geo-map",
"version": "2.2.5",
"version": "2.3.0",
"scripts": {
"start": "nr1 nerdpack:serve",
"test": "exit 0"
"prettier": "prettier --write ."
},
"dependencies": {
"javascript-color-gradient": "^2.4.4",
"leaflet": "^1.6.0",
"prop-types": "15.7.2",
"react": "17.0.2",
Expand All @@ -20,5 +21,8 @@
"last 2 versions",
"not ie < 11",
"not dead"
]
],
"devDependencies": {
"prettier": "3.2.5"
}
}
11 changes: 7 additions & 4 deletions visualizations/store-map-viz/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Map, TileLayer } from "react-leaflet";

import Markers from "./Markers";
import Regions from "./Regions";
import { useStoreMap } from "../context/StoreMapContext";
import { useMap } from "../context/MapContextProvider";

// there are some issues with the default zoom and center from the context
// so just in case we'll set them here
Expand All @@ -25,10 +25,11 @@ L.Marker.prototype.options.icon = DefaultIcon;

const MapView = () => {
// get the zoom and center from the context (StoreMapContext.tsx)
const storeMap = useStoreMap();
const mapProps = useMap();
// Handle null values explicitly
const zoom = storeMap.zoom !== null ? storeMap.zoom : DEFAULT_ZOOM;
const center = storeMap.center !== null ? storeMap.center : DEFAULT_CENTER;
const zoom = mapProps.zoom !== null ? mapProps.zoom : DEFAULT_ZOOM;
const center = mapProps.center !== null ? mapProps.center : DEFAULT_CENTER;
const noWrap = mapProps.noWrap;

// use ref for the map to refresh it in Viz's config mode
const mapRef = useRef(null);
Expand All @@ -51,6 +52,8 @@ const MapView = () => {
return (
<Map ref={mapRef} center={center} zoom={zoom} style={mapStyle}>
<TileLayer
key={noWrap}
noWrap={noWrap}
attribution='&copy; <a href="http://osm.org/copyright">Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL.'
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
/>
Expand Down
118 changes: 39 additions & 79 deletions visualizations/store-map-viz/components/Markers.tsx
Original file line number Diff line number Diff line change
@@ -1,87 +1,34 @@
import React, { useState, useEffect, useContext } from "react";
import React, { useState, useEffect, useRef } from "react";
import { Marker } from "react-leaflet";
import MarkerClusterGroup from "react-leaflet-markercluster";
import { NerdGraphQuery, PlatformStateContext } from "nr1";
import { useNerdGraphQuery } from "../hooks/useNerdGraphQuery";
import { useCustomColors, Status } from "../hooks/useCustomColors";

import { nerdGraphMarkerQuery } from "../queries";
import { FETCH_INTERVAL_DEFAULT } from "../constants";
import {
createClusterCustomIcon,
createCustomIcon,
generateTooltipConfig,
} from "../utils/map";
} from "../utils";
import LocationPopup from "./LocationPopup";
import { useProps } from "../context/VizPropsProvider";
import { DEFAULT_DISABLE_CLUSTER_ZOOM, MARKER_COLOURS } from "../constants";

import { deriveStatus, formatValues } from "../utils/dataFormatting";
import { DEFAULT_DISABLE_CLUSTER_ZOOM } from "../constants";

const Markers = () => {
// const nerdletState = useContext(NerdletStateContext);
const {
accountId,
markersQuery,
disableClusterZoom,
fetchInterval,
ignorePicker,
defaultSince,
} = useProps();
const defSinceString =
defaultSince === undefined || defaultSince === null
? ""
: " " + defaultSince;
const { markersQuery, disableClusterZoom, markerColors, markerAggregation } =
useProps();

if (markersQuery === null || markersQuery === undefined) {
return null;
}
const { data: locations, lastUpdateStamp } = useNerdGraphQuery(markersQuery);

// timeRange formatting happens in the query (nerdGraphMarkerQuery)
const { timeRange } = useContext(PlatformStateContext);
const { customColors } = useCustomColors(markerColors);
const customColorsRef = useRef(customColors);

const [locations, setLocations] = useState([]);
useEffect(() => {
const fetchData = async () => {
const query = nerdGraphMarkerQuery(
markersQuery,
timeRange,
defSinceString,
ignorePicker
);
const variables = { id: parseInt(accountId) };

try {
const response = await NerdGraphQuery.query({ query, variables });
const results = response?.data?.actor?.account?.markers?.results;
if (results && Array.isArray(results)) {
results.forEach((location) => {
deriveStatus(location);
formatValues(location);
});
}
setLocations(response?.data?.actor?.account?.markers?.results);
} catch (error) {
console.error("Error fetching data:", error);
// Handle error appropriately
}
};

// Perform the immediate fetch to populate the initial data
fetchData();

// Then set an interval to continue fetching
const fetchIntervalms = (fetchInterval || FETCH_INTERVAL_DEFAULT) * 1000;

if (fetchIntervalms >= 1000) {
const intervalId = setInterval(fetchData, fetchIntervalms);
// Clear the interval when the component unmounts
return () => clearInterval(intervalId);
} else {
return null;
}
}, [timeRange, fetchInterval]);
customColorsRef.current = customColors;
// Update the renderKey when customColors or markerAggregation changes
setRenderKey(Math.random());
}, [customColors, markerAggregation,lastUpdateStamp]);

// This is a hack to force a re-render when markers show up for the first time.
// Without this, the createCustomIcon icon (/utils/map.tsx) does not render as expected.
const [renderKey, setRenderKey] = useState(Math.random());
useEffect(() => {
if (locations) {
Expand All @@ -94,29 +41,42 @@ const Markers = () => {
if (locations === undefined) {
return null;
}

const getPoligonOptions = () => ({
fillColor: customColors[Status.CLUSTER].borderColor,
color: customColors[Status.CLUSTER].color,
weight: 3,
opacity: 0.9,
fillOpacity: 0.4,
});

return (
<MarkerClusterGroup
key={`${markerAggregation}-${lastUpdateStamp}`}
singleMarkerMode={true}
spiderfyOnMaxZoom={7}
disableClusteringAtZoom={
disableClusterZoom === "default"
? DEFAULT_DISABLE_CLUSTER_ZOOM
: disableClusterZoom
}
iconCreateFunction={createClusterCustomIcon}
polygonOptions={{
fillColor: MARKER_COLOURS.groupBorder,
color: MARKER_COLOURS.groupBorder,
weight: 3,
opacity: 0.9,
fillOpacity: 0.4,
iconCreateFunction={(cluster) => {
return createClusterCustomIcon(
cluster,
customColorsRef.current,
markerAggregation,
);
}}
polygonOptions={getPoligonOptions()}
>
{locations.map((location) => (
<Marker
key={location.storeNumber}
{locations.map((location,idx) => {
if(isNaN(location?.latitude) || isNaN(location?.longitude)) {
return null;
}
return (<Marker
key={`${idx}-${location.value}-${lastUpdateStamp}`}
position={[location.latitude, location.longitude]}
icon={createCustomIcon(location)}
icon={createCustomIcon(location, customColors)}
onClick={() => {
if (location.link) {
window.open(location.link, "_blank");
Expand All @@ -125,7 +85,7 @@ const Markers = () => {
>
<LocationPopup location={location} config={tooltipConfig} />
</Marker>
))}
);})}
</MarkerClusterGroup>
);
};
Expand Down
Loading
Loading