Lattice Grid Buy a licence

demo D261

Real-Time Trading Dashboard: One Market Feed in Angular

One market feed fans to a quotes grid and a movers grid narrowed by its own route, plus a price chart

createDataRouter · attach filter · priceFeed

This is a ready-made trading terminal: one market feed fans out to a quotes grid and a movers grid narrowed by its own route, with a price chart alongside. It shows a whole desk built from a single feed, ready to adapt to a real market source.

Building…
Loading a live grid…

This is the Angular version. A standalone component takes the whole configuration through one config input, surfaces grid events as outputs, and exposes the live grid on a getter for anything the inputs do not cover.

The configuration

import { Component, signal, inject, OnDestroy } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { createGrid, type Grid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
import { createDataRouter } from '@toclocoinc/lattice-grid/modules/data-router';
import { MockWebSocket, priceFeed } from '@toclocoinc/lattice-grid/modules/mock-socket';
import { LatticeGridComponent, LatticeChartComponent, LatticeRouter, provideLattice, provideLatticeRouter } from '@toclocoinc/lattice-grid/angular';

const MONEY = { style: 'currency', currency: 'USD', decimals: 2 };
const UP_DOWN = [
  { id: 'up', when: { op: 'gt', value: 0 }, style: { color: '#3ddc97' } },
  { id: 'down', when: { op: 'lt', value: 0 }, style: { color: '#ff6b6b' } },
];
const COLUMNS = [
  { field: 'symbol', title: 'Symbol', layout: { width: 84, pin: 'start' } },
  { field: 'last', title: 'Last', type: 'number', format: MONEY },
  { field: 'chg', title: 'Chg', type: 'number', format: { decimals: 2 } },
  { field: 'bid', title: 'Bid', type: 'number', format: MONEY },
  { field: 'ask', title: 'Ask', type: 'number', format: MONEY },
];

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [LatticeGridComponent, LatticeChartComponent],
  // overlap lets the one 'price' partition reach both grids.
  providers: [provideLatticeRouter({ key: 'type', rowKey: 'symbol', overlap: true })],
  template:
    '<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">' +
    '  <lattice-grid route="price" [config]="quotesConfig" (gridReady)="quotesGrid.set($event)" style="display:block;height:300px"></lattice-grid>' +
    // The movers route reshapes its slice before it lands: a filter, then a sort, per route.
    '  <lattice-grid route="price" [routeOptions]="moversRouteOptions" [config]="moversConfig" style="display:block;height:300px"></lattice-grid>' +
    '</div>' +
    '@if (quotesGrid(); as g) {' +
    '  <lattice-chart [grid]="g" [config]="chartConfig" style="display:block;height:220px;margin-top:14px"></lattice-chart>' +
    '}',
})
export class AppComponent implements OnDestroy {
  private socket = new MockWebSocket({ feed: priceFeed({ seed: 7 }), rate: 260 });

  quotesGrid = signal<Grid | null>(null);
  chartConfig = { type: 'bar', x: 'symbol', y: 'last', title: 'Last price by symbol' };
  quotesConfig = { rowKey: 'symbol', highlightOnChange: { duration: 350 }, formatting: { chg: UP_DOWN }, columns: COLUMNS };
  moversConfig = this.quotesConfig;
  moversRouteOptions = { filter: (row: any) => Math.abs(row.chg) >= 0.4, sort: { key: 'chg', dir: 'desc' } };

  constructor(private routerService: LatticeRouter) {
    this.socket.onmessage = (event) => {
      const message = JSON.parse(event.data);
      if (message.kind === 'snapshot') this.routerService.router.load(message.rows);
      else this.routerService.router.apply(message.changes);
    };
  }

  ngOnDestroy() { this.socket.close(); }
}

bootstrapApplication(AppComponent, {
  providers: [provideLattice({ createGrid, createChart, createDataRouter })],
});