Was thinking that it would be nice to be able to set a range of colors for a heatmap instead of just two (min and max). Like what we do for the gradient list.
Something like...
function am4themes_myHeatmap(target) {
if (target instanceof am4core.ColorSet) {
target.list = [
am4core.color("#F7E3D4"),
am4core.color("#FFC480"),
am4core.color("#DC60BF"),
am4core.color("#A43B7D"),
am4core.color("#5B0A25")
];
}
}
See example from mockup
If something like this already exists, I would the love to see it.
Something like this didn't exist.
Unfortunately there's not a simple way to just use heatRules
and HeatLegend
and have them use additional colors. But it's not overly complicated to emulate heatRules
and if you're only using 1 marker
in your HeatLegend
(i.e. one long bar as opposed to multiple bars), to override its gradient with a custom one.
I grabbed 2 colors from the image you provided and threw those and black in an array:
var heatColors = [
am4core.color("rgb(248, 227, 211)"),
am4core.color("rgb(237, 137, 166)"),
am4core.color("rgb(0,0,0)")
];
It's not necessary, but can be useful. Those are the colors of a 3-color gradient. I elected to use 3 colors so we can split the calculations evenly between left/right halves of the gradient, it ought to simplify the demo below. The picture you shared might require an additional color in the left half, you would have to adjust calculations accordingly, but it's just as doable.
To emulate heatRules
, we'll provide an adapter for mapPolygons
' fill
. In there, we'll compare a mapPolygon
's value
against the min/max of the values, the latter can be found via the series' dataItem.values["value"].low
and .high
. This will give us a percentage in decimals to grab a color from a range of colors. The utility function to pick a color from a range is am4core.colors.interpolate
, its first two arguments are iRGB
s (plain object with r
, g
, b
, and a
properties/values), and the third is the percentage in decimals. If the percentage is within the first half, we'll have the adapter return a color between the first two in heatColors
above, if it's in the second half, we'll return a color from the latter two.
Here's what that code looks like:
polygonSeries.mapPolygons.template.adapter.add("fill", function(
fill,
mapPolygon
) {
var workingValue = mapPolygon.dataItem.values["value"].workingValue;
var minValue = polygonSeries.dataItem.values["value"].low;
var maxValue = polygonSeries.dataItem.values["value"].high;
var percent = (workingValue - minValue) / (maxValue - minValue);
if (am4core.type.isNumber(percent)) {
if (percent > 0.5) {
return new am4core.Color(
am4core.colors.interpolate(
heatColors[1].rgb,
heatColors[2].rgb,
(percent - 0.5) * 2
)
);
} else {
return new am4core.Color(
am4core.colors.interpolate(
heatColors[0].rgb,
heatColors[1].rgb,
percent * 2
)
);
}
}
return fill;
});
If you have a 1-marker heatLegend
, i.e. just a bar with a gradient going across, you can make your own gradient and assign it in an adapter, too:
var gradient = new am4core.LinearGradient();
heatColors.forEach(function(color) {
gradient.addColor(color);
});
heatLegend.markers.template.adapter.add("fill", function() {
return gradient;
});
If you have multiple markers in a heatLegend
(as per the top heat legend in your picture), custom coloring would be more like what we did for the heatRules
, except instead of an adapter, because we need to know their place and there's no dataItem
or index
available, we'll iterate through the markers
once they're ready and then override their colors there:
var heatLegendTop = chart.createChild(am4maps.HeatLegend);
heatLegendTop.series = polygonSeries;
heatLegendTop.minColor = heatColors[0];
heatLegendTop.maxColor = heatColors[2];
heatLegendTop.marginBottom = 10;
heatLegendTop.markerCount = 10;
heatLegendTop.events.on("inited", function() {
heatLegendTop.markers.each(function(marker, markerIndex) {
// Gradient colors!
if (markerIndex < heatLegendTop.markerCount / 2) {
marker.fill = new am4core.Color(
am4core.colors.interpolate(
heatColors[0].rgb,
heatColors[1].rgb,
(markerIndex / heatLegendTop.markerCount) * 2
)
);
} else {
marker.fill = new am4core.Color(
am4core.colors.interpolate(
heatColors[1].rgb,
heatColors[2].rgb,
((markerIndex - heatLegendTop.markerCount / 2) /
heatLegendTop.markerCount) *
2
)
);
}
});
});
I forked our US heat (choropleth) map demo with the above code and then some to get closer to the look/feel of the image you shared:
https://codepen.io/team/amcharts/pen/7fd84c880922a6fc50f80330d778654a
I simplified the demo and made it responsive: https://codepen.io/team/amcharts/pen/eYJZVEV
// Themes begin
am4core.useTheme(am4themes_animated);
// Themes end
// Create map instance
var chart = am4core.create("chartdiv", am4maps.MapChart);
// Set map definition
chart.geodata = am4geodata_usaAlbersLow;
// Set projection
chart.projection = new am4maps.projections.Miller();
// Create map polygon series
var polygonSeries = chart.series.push(new am4maps.MapPolygonSeries());
// Make map load polygon data (state shapes and names) from GeoJSON
polygonSeries.useGeodata = true;
// //Set min/max fill color for each area
// polygonSeries.heatRules.push({
// property: "fill",
// target: polygonSeries.mapPolygons.template,
// min: chart.colors.getIndex(1).brighten(1),
// max: chart.colors.getIndex(1).brighten(-0.3)
// });
// make room for heatLegends
chart.chartContainer.paddingBottom = 50;
// Base colors for custom "heatRules" gradient
var heatColors = [
am4core.color("rgb(248, 227, 211)"),
am4core.color("rgb(237, 137, 166)"),
am4core.color("rgb(0,0,0)")
];
// Let hover state colors be relative to the "heatRule" color
var hoverState = polygonSeries.mapPolygons.template.states.create("hover");
hoverState.adapter.add("fill", function(fill) {
return fill.lighten(-0.1);
});
// Emulate heatRule but with 2 color ranges instead of 1
polygonSeries.mapPolygons.template.adapter.add("fill", function(
fill,
mapPolygon
) {
var workingValue = mapPolygon.dataItem.values["value"].workingValue;
var minValue = polygonSeries.dataItem.values["value"].low;
var maxValue = polygonSeries.dataItem.values["value"].high;
var percent = (workingValue - minValue) / (maxValue - minValue);
// This may run before workingValue is even a thing. Let's only do our thing
// if workingValue and ergo percent are a thing.
if (am4core.type.isNumber(percent)) {
if (percent > 0.5) {
return new am4core.Color(
am4core.colors.interpolate(
heatColors[1].rgb,
heatColors[2].rgb,
(percent - 0.5) * 2
)
);
} else {
return new am4core.Color(
am4core.colors.interpolate(
heatColors[0].rgb,
heatColors[1].rgb,
percent * 2
)
);
}
}
return fill;
});
// Set up heat legends
var heatLegendTop = chart.createChild(am4maps.HeatLegend);
heatLegendTop.series = polygonSeries;
heatLegendTop.align = "center";
heatLegendTop.width = am4core.percent(38);
heatLegendTop.minValue = 0;
heatLegendTop.maxValue = 40000000;
heatLegendTop.minColor = heatColors[0];
heatLegendTop.maxColor = heatColors[2];
heatLegendTop.marginBottom = 10;
heatLegendTop.markerCount = 10;
heatLegendTop.markerContainer.minHeight = 10;
heatLegendTop.markers.template.minHeight = 10;
var markerWidth = 20;
heatLegendTop.events.on("inited", function() {
heatLegendTop.markers.each(function(marker, markerIndex) {
// Override default heatLegend functionality
marker.width = markerWidth;
// Distribute the space, this needs to be repeated e.g. on window resize,
// orientation change, etc.
if (markerIndex < heatLegendTop.markers.length - 1) {
marker.marginRight =
(heatLegendTop.markerContainer.pixelWidth -
heatLegendTop.markerCount * markerWidth) /
(heatLegendTop.markerCount - 1);
}
// Gradient colors!
if (markerIndex < heatLegendTop.markerCount / 2) {
marker.fill = new am4core.Color(
am4core.colors.interpolate(
heatColors[0].rgb,
heatColors[1].rgb,
(markerIndex / heatLegendTop.markerCount) * 2
)
);
} else {
marker.fill = new am4core.Color(
am4core.colors.interpolate(
heatColors[1].rgb,
heatColors[2].rgb,
((markerIndex - heatLegendTop.markerCount / 2) /
heatLegendTop.markerCount) *
2
)
);
}
});
});
// Blank out internal heat legend value axis labels
heatLegendTop.valueAxis.renderer.labels.template.disabled = true;
var heatLegend = chart.createChild(am4maps.HeatLegend);
heatLegend.series = polygonSeries;
heatLegend.align = "center";
heatLegend.width = am4core.percent(38);
heatLegend.minValue = 0;
heatLegend.maxValue = 40000000;
heatLegend.markerContainer.minHeight = 10;
heatLegend.markers.template.minHeight = 10;
// Set up custom heat map legend labels using axis ranges
var minRange = heatLegend.valueAxis.axisRanges.create();
minRange.value = heatLegend.minValue;
minRange.label.inside = true;
minRange.label.horizontalCenter = "right";
minRange.label.dy = 5;
minRange.label.dx = -3;
minRange.label.text = "Less";
var maxRange = heatLegend.valueAxis.axisRanges.create();
maxRange.value = heatLegend.maxValue;
maxRange.label.inside = true;
maxRange.label.horizontalCenter = "left";
maxRange.label.dy = 5;
maxRange.label.dx = 3;
maxRange.label.text = "More";
// Blank out internal heat legend value axis labels
heatLegend.valueAxis.renderer.labels.template.adapter.add("text", function(
labelText
) {
return "";
});
// Allow the heatLegend to function in general
heatLegend.minColor = heatColors[0];
heatLegend.maxColor = heatColors[2];
// Override heatLegend gradient
var gradient = new am4core.LinearGradient();
heatColors.forEach(function(color) {
gradient.addColor(color);
});
heatLegend.markers.template.adapter.add("fill", function() {
return gradient;
});
// Configure series tooltip
var polygonTemplate = polygonSeries.mapPolygons.template;
polygonTemplate.tooltipText = "{name}: {value}";
// // Create hover state and set alternative fill color
// var hs = polygonTemplate.states.create("hover");
// hs.properties.fill = am4core.color("#3c5bdc");
// Set heatmap values for each state
polygonSeries.data = [
{
id: "US-AL",
value: 4447100
},
{
id: "US-AK",
value: 626932
},
{
id: "US-AZ",
value: 5130632
},
{
id: "US-AR",
value: 2673400
},
{
id: "US-CA",
value: 33871648
},
{
id: "US-CO",
value: 4301261
},
{
id: "US-CT",
value: 3405565
},
{
id: "US-DE",
value: 783600
},
{
id: "US-FL",
value: 15982378
},
{
id: "US-GA",
value: 8186453
},
{
id: "US-HI",
value: 1211537
},
{
id: "US-ID",
value: 1293953
},
{
id: "US-IL",
value: 12419293
},
{
id: "US-IN",
value: 6080485
},
{
id: "US-IA",
value: 2926324
},
{
id: "US-KS",
value: 2688418
},
{
id: "US-KY",
value: 4041769
},
{
id: "US-LA",
value: 4468976
},
{
id: "US-ME",
value: 1274923
},
{
id: "US-MD",
value: 5296486
},
{
id: "US-MA",
value: 6349097
},
{
id: "US-MI",
value: 9938444
},
{
id: "US-MN",
value: 4919479
},
{
id: "US-MS",
value: 2844658
},
{
id: "US-MO",
value: 5595211
},
{
id: "US-MT",
value: 902195
},
{
id: "US-NE",
value: 1711263
},
{
id: "US-NV",
value: 1998257
},
{
id: "US-NH",
value: 1235786
},
{
id: "US-NJ",
value: 8414350
},
{
id: "US-NM",
value: 1819046
},
{
id: "US-NY",
value: 18976457
},
{
id: "US-NC",
value: 8049313
},
{
id: "US-ND",
value: 642200
},
{
id: "US-OH",
value: 11353140
},
{
id: "US-OK",
value: 3450654
},
{
id: "US-OR",
value: 3421399
},
{
id: "US-PA",
value: 12281054
},
{
id: "US-RI",
value: 1048319
},
{
id: "US-SC",
value: 4012012
},
{
id: "US-SD",
value: 754844
},
{
id: "US-TN",
value: 5689283
},
{
id: "US-TX",
value: 20851820
},
{
id: "US-UT",
value: 2233169
},
{
id: "US-VT",
value: 608827
},
{
id: "US-VA",
value: 7078515
},
{
id: "US-WA",
value: 5894121
},
{
id: "US-WV",
value: 1808344
},
{
id: "US-WI",
value: 5363675
},
{
id: "US-WY",
value: 493782
}
];
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With