Skip to content

Commit

Permalink
Heatmap, color customisation and marker aggregation (#7)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: matewilk <matewilk@gmail.com>
  • Loading branch information
jsbnr and matewilk committed Apr 25, 2024
1 parent 0d2434e commit 0c6cd15
Show file tree
Hide file tree
Showing 22 changed files with 733 additions and 427 deletions.
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

0 comments on commit 0c6cd15

Please sign in to comment.