demo D261
Real-Time Trading Dashboard: One Market Feed in ESM
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.
This is the ES module version. The grid is imported straight from a CDN as a module, the same configuration as the vanilla tab without a global script tag, so it drops into any bundler or a bare module script.
The configuration
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.71.0/lattice-grid.min.css">
<div id="quotes" style="height: 300px"></div>
<div id="movers" style="height: 300px"></div>
<div id="chart" style="height: 220px"></div>
<script type="module">
import { createDataRouter } from '@toclocoinc/lattice-grid/modules/data-router';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';
import { MockWebSocket, priceFeed } from '@toclocoinc/lattice-grid/modules/mock-socket';
const money = { style: 'currency', currency: 'USD', decimals: 2 };
const upDown = [
{ id: 'up', when: { op: 'gt', value: 0 }, style: { color: '#3ddc97' } },
{ id: 'down', when: { op: 'lt', value: 0 }, style: { color: '#ff6b6b' } },
];
const cols = [
{ 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 },
];
const quotes = createGrid(document.getElementById('quotes'), {
rowKey: 'symbol', highlightOnChange: { duration: 350 }, formatting: { chg: upDown }, columns: cols,
});
const movers = createGrid(document.getElementById('movers'), {
rowKey: 'symbol', highlightOnChange: { duration: 350 }, formatting: { chg: upDown }, columns: cols,
});
// overlap lets the one 'price' partition reach both grids. The movers route
// reshapes its slice before it lands: a filter, then a sort, per route.
const router = createDataRouter({ key: 'type', rowKey: 'symbol', overlap: true });
router.attach(quotes, 'price');
router.attach(movers, 'price', {
filter: (row) => Math.abs(row.chg) >= 0.4,
sort: { key: 'chg', dir: 'desc' },
});
createChart({ grid: quotes, container: document.getElementById('chart'), type: 'bar', x: 'symbol', y: 'last', title: 'Last price by symbol' });
const socket = new MockWebSocket({ feed: priceFeed({ seed: 7 }), rate: 260 });
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.kind === 'snapshot') router.load(message.rows);
else router.apply(message.changes);
};
</script>