Lattice Grid Buy a licence

demo D371

Reading ClickHouse live, filter to SQL: JavaScript Data Grid Demo

A grid over ClickHouse’s own public playground: filter, sort, page and count are pushed as SQL, and a grouped average lands in a KPI tile

createPushdownSource · clickhouseAdapter · lastPlan() · source.aggregate()

This grid reads ClickHouse's own public playground live: filtering, sorting, paging and counting are pushed down as SQL, shown beside the grid as it runs, and a grouped average lands straight in a KPI tile. It runs read-only and keyless against play.clickhouse.com; availability is ClickHouse's own.

Building…
Loading a live grid…

The configuration

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.76.0/lattice-grid.min.css">
<script src="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.76.0/lattice-grid.min.js"></script>

<pre id="sql" style="margin-bottom:12px"></pre>
<div id="tiles" style="margin-bottom:12px"></div>
<div id="grid" style="height:520px"></div>

<script type="module">
  import { createKPI } from '@toclocoinc/lattice-grid/modules/kpi';

  const sqlEl = document.getElementById('sql');
  const adapter = LatticeGrid.clickhouseAdapter({
    url: 'https://play.clickhouse.com',
    table: 'uk_price_paid',
    headers: { 'X-ClickHouse-User': 'play' },   // ClickHouse's own read-only playground user
  });
  const source = LatticeGrid.createPushdownSource({ adapter, compute: LatticeGrid, pageSize: 100 });

  const kpi = createKPI(document.getElementById('tiles'), {
    rowKey: 'town',
    columns: 2,
    tiles: [
      { id: 'avgPrice', label: 'Average price · busiest town', aggregation: 'avg', field: 'avg_price', format: { style: 'currency', currency: 'GBP', notation: 'compact' } },
      { id: 'sales', label: 'Sales counted there', aggregation: 'sum', field: 'n' },
    ],
  });

  let currentFilters = null;
  const refreshKpi = async () => {
    const result = await source.aggregate(
      { filters: currentFilters, sort: [], range: null, groupBy: ['town'] },
      [{ id: 'avg_price', col: 'price', fn: 'avg' }, { id: 'n', col: 'price', fn: 'count' }],
    );
    const top = (result.groups ?? [])
      .filter((g) => g.keys?.[0])
      .sort((a, b) => (b.values.n ?? 0) - (a.values.n ?? 0))[0];
    if (top) kpi.rows.apply({ add: [{ town: String(top.keys[0]), avg_price: top.values.avg_price ?? 0, n: top.values.n ?? 0 }] });
  };

  // Every filter, sort and page becomes one statement; sqlFor(query) shows it
  // without running it twice, and the grouped average is a second statement
  // ClickHouse itself answers over the whole matching set, not just the page.
  const inner = adapter.execute.bind(adapter);
  adapter.execute = async (query, request) => {
    const result = await inner(query, request);
    currentFilters = query.filters ?? null;
    // uk_price_paid carries no column that uniquely identifies a row; stamp
    // one from each page's own offset, enough for the grid's own diffing
    // since nothing here is written back.
    const offset = query.range?.offset ?? 0;
    result.rows = (result.rows ?? []).map((r, i) => ({ ...r, __k: offset + '-' + i }));
    const f = adapter.sqlFor(query);
    sqlEl.textContent = f.sql + (f.params?.length ? '\n-- bound: ' + JSON.stringify(f.params) : '');
    void refreshKpi();
    return result;
  };

  const grid = LatticeGrid.createGrid(document.getElementById('grid'), {
    rowKey: '__k',
    toolPanel: { side: 'left', panels: ['filters', 'columns'] },
    columns: [
      { field: 'date', title: 'Date', type: 'dateString' },
      { field: 'price', title: 'Price', type: 'number', format: { style: 'currency', currency: 'GBP', notation: 'compact' }, filter: { type: 'number' } },
      { field: 'type', title: 'Type', filter: { type: 'set' } },
      { field: 'town', title: 'Town', filter: { type: 'set' } },
      { field: 'county', title: 'County', filter: { type: 'set' } },
    ],
    source,
  });
</script>