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 5 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,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,6 +98,7 @@ 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).

Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion 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.2.10",
"scripts": {
"start": "nr1 nerdpack:serve",
"test": "exit 0"
},
"dependencies": {
"javascript-color-gradient": "^2.4.4",
"leaflet": "^1.6.0",
"prop-types": "15.7.2",
"react": "17.0.2",
Expand Down
18 changes: 12 additions & 6 deletions visualizations/store-map-viz/components/Markers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const Markers = () => {
fetchInterval,
ignorePicker,
defaultSince,
markerColors,
markerAggregation
} = useProps();
const defSinceString =
defaultSince === undefined || defaultSince === null
Expand All @@ -35,11 +37,14 @@ const Markers = () => {
return null;
}

const customColors = markerColors && markerColors!="" ? markerColors.split(",") : [];
// timeRange formatting happens in the query (nerdGraphMarkerQuery)
const { timeRange } = useContext(PlatformStateContext);

const [locations, setLocations] = useState([]);

useEffect(() => {
setLocations([]);
const fetchData = async () => {
const query = nerdGraphMarkerQuery(
markersQuery,
Expand Down Expand Up @@ -78,7 +83,7 @@ const Markers = () => {
} else {
return null;
}
}, [timeRange, fetchInterval]);
}, [timeRange, fetchInterval, markersQuery]);

// 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.
Expand All @@ -94,19 +99,20 @@ const Markers = () => {
if (locations === undefined) {
return null;
}

return (
<MarkerClusterGroup
<MarkerClusterGroup key={markerAggregation}
singleMarkerMode={true}
spiderfyOnMaxZoom={7}
disableClusteringAtZoom={
disableClusterZoom === "default"
? DEFAULT_DISABLE_CLUSTER_ZOOM
: disableClusterZoom
}
iconCreateFunction={createClusterCustomIcon}
iconCreateFunction={(c)=>{ return createClusterCustomIcon(c,customColors,markerAggregation);}}
polygonOptions={{
fillColor: MARKER_COLOURS.groupBorder,
color: MARKER_COLOURS.groupBorder,
fillColor: customColors[0] ? customColors[0]+"70" : MARKER_COLOURS.groupBorder,
color: customColors[0] ? customColors[0]+"70" : MARKER_COLOURS.groupBorder,
weight: 3,
opacity: 0.9,
fillOpacity: 0.4,
Expand All @@ -116,7 +122,7 @@ const Markers = () => {
<Marker
key={location.storeNumber}
position={[location.latitude, location.longitude]}
icon={createCustomIcon(location)}
icon={createCustomIcon(location,customColors)}
onClick={() => {
if (location.link) {
window.open(location.link, "_blank");
Expand Down
40 changes: 31 additions & 9 deletions visualizations/store-map-viz/components/Region.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,39 @@ import { GeoJSON } from "react-leaflet";
import { regionStatusColor } from "../utils/map";
import LocationPopup from "./LocationPopup";

const Region = ({ region, location, tooltipConfig, defaultHeader }) => {
// memoize to avoid unnecessary recalculations
const Region = ({ key, region, location, tooltipConfig, defaultHeader, heatMap, heatMapSteps, customColors }) => {

const style = useMemo(
() => ({
color: regionStatusColor(location.status).borderColor,
fillColor: regionStatusColor(location.status).color,
opacity: 0.5,
}),
[location.status],
() => {

if(location.custom_color) {
return ({
color: location.custom_color,
fillColor: location.custom_color,
opacity: 0.5,
fillOpacity: 0.7
});
}
else if(heatMap != null) {
return ({
color: heatMap(location.value),
fillColor: heatMap(location.value),
opacity: 0.5,
fillOpacity: 0.7
});
} else {
return ({
color: regionStatusColor(location.status,customColors).borderColor,
fillColor: regionStatusColor(location.status,customColors).color,
opacity: 0.7,
});
}

},
[location.value,heatMapSteps,customColors],
);


// determine the tooltip title, memoized to avoid unnecessary recalculations
const getTooltipTitle = () => {
if (location.tooltip_header === "NONE" || location.tooltip_header === "") {
Expand All @@ -34,7 +56,7 @@ const Region = ({ region, location, tooltipConfig, defaultHeader }) => {
};

return (
<GeoJSON key={location.status} data={region} style={style} onClick={handleRegionClick}>
<GeoJSON key={key+"-"+location.value+"-"+style.fillColor} data={region} style={style} onClick={handleRegionClick}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this so that if the color changes it re-renders, is there a better way?

<LocationPopup
location={location}
config={tooltipConfig}
Expand Down
72 changes: 61 additions & 11 deletions visualizations/store-map-viz/components/Regions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect, useContext, useState } from "react";
import { PlatformStateContext, NerdGraphQuery } from "nr1";

import { nerdGraphMarkerQuery } from "../queries";
import { FETCH_INTERVAL_DEFAULT } from "../constants";
import { FETCH_INTERVAL_DEFAULT, MARKER_COLOURS } from "../constants";

import { generateTooltipConfig } from "../utils/map";
import { deriveStatus, formatValues } from "../utils/dataFormatting";
Expand All @@ -13,23 +13,33 @@ import countries from "../geo/countries.geojson.json";
import geoUSStates from "../geo/us-states/us-states";
import allUKRegions from "../geo/uk-regions/all-uk-regions";

import Gradient from "javascript-color-gradient";

const Regions = () => {
const { accountId, regionsQuery, fetchInterval, ignorePicker, defaultSince } =
useProps();
const { accountId, regionsQuery, fetchInterval, ignorePicker, defaultSince, heatMapSteps, regionColors } = useProps();

const [regions, setRegions] = useState([]);
const [HMSteps, setHMSteps] = useState(0);
const [HMColors,setHMColors] = useState([]);
const [customColors,setCustomColors] = useState(null);


const defSinceString =
defaultSince === undefined || defaultSince === null
? ""
: " " + defaultSince;
if (regionsQuery === null || regionsQuery === undefined) {
return null;
}

const { timeRange } = useContext(PlatformStateContext);

useEffect(() => {

const defSinceString =
defaultSince === undefined || defaultSince === null
? ""
: " " + defaultSince;
if (regionsQuery === null || regionsQuery === undefined) {
return null;
}

setHMSteps(heatMapSteps && heatMapSteps!="" ? parseInt(heatMapSteps) : 0);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This state setting was all to try and force a re-render whne config options changed. It seemed unnecessary to me but i couldnt find anbother way to trigger the re-render on prop change.

setHMColors(regionColors && regionColors!="" ? regionColors.split(",") : MARKER_COLOURS.heatMapDefault);
setCustomColors(regionColors && regionColors!="" ? regionColors.split(",") : null);
const fetchData = async () => {
const query = nerdGraphMarkerQuery(
regionsQuery,
Expand Down Expand Up @@ -68,7 +78,7 @@ const Regions = () => {
} else {
return null;
}
}, [timeRange, fetchInterval]);
}, [timeRange, fetchInterval, heatMapSteps,regionColors,regionsQuery]);

if (!regions || regions.length == 0) {
return null; //no regions to display
Expand All @@ -77,6 +87,34 @@ const Regions = () => {

let geoFeatureLocations = [];

// ---- heat map configuration -------

const gradientSteps=HMSteps;
let getGradientColor=null;

if(gradientSteps > 0) {
let maxValue=-Infinity, minValue=Infinity;
regions.forEach(location=>{
maxValue = location.value > maxValue ? location.value : maxValue;
minValue = location.value < minValue ? location.value : minValue;
})

if(HMColors.length > 1) {

const gradientArray = new Gradient()
.setColorGradient(...HMColors)
.setMidpoint(gradientSteps)
.getColors();

getGradientColor = (value) => {
let ratio = (value - minValue) / (maxValue-minValue);
let element = Math.floor((gradientSteps-1)*ratio);
return gradientArray[element];
}
}
}


regions.forEach((location, index) => {
// World Countries
if (location.geoISOCountry && location.geoISOCountry != "") {
Expand All @@ -94,6 +132,10 @@ const Regions = () => {
location={location}
tooltipConfig={tooltipConfig}
defaultHeader={feature.properties.ADMIN}
heatMap={getGradientColor}
heatMapSteps={gradientSteps}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theres probbaly a better way to do this. In order for the region color to update when the config changed i had to pass ll this information over to use in the useEffect trigger. I cant help but feel the gradient generator function should live within Region.tsx, but its here because it needs information form all the regions for the gradient bucketing to be derived.

heatMapColors={HMColors}
customColors={customColors}
/>,
);
} else {
Expand All @@ -118,6 +160,10 @@ const Regions = () => {
location={location}
tooltipConfig={tooltipConfig}
defaultHeader={feature.properties.NAME}
heatMap={getGradientColor}
heatMapSteps={gradientSteps}
heatMapColors={HMColors}
customColors={customColors}
/>,
);
} else {
Expand All @@ -139,6 +185,10 @@ const Regions = () => {
location={location}
tooltipConfig={tooltipConfig}
defaultHeader={region.name}
heatMap={getGradientColor}
heatMapSteps={gradientSteps}
heatMapColors={HMColors}
customColors={customColors}
/>,
);
});
Expand Down
3 changes: 3 additions & 0 deletions visualizations/store-map-viz/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,8 @@ export const MARKER_COLOURS = {
groupColour: "#757575", //grey for group
groupBorder: "#75757570", //border, including transparency
groupText: "#fff",

heatMapDefault: [ "#420052","#6C0485","#8F18AC","#FFBE35","#FFA022"],
heatMapStepsDefault: 50
};
export const DEFAULT_DISABLE_CLUSTER_ZOOM = "7"; // default disbale cluserting level
46 changes: 46 additions & 0 deletions visualizations/store-map-viz/nr1.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,58 @@
"description": "Query to gather markers",
"type": "nrql"
},
{
"name": "markerAggregation",
"title": "Marker aggregation",
"description": "How should clustered markers be treated",
"type": "enum",
"items": [
{
"title": "Show count of markers",
"value": "count"
},
{
"title": "Show sum of markers",
"value": "sum"
},
{
"title": "Show mean average of markers",
"value": "average"
},
{
"title": "Show max of markers",
"value": "max"
},
{
"title": "Show min of markers",
"value": "min"
}
]
},
{
"name": "markerColors",
"title": "Marker colors",
"description": "A comma seperated list of hex colours in the order: cluster,no-status,ok,warning,critical",
"type": "string"
} ,
{
"name": "regionsQuery",
"title": "Regions query",
"description": "Query to gather reqgions",
"type": "nrql"
},
{
"name": "heatMapSteps",
"title": "Heatmap steps",
"description": "The nnumber of steps in the heat map gradient. Empty or 0 disables heat map.",
"type": "string"
} ,
{
"name": "regionColors",
"title": "Region colors",
"description": "A comma seperated list of hex colours. Used to create heatmap or control threshold coloring. For critical/warning states provide no-status,ok,warning,critical ",
"type": "string"
} ,
{
"name": "defaultSince",
"title": "Default since/until clause",
Expand Down
Loading
Loading