Lattice Grid Buy a licence

demo D222

Chaining, three levels deep

sales → revenue per rep → top three and a profile, with tiles reading the middle level; one filter moves all of it

source: { mode: 'derived', from, groupBy, profile } · createStat

Building…
Loading a live grid…

The configuration

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.28.0/lattice-grid.min.css">
<style>
  #tiles { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-bottom: 16px; }
  #book > div { height: 320px; }
  #lower { display: flex; gap: 16px; margin-top: 16px; }
  #byRep, #top3, #profile { flex: 1; }
</style>
<div id="app"></div>

<script type="module">
  import * as Vue from 'https://esm.sh/vue@3';
  import { createGrid, createStat } from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.28.0/lattice-grid.esm.min.js';
  import createLatticeGrid from 'https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.28.0/modules/vue.esm.min.js';

  const LatticeGrid = createLatticeGrid({ vue: Vue, createGrid });

  const USD = { style: 'currency', currency: 'USD', maximumFractionDigits: 0 };

  Vue.createApp({
    components: { LatticeGrid },
    data() {
      return {
        // Level 0 - the book. A plain grid, filterable, over the sales rows.
        book: {
          rowKey: 'id',
          toolPanel: { side: 'left', panels: ['filters', 'columns'] },
          columns: [
            { field: 'rep', title: 'Rep' },
            { field: 'region', title: 'Region', filter: { type: 'set' } },
            { field: 'amount', title: 'Deal', type: 'number', format: USD, total: 'sum' },
          ],
          rows,  // e.g. [{ id: 'S0', rep: 'Ana Bright', region: 'EMEA', amount: 2140 }, ...]
        },
      };
    },
    mounted() {
      // The chain reads live grid instances, so it composes after the book is mounted.
      const book = this.$refs.book.grid();

      // Level 1 - revenue per rep, DERIVED from the book and following its filter.
      // No rows and no rowKey: the group value is the identity. refresh: 'live'
      // re-derives on the next frame when the book is filtered.
      const byRep = createGrid(document.getElementById('byRep'), {
        autoHeight: true,
        source: { mode: 'derived', from: book, follow: 'filtered', refresh: 'live', groupBy: 'rep',
          select: { revenue: { of: 'amount', fn: 'sum' }, deals: { fn: 'count' } },
          sort: [{ col: 'revenue', dir: 'desc' }] },
        columns: [
          { field: 'rep', title: 'Rep' },
          { field: 'revenue', title: 'Revenue', type: 'number', format: USD },
          { field: 'deals', title: 'Deals', type: 'number' },
        ],
      });

      // Level 2 - derived from byRep, not from the book. The chain composes.
      createGrid(document.getElementById('top3'), {
        autoHeight: true,
        source: { mode: 'derived', from: byRep, refresh: 'live', sort: [{ col: 'revenue', dir: 'desc' }], limit: 3 },
        columns: [
          { field: 'rep', title: 'Rep' },
          { field: 'revenue', title: 'Revenue', type: 'number', format: USD },
        ],
      });

      // Level 2 - the profile transpose: one row of stats over byRep's revenues.
      createGrid(document.getElementById('profile'), {
        autoHeight: true,
        source: { mode: 'derived', from: byRep, profile: ['revenue'], refresh: 'live' },
        columns: [
          { field: 'column', title: 'Measure' },
          { field: 'mean', title: 'Mean', type: 'number', format: { maximumFractionDigits: 0 } },
          { field: 'stddev', title: 'σ', type: 'number', format: { maximumFractionDigits: 0 } },
          { field: 'min', title: 'Weakest', type: 'number', format: { maximumFractionDigits: 0 } },
          { field: 'max', title: 'Strongest', type: 'number', format: { maximumFractionDigits: 0 } },
        ],
      });

      // A tile is one reduced figure over a grid. These read byRep (level 1). The
      // concentration is a Gini coefficient, a ratio, so it renders 0.34 rather than
      // borrowing the currency format; show: 'rep' turns max into an argmax.
      const tile = () => { const d = document.createElement('div'); document.getElementById('tiles').append(d); return d; };
      createStat({ grid: byRep, container: tile(), title: 'Revenue concentration', of: 'revenue', fn: 'gini', goodWhen: 'down', footer: '0 = even, 1 = one rep has it all' });
      createStat({ grid: byRep, container: tile(), title: 'Best rep', of: 'revenue', fn: 'max', show: 'rep' });
      createStat({ grid: byRep, container: tile(), title: 'Weakest rep', of: 'revenue', fn: 'min', show: 'rep' });
      createStat({ grid: byRep, container: tile(), title: 'Spread across reps', of: 'revenue', fn: 'stddev' });
      createStat({ grid: byRep, container: tile(), title: 'Reps selling', fn: 'count' });
    },
    template: `
      <div>
        <div id="tiles"></div>
        <div id="book"><lattice-grid ref="book" v-bind="book" /></div>
        <div id="lower">
          <div id="byRep"></div>
          <div id="top3"></div>
          <div id="profile"></div>
        </div>
      </div>
    `,
  }).mount('#app');
</script>