Column Field
A column's field determines how LyteNyte Grid retrieves a cell value. This guide explains the four field types supported by LyteNyte Grid.
Set a column's value source using the field property on the column definition.
The field property applies only to individual columns and cannot be set on the
base column specification.
LyteNyte Grid supports four field types:
- String: Use when each row is a flat JavaScript object. The string value is used as an object key to read the cell value.
- Number: Use when each row is an array. The number value is used as an array index to read the cell value.
- Function: Use to compute the cell value from the row data. This callback can run any logic and is commonly used for derived values.
- Path object: Use when each row is a nested JavaScript object. Provide an object of the form
{ kind: "path"; path: string }to read a nested cell value.
The field property is optional. If omitted, LyteNyte Grid uses the column's id as
a fallback, which behaves the same as a string field.
String Fields
When field is a string, the grid uses it as a key to read the value from
object-based row data. Use string fields when each row is a standard JavaScript object.
This is demonstrated in the example below:
String Field
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId } from "react";import {ExchangeCell,makePerfHeaderCell,NetworkCell,PercentCell,PercentCellPositiveNegative,SymbolCell,tw,} from "./components";import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";const columns: Column<DEXPerformanceData>[] = [{ id: "Symbol", cellRenderer: SymbolCell, width: 220, field: "symbol" },{ id: "Network", cellRenderer: NetworkCell, width: 220, field: "network" },{ id: "Exchange", cellRenderer: ExchangeCell, width: 220, field: "exchange" },{id: "Change % 24h",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Change", "24h"),type: "number,",field: "change24h",},{field: "perf1w",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "1w"),id: "Perf % 1W",type: "number,",},{field: "perf1m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "1m"),id: "Perf % 1M",type: "number,",},{field: "perf3m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "3m"),id: "Perf % 3M",type: "number,",},{field: "perf6m",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "6m"),id: "Perf % 6M",type: "number,",},{field: "perfYtd",cellRenderer: PercentCellPositiveNegative,headerRenderer: makePerfHeaderCell("Perf %", "YTD"),id: "Perf % YTD",type: "number",},{ field: "volatility", cellRenderer: PercentCell, id: "Volatility", type: "number" },{field: "volatility1m",cellRenderer: PercentCell,headerRenderer: makePerfHeaderCell("Volatility", "1m"),id: "Volatility 1M",type: "number",},];export default function ColumnBase() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: { width: 80 },});const view = grid.view.useValue();return (<div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("text-ln-gray-60! dark:text-ln-gray-70! flex h-full w-full items-center text-nowrap px-2 text-xs capitalize",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className="text-xs! flex h-full w-full items-center px-2"/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";import {exchanges,networks,symbols,} from "@1771technologies/grid-sample-data/dex-pairs-performance";export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}export function SymbolCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const ticker = row.data.symbolTicker;const symbol = row.data.symbol;const image = symbols[row.data.symbolTicker];return (<div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for symbol ${symbol}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="bg-ln-gray-20 text-ln-gray-100 flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">{ticker}</div><div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div></div>);}export function NetworkCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const name = row.data.network;const image = networks[name];return (<div className="grid grid-cols-[20px_1fr] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for network ${name}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="w-full overflow-hidden text-ellipsis">{name}</div></div>);}export function ExchangeCell({ grid: { api }, row }: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const name = row.data.exchange;const image = exchanges[name];return (<div className="grid grid-cols-[20px_1fr] items-center gap-1.5"><div><imgsrc={image}alt={`Logo for exchange ${name}`}className="h-full w-full overflow-hidden rounded-full"/></div><div className="w-full overflow-hidden text-ellipsis">{name}</div></div>);}export function PercentCellPositiveNegative({grid: { api },column,row,}: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const field = api.columnField(column, row);if (typeof field !== "number") return "-";const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";return (<divclassName={tw("h-ful flex w-full items-center justify-end tabular-nums",field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",)}>{value}</div>);}export function PercentCell({grid: { api },column,row,}: CellRendererParams<DEXPerformanceData>) {if (!api.rowIsLeaf(row) || !row.data) return null;const field = api.columnField(column, row);if (typeof field !== "number") return "-";const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;}export const makePerfHeaderCell = (name: string, subname: string) => {return (_: HeaderCellRendererParams<DEXPerformanceData>) => {return (<div className="flex h-full w-full flex-col items-end justify-center"><div>{name}</div><div className="font-mono uppercase">{subname}</div></div>);};};
In the example, each column specifies its field directly:
const columns = [{ id: "Symbol", field: "symbol" },{ id: "Network", field: "network" },{ id: "Exchange", field: "exchange" },// other columns];
Number Fields
When field is a number, row data must be an array. The number acts as the index used
to retrieve the value. In the demo below, each row is an array, so each column uses a numeric index.
Number Index Fields
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { stockData } from "@1771technologies/grid-sample-data/stock-data-smaller";import { useId } from "react";import {AnalystRatingCell,PercentCell,CurrencyCell,SymbolCell,CompactNumberCell,HeaderRenderer,} from "./components";type StockData = (typeof stockData)[number];const columns: Column<StockData>[] = [{ field: 0, id: "symbol", name: "Symbol", cellRenderer: SymbolCell, width: 220 },{ field: 2, id: "analyst-rating", cellRenderer: AnalystRatingCell, width: 130 },{ field: 3, id: "price", type: "number", cellRenderer: CurrencyCell, width: 110 },{ field: 5, id: "change", type: "number", cellRenderer: PercentCell, width: 130 },{ field: 11, id: "eps", name: "EPS", type: "number", cellRenderer: CurrencyCell, width: 130 },{ field: 6, id: "volume", type: "number", cellRenderer: CompactNumberCell, width: 130 },];export default function ColumnFieldNumberIndex() {const ds = useClientRowDataSource({ data: stockData });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: {headerRenderer: HeaderRenderer,widthFlex: 1,},});const view = grid.view.useValue();return (<div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className="flex h-full w-full items-center px-2 capitalize"/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cell key={c.id} cell={c} className="text-xs! flex text-nowrap" />);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type { stockData } from "@1771technologies/grid-sample-data/stock-data-smaller";import { memo, type ReactNode } from "react";import { logos } from "@1771technologies/grid-sample-data/stock-data-smaller";import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";type StockData = (typeof stockData)[number];export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}function AnalystRatingCellImpl({ grid, row, column }: CellRendererParams<StockData>) {const field = grid.api.columnField(column, row) as string;let Icon: (() => ReactNode) | null = null;const label = field || "-";let clx = "";if (label === "Strong buy") {Icon = StrongBuy;clx = "text-green-500";} else if (label === "Strong Sell") {Icon = StrongSell;clx = "text-red-500";} else if (label === "Neutral") {Icon = Minus;} else if (label === "Buy") {Icon = Buy;clx = "text-green-500";} else if (label === "Sell") {Icon = Sell;clx = "text-red-500";}const aggModel = grid.state.aggModel.useValue();const activeAgg = aggModel[column.id];const isCount = activeAgg?.fn === "count";const isGroupRow = grid.api.rowIsGroup(row);return (<divclassName={tw("grid h-full grid-cols-[16px_1fr] items-center gap-4 text-nowrap px-3",clx,isGroupRow && isCount && "flex justify-end",)}>{Icon && <Icon />}<div>{label}{isCount && <span className="pl-1">Ratings</span>}</div></div>);}function Minus() {return (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"strokeWidth={1.5}stroke="currentColor"width={16}><path strokeLinecap="round" strokeLinejoin="round" d="M5 12h14" /></svg>);}function Sell() {return (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"strokeWidth={1.5}stroke="currentColor"width={16}><path strokeLinecap="round" strokeLinejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /></svg>);}function Buy() {return (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"strokeWidth={1.5}stroke="currentColor"width={16}><path strokeLinecap="round" strokeLinejoin="round" d="m4.5 15.75 7.5-7.5 7.5 7.5" /></svg>);}function StrongSell() {return (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"strokeWidth={1.5}stroke="currentColor"width={16}><pathstrokeLinecap="round"strokeLinejoin="round"d="m4.5 5.25 7.5 7.5 7.5-7.5m-15 6 7.5 7.5 7.5-7.5"/></svg>);}function StrongBuy() {return (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"strokeWidth={1.5}stroke="currentColor"width={16}><path strokeLinecap="round" strokeLinejoin="round" d="m4.5 18.75 7.5-7.5 7.5 7.5" /><path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 7.5-7.5 7.5 7.5" /></svg>);}export const AnalystRatingCell = memo(AnalystRatingCellImpl);function CompactNumberCellImpl({ row, grid, column }: CellRendererParams<StockData>) {const field = grid.api.columnField(column, row);const isGroup = grid.api.rowIsGroup(row);const aggregations = grid.state.aggModel.useValue();const isCount = aggregations[column.id]?.fn === "count";const [label, suffix] =typeof field === "number"? isGroup && isCount? [Math.round(field), ""]: formatCompactNumber(field): ["-", ""];return (<div className="flex h-full w-full items-center justify-end gap-1 text-nowrap px-3 tabular-nums"><span>{label}</span><span className="font-semibold">{suffix}</span></div>);}export const CompactNumberCell = memo(CompactNumberCellImpl);function formatCompactNumber(n: number) {const suffixes = ["", "K", "M", "B", "T"];let magnitude = 0;let num = Math.abs(n);while (num >= 1000 && magnitude < suffixes.length - 1) {num /= 1000;magnitude++;}const decimals = 2;const formatted = num.toFixed(decimals);return [`${n < 0 ? "-" : ""}${formatted}`, suffixes[magnitude]];}const formatter = new Intl.NumberFormat("en-US", {minimumFractionDigits: 2,maximumFractionDigits: 2,});function CurrencyCellImpl({ grid, row, column }: CellRendererParams<StockData>) {const field = grid.api.columnField(column, row);const label = typeof field === "number" ? formatter.format(field) : "-";const currency = "USD";const aggregations = grid.state.aggModel.useValue();const isCount = aggregations[column.id]?.fn === "count";return (<div className="flex h-full w-full items-center justify-end text-nowrap px-3 tabular-nums"><div className="flex items-baseline gap-1"><span>{label}</span>{!isCount && <span className="text-[9px]">{currency}</span>}</div></div>);}export const CurrencyCell = memo(CurrencyCellImpl);function PercentCellImpl({ grid, row, column }: CellRendererParams<StockData>) {const field = grid.api.columnField(column, row) as number;const isGroup = grid.api.rowIsGroup(row);const aggregations = grid.state.aggModel.useValue();const isCount = aggregations[column.id]?.fn === "count";const label =typeof field === "number"? isGroup && isCount? Math.round(field): formatter.format(field) + "%": "-";return (<divclassName={tw("flex h-full w-full items-center justify-end text-nowrap px-3 tabular-nums")}>{label}</div>);}export const PercentCell = memo(PercentCellImpl);function SymbolCellImpl({ grid, row, column }: CellRendererParams<StockData>) {if (grid.api.rowIsGroup(row)) {const data = row.data[column.id];const agg = grid.state.aggModel.get()[column.id];return (<div className="flex h-full items-center text-nowrap px-3">{data as string} {agg.fn === "count" ? (data === 1 ? "Symbol" : "Symbols") : ""}</div>);}if (!grid.api.rowIsLeaf(row)) return null;const symbol = grid.api.columnField(column, row) as string;const desc = row.data?.[1];return (<div className="grid h-full w-full grid-cols-[32px_1fr] items-center gap-3 overflow-hidden text-nowrap px-3"><div className="flex h-8 min-h-8 w-8 min-w-8 items-center justify-center overflow-hidden rounded-full"><imgsrc={logos[symbol]}alt=""width={26}height={26}className="pointer-events-none h-[26px] min-h-[26px] w-[26px] min-w-[26] rounded-full bg-black p-1"/></div><div className="overflow-hidden text-ellipsis">{desc}</div></div>);}export const SymbolCell = memo(SymbolCellImpl);export function HeaderRenderer({ column, grid }: HeaderCellRendererParams<StockData>) {const name = column.name ?? column.id;const isNumber = column.type === "number";const aggModel = grid.state.aggModel.useValue();const isGrouped = grid.state.rowGroupModel.useValue().length > 0;const agg = (aggModel[column.id]?.fn ?? "") as string;return (<divclassName={clsx("text-ln-gray-80 group relative flex h-full w-full items-center gap-1 overflow-hidden text-nowrap px-2 text-xs",isNumber && "justify-end",)}><span className={tw(isNumber && "order-2")}>{name}</span>{isGrouped && agg && (<div className={tw("text-ln-primary-50 text-xs", isNumber && "order-1")}>({agg})</div>)}</div>);}
The column definitions specify numeric indices. These indices do not need to be sequential:
const columns: Column<StockData>[] = [{ field: 0, id: "symbol", name: "Symbol" },{ field: 2, id: "analyst-rating" },{ field: 3, id: "price", type: "number" },{ field: 5, id: "change", type: "number" },{ field: 11, id: "eps", type: "number" },{ field: 6, id: "volume", type: "number" },];
Number fields are efficient. In JavaScript, array indexing usually performs better than key-based lookups, and array-structured row data is often more compact than object-based data. This makes number fields a strong choice for performance-sensitive grids.
Function Fields
A function field offers the most flexible way to compute a column's value. You provide a function, and LyteNyte Grid calls it for each row to compute the cell value. Function fields are commonly mixed with other field types rather than being used for every column.
LyteNyte Grid passes a single argument to the function. This argument contains either aggregated group data or leaf-row data, depending on the row being processed. See the field data param for the exact type.
Function fields are ideal for derived or calculated values. In the demo below, the GBP Price column computes a value from the base USD Price column.
Function Fields
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { stockData } from "@1771technologies/grid-sample-data/stock-data-smaller";import { useId } from "react";import {PercentCell,CurrencyCell,SymbolCell,CompactNumberCell,HeaderRenderer,CurrencyCellGBP,AnalystRatingCell,} from "./components";type StockData = (typeof stockData)[number];const columns: Column<StockData>[] = [{ field: 0, id: "symbol", name: "Symbol", cellRenderer: SymbolCell, width: 220 },{ field: 2, id: "analyst-rating", cellRenderer: AnalystRatingCell, width: 130 },{field: 3,id: "price",name: "USD Price",type: "number",cellRenderer: CurrencyCell,width: 110,},{field: (d) => {if (d.data.kind === "branch" || !d.data.data) return 0;return ((d.data.data as StockData)[3] as number) * 1.36;},id: "price",name: "GBP Price",type: "number",cellRenderer: CurrencyCellGBP,width: 110,},{ field: 5, id: "change", type: "number", cellRenderer: PercentCell, width: 130 },{ field: 11, id: "eps", name: "EPS", type: "number", cellRenderer: CurrencyCell, width: 130 },{ field: 6, id: "volume", type: "number", cellRenderer: CompactNumberCell, width: 130 },];export default function ColumnFieldNumberIndex() {const ds = useClientRowDataSource({ data: stockData });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: {headerRenderer: HeaderRenderer,widthFlex: 1,},});const view = grid.view.useValue();return (<div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className="flex h-full w-full items-center px-2 capitalize"/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id}>{row.cells.map((c) => {return (<Grid.Cell key={c.id} cell={c} className="text-xs! flex text-nowrap" />);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type { stockData } from "@1771technologies/grid-sample-data/stock-data-smaller";import { memo, type ReactNode } from "react";import { logos } from "@1771technologies/grid-sample-data/stock-data-smaller";import type {CellRendererParams,HeaderCellRendererParams,} from "@1771technologies/lytenyte-pro/types";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";type StockData = (typeof stockData)[number];export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}function AnalystRatingCellImpl({ grid, row, column }: CellRendererParams<StockData>) {const field = grid.api.columnField(column, row) as string;let Icon: (() => ReactNode) | null = null;const label = field || "-";let clx = "";if (label === "Strong buy") {Icon = StrongBuy;clx = "text-green-500";} else if (label === "Strong Sell") {Icon = StrongSell;clx = "text-red-500";} else if (label === "Neutral") {Icon = Minus;} else if (label === "Buy") {Icon = Buy;clx = "text-green-500";} else if (label === "Sell") {Icon = Sell;clx = "text-red-500";}const aggModel = grid.state.aggModel.useValue();const activeAgg = aggModel[column.id];const isCount = activeAgg?.fn === "count";const isGroupRow = grid.api.rowIsGroup(row);return (<divclassName={tw("grid h-full grid-cols-[16px_1fr] items-center gap-4 text-nowrap px-3",clx,isGroupRow && isCount && "flex justify-end",)}>{Icon && <Icon />}<div>{label}{isCount && <span className="pl-1">Ratings</span>}</div></div>);}function Minus() {return (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"strokeWidth={1.5}stroke="currentColor"width={16}><path strokeLinecap="round" strokeLinejoin="round" d="M5 12h14" /></svg>);}function Sell() {return (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"strokeWidth={1.5}stroke="currentColor"width={16}><path strokeLinecap="round" strokeLinejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /></svg>);}function Buy() {return (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"strokeWidth={1.5}stroke="currentColor"width={16}><path strokeLinecap="round" strokeLinejoin="round" d="m4.5 15.75 7.5-7.5 7.5 7.5" /></svg>);}function StrongSell() {return (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"strokeWidth={1.5}stroke="currentColor"width={16}><pathstrokeLinecap="round"strokeLinejoin="round"d="m4.5 5.25 7.5 7.5 7.5-7.5m-15 6 7.5 7.5 7.5-7.5"/></svg>);}function StrongBuy() {return (<svgxmlns="http://www.w3.org/2000/svg"fill="none"viewBox="0 0 24 24"strokeWidth={1.5}stroke="currentColor"width={16}><path strokeLinecap="round" strokeLinejoin="round" d="m4.5 18.75 7.5-7.5 7.5 7.5" /><path strokeLinecap="round" strokeLinejoin="round" d="m4.5 12.75 7.5-7.5 7.5 7.5" /></svg>);}export const AnalystRatingCell = memo(AnalystRatingCellImpl);function CompactNumberCellImpl({ row, grid, column }: CellRendererParams<StockData>) {const field = grid.api.columnField(column, row);const isGroup = grid.api.rowIsGroup(row);const aggregations = grid.state.aggModel.useValue();const isCount = aggregations[column.id]?.fn === "count";const [label, suffix] =typeof field === "number"? isGroup && isCount? [Math.round(field), ""]: formatCompactNumber(field): ["-", ""];return (<div className="flex h-full w-full items-center justify-end gap-1 text-nowrap px-3 tabular-nums"><span>{label}</span><span className="font-semibold">{suffix}</span></div>);}export const CompactNumberCell = memo(CompactNumberCellImpl);function formatCompactNumber(n: number) {const suffixes = ["", "K", "M", "B", "T"];let magnitude = 0;let num = Math.abs(n);while (num >= 1000 && magnitude < suffixes.length - 1) {num /= 1000;magnitude++;}const decimals = 2;const formatted = num.toFixed(decimals);return [`${n < 0 ? "-" : ""}${formatted}`, suffixes[magnitude]];}const formatter = new Intl.NumberFormat("en-US", {minimumFractionDigits: 2,maximumFractionDigits: 2,});function CurrencyCellImpl({ grid, row, column }: CellRendererParams<StockData>) {const field = grid.api.columnField(column, row);const label = typeof field === "number" ? formatter.format(field) : "-";const currency = "USD";const aggregations = grid.state.aggModel.useValue();const isCount = aggregations[column.id]?.fn === "count";return (<div className="flex h-full w-full items-center justify-end text-nowrap px-3 tabular-nums"><div className="flex items-baseline gap-1"><span>{label}</span>{!isCount && <span className="text-[9px]">{currency}</span>}</div></div>);}export const CurrencyCell = memo(CurrencyCellImpl);function CurrencyCellGBPImpl({ grid, row, column }: CellRendererParams<StockData>) {const field = grid.api.columnField(column, row);const label = typeof field === "number" ? formatter.format(field) : "-";const currency = "GBP";const aggregations = grid.state.aggModel.useValue();const isCount = aggregations[column.id]?.fn === "count";return (<div className="flex h-full w-full items-center justify-end text-nowrap px-3 tabular-nums"><div className="flex items-baseline gap-1"><span>{label}</span>{!isCount && <span className="text-[9px]">{currency}</span>}</div></div>);}export const CurrencyCellGBP = memo(CurrencyCellGBPImpl);function PercentCellImpl({ grid, row, column }: CellRendererParams<StockData>) {const field = grid.api.columnField(column, row) as number;const isGroup = grid.api.rowIsGroup(row);const aggregations = grid.state.aggModel.useValue();const isCount = aggregations[column.id]?.fn === "count";const label =typeof field === "number"? isGroup && isCount? Math.round(field): formatter.format(field) + "%": "-";return (<divclassName={tw("flex h-full w-full items-center justify-end text-nowrap px-3 tabular-nums")}>{label}</div>);}export const PercentCell = memo(PercentCellImpl);function SymbolCellImpl({ grid, row, column }: CellRendererParams<StockData>) {if (grid.api.rowIsGroup(row)) {const data = row.data[column.id];const agg = grid.state.aggModel.get()[column.id];return (<div className="flex h-full items-center text-nowrap px-3">{data as string} {agg.fn === "count" ? (data === 1 ? "Symbol" : "Symbols") : ""}</div>);}if (!grid.api.rowIsLeaf(row)) return null;const symbol = grid.api.columnField(column, row) as string;const desc = row.data?.[1];return (<div className="grid h-full w-full grid-cols-[32px_1fr] items-center gap-3 overflow-hidden text-nowrap px-3"><div className="flex h-8 min-h-8 w-8 min-w-8 items-center justify-center overflow-hidden rounded-full"><imgsrc={logos[symbol]}alt=""width={26}height={26}className="pointer-events-none h-[26px] min-h-[26px] w-[26px] min-w-[26] rounded-full bg-black p-1"/></div><div className="overflow-hidden text-ellipsis">{desc}</div></div>);}export const SymbolCell = memo(SymbolCellImpl);export function HeaderRenderer({ column, grid }: HeaderCellRendererParams<StockData>) {const name = column.name ?? column.id;const isNumber = column.type === "number";const aggModel = grid.state.aggModel.useValue();const isGrouped = grid.state.rowGroupModel.useValue().length > 0;const agg = (aggModel[column.id]?.fn ?? "") as string;return (<divclassName={clsx("text-ln-gray-80 group relative flex h-full w-full items-center gap-1 overflow-hidden text-nowrap px-2 text-xs",isNumber && "justify-end",)}><span className={tw(isNumber && "order-2")}>{name}</span>{isGrouped && agg && (<div className={tw("text-ln-primary-50 text-xs", isNumber && "order-1")}>({agg})</div>)}</div>);}
Path Fields
Set the field property to an object of the form { kind: "path"; path: string } to retrieve a nested value from the row data.
The path syntax matches Lodash's get function. Examples:
{ kind: "path", path: "alpha.beta[0]" };{ kind: "path", path: "beta[0].alpha" };
The demo below shows path fields in use. Each row exposes temperatures
under a temps property keyed by month, so fields such
as { kind: "path", path: "temps.Jan" } read the appropriate value.
Path Fields
"use client";import { Grid, useClientRowDataSource } from "@1771technologies/lytenyte-pro";import "@1771technologies/lytenyte-pro/grid.css";import type { Column } from "@1771technologies/lytenyte-pro/types";import { useId } from "react";import type { MonthlyTemperature } from "@1771technologies/grid-sample-data/temperatures";import { data } from "@1771technologies/grid-sample-data/temperatures";import { HeatMapCell, tw, YearCell } from "./components";const columns: Column<MonthlyTemperature>[] = [{ id: "year", cellRenderer: YearCell, width: 100 },{ id: "Jan", field: { kind: "path", path: "temps.Jan" } },{ id: "Feb", field: { kind: "path", path: "temps.Feb" } },{ id: "Mar", field: { kind: "path", path: "temps.Mar" } },{ id: "Apr", field: { kind: "path", path: "temps.Apr" } },{ id: "May", field: { kind: "path", path: "temps.May" } },{ id: "Jun", field: { kind: "path", path: "temps.Jun" } },{ id: "Jul", field: { kind: "path", path: "temps.Jul" } },{ id: "Aug", field: { kind: "path", path: "temps.Aug" } },{ id: "Sep", field: { kind: "path", path: "temps.Sep" } },{ id: "Oct", field: { kind: "path", path: "temps.Oct" } },{ id: "Nov", field: { kind: "path", path: "temps.Nov" } },{ id: "Dec", field: { kind: "path", path: "temps.Dec" } },];export default function ColumnBase() {const ds = useClientRowDataSource({ data: data });const grid = Grid.useLyteNyte({gridId: useId(),rowDataSource: ds,columns,columnBase: {widthMin: 30,width: 50,widthFlex: 1,cellRenderer: HeatMapCell,},});const view = grid.view.useValue();return (<div className="lng-grid" style={{ height: 500 }}><Grid.Root grid={grid}><Grid.Viewport><Grid.Header>{view.header.layout.map((row, i) => {return (<Grid.HeaderRow key={i} headerRowIndex={i}>{row.map((c) => {if (c.kind === "group") return null;return (<Grid.HeaderCellkey={c.id}cell={c}className={tw("flex h-full w-full items-center justify-center text-nowrap text-sm capitalize",c.column.type === "number" && "justify-end",)}/>);})}</Grid.HeaderRow>);})}</Grid.Header><Grid.RowsContainer><Grid.RowsCenter>{view.rows.center.map((row) => {if (row.kind === "full-width") return null;return (<Grid.Row row={row} key={row.id} className="group">{row.cells.map((c) => {return (<Grid.Cellkey={c.id}cell={c}className="flex h-full w-full items-center justify-center text-sm"/>);})}</Grid.Row>);})}</Grid.RowsCenter></Grid.RowsContainer></Grid.Viewport></Grid.Root></div>);}
import type { MonthlyTemperature } from "@1771technologies/grid-sample-data/temperatures";import type { CellRendererParams } from "@1771technologies/lytenyte-pro/types";import type { ClassValue } from "clsx";import clsx from "clsx";import { twMerge } from "tailwind-merge";export function tw(...c: ClassValue[]) {return twMerge(clsx(...c));}export function HeatMapCell({grid: { api },column,row,}: CellRendererParams<MonthlyTemperature>) {if (!api.rowIsLeaf(row) || !row.data) return null;const value = row.data.temps[column.id as keyof MonthlyTemperature["temps"]];const bg = valueToColor(value);return (<divstyle={{ background: bg }}className="flex h-full w-full items-center justify-center text-white group-hover:opacity-70">{value.toFixed(1)}°C</div>);}export function YearCell({ grid: { api }, row, column }: CellRendererParams<MonthlyTemperature>) {const field = api.columnField(column, row);return `${field}`;}/*** Interpolate between two HSL colors based on value in [0, 30].* 0 -> hsl(173.4 80.4% 40%)* 30 -> hsl(0 84.2% 60.2%)*/export function valueToColor(value: number): string {const percentage = Math.min((value / 19) * 100, 100);const start = "174.7 83.9% 31.6%";const end = "178.6 84.3% 10%";return `color-mix(in hsl, hsl(${start}) ${100 - percentage}%, hsl(${end}) ${percentage}%)`;}
Next Steps
- Column Resizing: Change column widths programmatically or via user interaction.
- Column ID & Name: Define user-friendly column names and ensure unique IDs.
- Column Moving: Reorder columns programmatically or through drag-and-drop.
- Column Base: See the default column values and how to override them with the
columnBaseproperty.
Column Resizing
Columns can be resized programmatically or through user interaction. LyteNyte Grid includes built-in drag-to-resize behavior for column headers.
Column Groups
LyteNyte Grid lets you organize columns into groups to create visual relationships between related columns. Each column belongs to one group, and groups may contain nested groups to form hierarchies.