Heatmap
Render a two-dimensional aggregation as a grid of intensity-shaded cells — no charting dependency, real clickable buttons for cells and axis labels, readable by a screen reader.
A two-dimensional distribution — documents by source and by format, say — needs more than a list: a
heatmap shows both axes at once, with colour standing in for count. <Heatmap> renders it as a CSS grid,
which is what lets every cell and every axis label be a real, clickable, keyboard-reachable <button>.
Concept — cross-distribution encoding
Sinequa encodes a two-dimensional aggregation's two axes inside each item's single value field, one of three
ways depending on how the aggregation was configured on the server: "separator" (both values joined by a
delimiter), "cooccurrence" (a nested structure) or "cross" (a fielded expression, where the filterable
value differs from the displayed label). <Heatmap>'s encoding input tells it which one to parse.
Minimal example
import { Component, signal } from "@angular/core";
import { HeatmapComponent } from "@sinequa/atomic-angular";
import type { AggregationLike, HeatmapAxisSelection, HeatmapCellSelection } from "@sinequa/atomic-angular";
@Component({
selector: "sample-component",
imports: [HeatmapComponent],
template: `
<div class="h-80">
<Heatmap
[aggregation]="sourcesByFormat()"
(cellSelected)="onCell($event)"
(axisSelected)="onAxis($event)"
/>
</div>
`,
})
export class SampleComponent {
// Fed from AggregationsStore, QueryService.search() or fetchQuery() — the component only reads it.
readonly sourcesByFormat = signal<AggregationLike | undefined>(undefined);
protected onCell(selection: HeatmapCellSelection) {
console.log(`${selection.x} / ${selection.y}: ${selection.count}`);
}
protected onAxis(selection: HeatmapAxisSelection) {
console.log(`Filter the whole ${selection.axis} axis on`, selection.value);
}
}The host needs a resolved height (class="h-80" above) for the same reason every chart in this section does:
the grid fills its container, and a container with no height renders nothing to see.
How it works
<Heatmap> never fetches and never transforms the aggregation itself — buildHeatmap(), a pure function, does
both jobs: it parses encoding, ranks and caps both axes at maxX/maxY, and returns an indexed grid the
template renders directly.
AggregationLike is a structural type, not an import of @sinequa/atomic's Aggregation — a real
Aggregation is assignable to it with no cast, so nothing here depends on that package. The same pattern
recurs across every component in this section: a pure, dependency-free data module (buildHeatmap,
toSeries, buildTimeline, buildTagCloud) does the parsing, and the component only renders.
Colour comes from a primitive galactik token (--blue-500), resolved once after the first render — reading
it any earlier returns nothing useful, since getComputedStyle has nothing to compute against before styles
apply. A primitive token is invariant across themes, which is why the ramp keeps its identity when the page
flips to dark mode, unlike the semantic tokens (axis text, grid lines) the Chart.js-based charts use instead.
Recipes
Filtering from an axis label
(axisSelected) reports the value behind the label, not the label text — under the "cross" encoding the
two differ, so always read .value when building a filter, never .label.
import { Component, inject, signal } from "@angular/core";
import { HeatmapComponent, QueryParamsStore } from "@sinequa/atomic-angular";
import type { AggregationLike, HeatmapAxisSelection } from "@sinequa/atomic-angular";
import type { LegacyFilter } from "@sinequa/atomic";
@Component({
selector: "heatmap-facet",
imports: [HeatmapComponent],
template: `
<div class="h-80">
<Heatmap [aggregation]="data()" [encoding]="{ kind: 'cross' }" (axisSelected)="onAxis($event)" />
</div>
`,
})
export class HeatmapFacetComponent {
private readonly queryParams = inject(QueryParamsStore);
readonly data = signal<AggregationLike | undefined>(undefined);
protected onAxis(selection: HeatmapAxisSelection) {
const field = selection.axis === "x" ? "docformat" : "sourcestr4";
const legacyFilter: LegacyFilter = { field, operator: "eq", values: [selection.value] };
this.queryParams.updateFilter(legacyFilter);
}
}Options
Prop
Type
Pitfalls
What's next
Data visualization
Render a Sinequa aggregation as a heatmap, a tag or word cloud, or a Chart.js bar/pie/doughnut/timeline — all in controlled mode, like <Aggregation>.
Tag Cloud
Aggregation values rendered as a wrapping list of tags sized by count — the lightweight default, with no layout algorithm and no canvas.