Atomic Angular — Doc interne

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

Sources by format
sample.component.ts
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.

click a cell click an axis label aggregation input (AggregationLike) buildHeatmap(aggregation, options) xLabels / yLabels, capped and ranked cells, indexed by 'x y' quantileScale(cells) — intensity per cell CSS grid render cellSelected axisSelected

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.

Turn an axis click into a filter
heatmap-facet.component.ts
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

On this page