Heatmap with static scale
The colors are static, and set in the ng-class
of the cell, with a simple condition and static thresholds.
The code is therefore much simpler than dynamic RGB computation.
But keep in mind that if the order of size of the aggregation changes, you’ll then need to adapt your thresholds, and therefore the page.
Dataset in use: comptage-velo-donnees-compteurs
(See it on userclub domain)
Fields in use:
nom_compteur | date (datetime) | sum_counts (numeric) |
---|---|---|
97 avenue Denfert Rochereau SO-NE | 2020-10-01T04:00:00+02:00 | 70 |
100 rue La Fayette O-E | 2021-01-30T06:00:00+02:00 | 15 |
10 avenue de la Grande Armée SE-NO | 2020-12-01T13:00:00+02:00 | 22 |
87 avenue de Flandre NE-SO | 2021-04-22T22:00:00+02:00 | 3 |
<div class="container">
<ods-dataset-context context="datum"
datum-dataset="comptage-velo-donnees-compteurs"
datum-parameters="{'q':'totem'}">
<div ng-init="variables = {};
xaxis = 'nom_compteur';
yaxis = 'year';
yLegendWidth = 1">
<h3>Average number of cyclists by counting totem</h3>
<div ods-adv-analysis="results"
ods-adv-analysis-context="datum"
ods-adv-analysis-select="avg(sum_counts) as count"
ods-adv-analysis-group-by="year(date) as {{ yaxis }}, {{ xaxis }}">
{{
variables.numrow = (results | toObject:yaxis | numKeys) ;
variables.numcol = (results | toObject:xaxis | numKeys) ;
variables.listrow = (results | toObject:yaxis | keys | orderBy) ;
variables.listcol = (results | toObject:xaxis | keys | orderBy) ; ""}}
<!-- the grid -->
<div class="heatmap"
ng-class="{'display-values': variables.displayvalues}">
<!-- X axis : (bottom horizontal) list all values and set the correct position -->
<div class="x-axis-centered"
ng-repeat="(i,e) in variables.listcol"
style="grid-column: {{ i + 1 + yLegendWidth }} ;
grid-row: {{variables.numrow + 1}};">
<strong>{{e}}</strong>
</div>
<!-- Y axis : (left vertical) list all values and set the correct position -->
<div ng-repeat="(i,e) in variables.listrow"
class="y-axis"
style="grid-column: 1 / span {{ yLegendWidth }};
grid-row: {{ i + 1 }};">
<strong>{{e}}</strong>
</div>
<!-- Grid content -->
<div class="cell"
ng-repeat="(i,e) in results"
ng-class="{'orange': e.count > 150, 'lightgreen': e.count > 100 && e.count <= 150,'darkgreen': e.count < 100}"
style="grid-column: {{variables.listcol.indexOf(e[xaxis]) + 1 + yLegendWidth}};
grid-row: {{variables.listrow.indexOf(e[yaxis]) + 1}};">
{{e.count | number : 0}}
</div>
</div>
<div class="heatmap-sub row">
<div class="col-sm-6">
<div class="heatmap-switch">
<p>Display values: </p>
<label class="switch">
<input class="switch-input"
ng-model="variables.displayvalues"
type="checkbox">
<div class="switch-button">
<span class="switch-button-left">OFF</span>
<span class="switch-button-right">ON</span>
</div>
</label>
</div>
</div>
<div class="col-sm-6">
</div>
</div>
</div>
</div>
</ods-dataset-context>
</div>
/* cells colors */
.orange {
background-color: orange;
}
.darkgreen {
background-color: #6ca474;
}
.mediumgreen {
background-color: #9ab932;
}
.lightgreen {
background-color: #b0c313;
}
:root {
--values-color: white;
}
.heatmap {
display: grid;
gap: 2px;
grid-auto-columns: 1fr;
grid-auto-flow: column;
grid-auto-rows: auto;
/* full auto, adapt to the content, ie the font size */
}
.heatmap > * {
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
font-size: 0.8rem;
}
.heatmap.display-values .cell {
font-size: 0.6rem;
}
.heatmap .cell {
position: relative;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
color: var(--values-color);
font-size: 0;
}
.heatmap .cell:hover {
border: 1px solid var(--values-color);
transform: scale(1.5);
font-size: 0.6rem;
z-index: 1;
}
.heatmap .cell.square:before {
content: "";
display: block;
padding-bottom: 100%;
}
.heatmap .cell .round {
border-radius: 100%;
border: 1px solid black;
display: flex;
justify-content: center;
align-items: center;
}
.x-axis-centered {
text-align: center;
}
.x-axis-rotate {
transform: rotate(-60deg);
padding: 35% 0;
}
.heatmap-sub {
margin-top: 13px;
}
.heatmap-legend {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
}
.heatmap-legend .heatmap-legend__gradient {
display: block;
height: 25px;
width: 100%;
margin: 0 6px;
border: 1px solid darkgray;
}
.heatmap-switch {
display: flex;
align-items: baseline;
}
.heatmap-switch .switch {
margin-left: 6px;
}
/* Button Group Switch
========================================================================== */
.switch {
display: inline-block;
}
.switch-button {
/* background and border when in "off" state */
background: var(--highlight);
border: 2px solid var(--highlight);
display: grid;
grid-template-columns: 1fr 1fr;
border-radius: 6px;
color: #FFFFFF;
position: relative;
cursor: pointer;
outline: 0;
user-select: none;
}
.switch-input:not(:checked) + .switch-button .switch-button-left {
/* text color when in "off" state */
color: var(--highlight);
}
.switch-input {
display: none;
}
.switch-button span {
font-size: 0.8rem;
padding: 0.2rem 0.7rem;
text-align: center;
z-index: 2;
color: #FFFFFF;
transition: color 0.2s;
}
.switch-button::before {
content: "";
position: absolute;
background-color: #FFFFFF;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
border-radius: 4px;
top: 0;
left: 0;
height: 100%;
width: 50%;
z-index: 1;
transition: left 0.3s cubic-bezier(0.175, 0.885, 0.32, 1), padding 0.2s ease, margin 0.2s ease;
}
.switch-button:hover::before {
will-change: padding;
}
.switch-button:active::before {
padding-right: 0.4rem;
}
/* "On" state
========================== */
.switch-input:checked + .switch-button {
/* background and border when in "on" state */
background-color: var(--links);
border-color: var(--links);
}
.switch-input:checked + .switch-button .switch-button-right {
/* text color when in "on" state */
color: var(--links);
}
.switch-input:checked + .switch-button::before {
left: 50%;
}
.switch-input:checked + .switch-button:active::before {
margin-left: -0.4rem;
}
/* Checkbox in disabled state
========================== */
.switch-input[type=checkbox]:disabled + .switch-button {
opacity: 0.6;
cursor: not-allowed;
}