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.
Creating a Column Group
Create column groups in LyteNyte Grid by setting the groupPath
property on each column. The grid builds a hierarchy from the
groupPath values across all columns. The demo below shows a basic
column group setup.
Column Groups
18 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import "@1771technologies/lytenyte-pro/pill-manager.css";3import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";4import {5 ExchangeCell,6 HeaderGroupCell,7 makePerfHeaderCell,8 NetworkCell,9 PercentCell,10 PercentCellPositiveNegative,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";15
16export interface GridSpec {17 readonly data: DEXPerformanceData;18}19
20const columns: Grid.Column<GridSpec>[] = [21 {22 id: "symbol",23 cellRenderer: SymbolCell,24 width: 220,25 name: "Symbol",26 groupPath: ["Market Info"],27 },28 {29 id: "network",30 cellRenderer: NetworkCell,31 width: 220,32 name: "Network",33 groupPath: ["Market Info"],34 },35 {36 id: "exchange",37 cellRenderer: ExchangeCell,38 width: 220,39 name: "Exchange",40 groupPath: ["Market Info"],41 },42
43 {44 id: "change24h",45 cellRenderer: PercentCellPositiveNegative,46 headerRenderer: makePerfHeaderCell("Change", "24h"),47 name: "Change % 24h",48 type: "number,",49 groupPath: ["Performance"],50 },51
52 {53 id: "perf1w",54 cellRenderer: PercentCellPositiveNegative,55 headerRenderer: makePerfHeaderCell("Perf %", "1w"),56 name: "Perf % 1W",57 type: "number,",58 groupPath: ["Performance"],59 },60 {61 id: "perf1m",62 cellRenderer: PercentCellPositiveNegative,63 headerRenderer: makePerfHeaderCell("Perf %", "1m"),64 name: "Perf % 1M",65 type: "number,",66 groupPath: ["Performance"],67 },68 {69 id: "perf3m",70 cellRenderer: PercentCellPositiveNegative,71 headerRenderer: makePerfHeaderCell("Perf %", "3m"),72 name: "Perf % 3M",73 type: "number,",74 groupPath: ["Performance"],75 },76 {77 id: "perf6m",78 cellRenderer: PercentCellPositiveNegative,79 headerRenderer: makePerfHeaderCell("Perf %", "6m"),80 name: "Perf % 6M",81 type: "number,",82 groupPath: ["Performance"],83 },84 {85 id: "perfYtd",86 cellRenderer: PercentCellPositiveNegative,87 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),88 name: "Perf % YTD",89 type: "number",90 groupPath: ["Performance"],91 },92 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },93 {94 id: "volatility1m",95 cellRenderer: PercentCell,96 headerRenderer: makePerfHeaderCell("Volatility", "1m"),97 name: "Volatility 1M",98 type: "number",99 },100];101
102const base: Grid.ColumnBase<GridSpec> = { width: 80 };103
104export default function ColumnDemo() {105 const ds = useClientDataSource({ data: data });106
107 return (108 <div109 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight ln-header-group:text-center ln-header-group:text-xs"110 style={{ height: 500 }}111 >112 <Grid columns={columns} columnBase={base} rowSource={ds} columnGroupRenderer={HeaderGroupCell} />113 </div>114 );115}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";12
13export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return null;15
16 const ticker = row.data.symbolTicker;17 const symbol = row.data.symbol;18 const image = symbols[row.data.symbolTicker];19
20 return (21 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">22 <div>23 <img24 src={image}25 alt={`Logo for symbol ${symbol}`}26 className="h-full w-full overflow-hidden rounded-full"27 />28 </div>29 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">30 {ticker}31 </div>32 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>33 </div>34 );35}36
37export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {38 if (!api.rowIsLeaf(row) || !row.data) return null;39
40 const name = row.data.network;41 const image = networks[name];42
43 return (44 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">45 <div>46 <img47 src={image}48 alt={`Logo for network ${name}`}49 className="h-full w-full overflow-hidden rounded-full"50 />51 </div>52 <div className="w-full overflow-hidden text-ellipsis">{name}</div>53 </div>54 );55}56
57export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {58 if (!api.rowIsLeaf(row) || !row.data) return null;59
60 const name = row.data.exchange;61 const image = exchanges[name];62
63 return (64 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">65 <div>66 <img67 src={image}68 alt={`Logo for exchange ${name}`}69 className="h-full w-full overflow-hidden rounded-full"70 />71 </div>72 <div className="w-full overflow-hidden text-ellipsis">{name}</div>73 </div>74 );75}76
77export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {78 if (!api.rowIsLeaf(row) || !row.data) return null;79
80 const field = api.columnField(column, row);81
82 if (typeof field !== "number") return "-";83
84 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";85
86 return (87 <div88 className={tw(89 "h-ful flex w-full items-center justify-end tabular-nums",90 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",91 )}92 >93 {value}94 </div>95 );96}97
98export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return null;100
101 const field = api.columnField(column, row);102
103 if (typeof field !== "number") return "-";104
105 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";106
107 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;108}109
110export const makePerfHeaderCell = (name: string, subname: string) => {111 return (_: Grid.T.HeaderParams<GridSpec>) => {112 return (113 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">114 <div>{name}</div>115 <div className="text-ln-text-light font-mono uppercase">{subname}</div>116 </div>117 );118 };119};120
121export function HeaderGroupCell({122 groupPath,123 api,124 collapsible,125 collapsed,126}: Grid.T.HeaderGroupParams<GridSpec>) {127 return (128 <>129 <div className="flex-1">{groupPath.at(-1)!}</div>130 <button131 data-ln-button="secondary"132 data-ln-icon133 data-ln-size="sm"134 onClick={() => api.columnToggleGroup(groupPath)}135 >136 {collapsible && collapsed && <ChevronRightIcon />}137 {collapsible && !collapsed && <ChevronLeftIcon />}138 </button>139 </>140 );141}Columns stay as a flat array, even when grouped. LyteNyte Grid creates the hierarchy by joining each column’s group path. Columns with the same group path belong to the same group. In the example below, the Symbol, Network, and Exchange columns all belong to the Market Info group.
1const columns: Column<DEXPerformanceData>[] = [2 {3 id: "symbol",4 name: "Symbol",5 groupPath: ["Market Info"],6 },7 {8 id: "network",9 name: "Network",10 groupPath: ["Market Info"],11 },12 {13 id: "exchange",14 name: "Exchange",15 groupPath: ["Market Info"],16 },17 // other columns18];Columns do not need to belong to a group. If a column has no group path, its header cell spans the full height of the header.
Split Column Groups
LyteNyte Grid uses the groupPath property to identify column groups.
A group appears split when columns from different groups are placed between its members.
Although the group remains a single logical entity, it renders as multiple visual segments.
When you move separated columns next to each other, the grid automatically merges the segments.
Pinned columns always appear visually separate from non-pinned columns, even when they share a group.
The demo below shows this behavior. The Market Info and Performance groups are split visually but remain single logical groups.
Split Column Groups
18 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import "@1771technologies/lytenyte-pro/pill-manager.css";3import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";4import {5 ExchangeCell,6 HeaderGroupCell,7 makePerfHeaderCell,8 NetworkCell,9 PercentCell,10 PercentCellPositiveNegative,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";15
16export interface GridSpec {17 readonly data: DEXPerformanceData;18}19const columns: Grid.Column<GridSpec>[] = [20 {21 id: "symbol",22 cellRenderer: SymbolCell,23 width: 220,24 name: "Symbol",25 groupPath: ["Market Info"],26 },27 {28 id: "exchange",29 cellRenderer: ExchangeCell,30 width: 220,31 name: "Exchange",32 groupPath: ["Market Info"],33 },34
35 {36 id: "change24h",37 cellRenderer: PercentCellPositiveNegative,38 headerRenderer: makePerfHeaderCell("Change", "24h"),39 name: "Change % 24h",40 type: "number,",41 groupPath: ["Performance"],42 },43
44 {45 id: "perf1w",46 cellRenderer: PercentCellPositiveNegative,47 headerRenderer: makePerfHeaderCell("Perf %", "1w"),48 name: "Perf % 1W",49 type: "number,",50 groupPath: ["Performance"],51 },52 {53 id: "network",54 cellRenderer: NetworkCell,55 width: 220,56 name: "Network",57 groupPath: ["Market Info"],58 },59 {60 id: "perf1m",61 cellRenderer: PercentCellPositiveNegative,62 headerRenderer: makePerfHeaderCell("Perf %", "1m"),63 name: "Perf % 1M",64 type: "number,",65 groupPath: ["Performance"],66 },67 {68 id: "perf3m",69 cellRenderer: PercentCellPositiveNegative,70 headerRenderer: makePerfHeaderCell("Perf %", "3m"),71 name: "Perf % 3M",72 type: "number,",73 groupPath: ["Performance"],74 },75 {76 id: "perf6m",77 cellRenderer: PercentCellPositiveNegative,78 headerRenderer: makePerfHeaderCell("Perf %", "6m"),79 name: "Perf % 6M",80 type: "number,",81 groupPath: ["Performance"],82 },83 {84 id: "perfYtd",85 cellRenderer: PercentCellPositiveNegative,86 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),87 name: "Perf % YTD",88 type: "number",89 groupPath: ["Performance"],90 },91 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },92 {93 id: "volatility1m",94 cellRenderer: PercentCell,95 headerRenderer: makePerfHeaderCell("Volatility", "1m"),96 name: "Volatility 1M",97 type: "number",98 },99];100
101const base: Grid.ColumnBase<GridSpec> = { width: 80 };102
103export default function ColumnDemo() {104 const ds = useClientDataSource({ data: data });105
106 return (107 <div108 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight ln-header-group:text-center ln-header-group:text-xs"109 style={{ height: 500 }}110 >111 <Grid columns={columns} columnBase={base} rowSource={ds} columnGroupRenderer={HeaderGroupCell} />112 </div>113 );114}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";12
13export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return null;15
16 const ticker = row.data.symbolTicker;17 const symbol = row.data.symbol;18 const image = symbols[row.data.symbolTicker];19
20 return (21 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">22 <div>23 <img24 src={image}25 alt={`Logo for symbol ${symbol}`}26 className="h-full w-full overflow-hidden rounded-full"27 />28 </div>29 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">30 {ticker}31 </div>32 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>33 </div>34 );35}36
37export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {38 if (!api.rowIsLeaf(row) || !row.data) return null;39
40 const name = row.data.network;41 const image = networks[name];42
43 return (44 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">45 <div>46 <img47 src={image}48 alt={`Logo for network ${name}`}49 className="h-full w-full overflow-hidden rounded-full"50 />51 </div>52 <div className="w-full overflow-hidden text-ellipsis">{name}</div>53 </div>54 );55}56
57export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {58 if (!api.rowIsLeaf(row) || !row.data) return null;59
60 const name = row.data.exchange;61 const image = exchanges[name];62
63 return (64 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">65 <div>66 <img67 src={image}68 alt={`Logo for exchange ${name}`}69 className="h-full w-full overflow-hidden rounded-full"70 />71 </div>72 <div className="w-full overflow-hidden text-ellipsis">{name}</div>73 </div>74 );75}76
77export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {78 if (!api.rowIsLeaf(row) || !row.data) return null;79
80 const field = api.columnField(column, row);81
82 if (typeof field !== "number") return "-";83
84 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";85
86 return (87 <div88 className={tw(89 "h-ful flex w-full items-center justify-end tabular-nums",90 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",91 )}92 >93 {value}94 </div>95 );96}97
98export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return null;100
101 const field = api.columnField(column, row);102
103 if (typeof field !== "number") return "-";104
105 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";106
107 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;108}109
110export const makePerfHeaderCell = (name: string, subname: string) => {111 return (_: Grid.T.HeaderParams<GridSpec>) => {112 return (113 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">114 <div>{name}</div>115 <div className="text-ln-text-light font-mono uppercase">{subname}</div>116 </div>117 );118 };119};120
121export function HeaderGroupCell({122 groupPath,123 api,124 collapsible,125 collapsed,126}: Grid.T.HeaderGroupParams<GridSpec>) {127 return (128 <>129 <div className="flex-1">{groupPath.at(-1)!}</div>130 <button131 data-ln-button="secondary"132 data-ln-icon133 data-ln-size="sm"134 onClick={() => api.columnToggleGroup(groupPath)}135 >136 {collapsible && collapsed && <ChevronRightIcon />}137 {collapsible && !collapsed && <ChevronLeftIcon />}138 </button>139 </>140 );141}Multiple Column Group Levels
The groupPath property is an array of strings. Each string represents
a grouping level. Adding more entries creates deeper nesting. The demo
below illustrates multiple group levels:
Multiple Group Levels
19 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import "@1771technologies/lytenyte-pro/pill-manager.css";3import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";4import {5 ExchangeCell,6 HeaderGroupCell,7 makePerfHeaderCell,8 NetworkCell,9 PercentCell,10 PercentCellPositiveNegative,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";15
16export interface GridSpec {17 readonly data: DEXPerformanceData;18}19
20const columns: Grid.Column<GridSpec>[] = [21 {22 id: "symbol",23 cellRenderer: SymbolCell,24 width: 220,25 name: "Symbol",26 groupPath: ["Market Info"],27 },28 {29 id: "network",30 cellRenderer: NetworkCell,31 width: 220,32 name: "Network",33 groupPath: ["Market Info", "Location"],34 },35 {36 id: "exchange",37 cellRenderer: ExchangeCell,38 width: 220,39 name: "Exchange",40 groupPath: ["Market Info", "Location"],41 },42
43 {44 id: "change24h",45 cellRenderer: PercentCellPositiveNegative,46 headerRenderer: makePerfHeaderCell("Change", "24h"),47 name: "Change % 24h",48 type: "number,",49 groupPath: ["Performance"],50 },51
52 {53 id: "perf1w",54 cellRenderer: PercentCellPositiveNegative,55 headerRenderer: makePerfHeaderCell("Perf %", "1w"),56 name: "Perf % 1W",57 type: "number,",58 groupPath: ["Performance"],59 },60 {61 id: "perf1m",62 cellRenderer: PercentCellPositiveNegative,63 headerRenderer: makePerfHeaderCell("Perf %", "1m"),64 name: "Perf % 1M",65 type: "number,",66 groupPath: ["Performance"],67 },68 {69 id: "perf3m",70 cellRenderer: PercentCellPositiveNegative,71 headerRenderer: makePerfHeaderCell("Perf %", "3m"),72 name: "Perf % 3M",73 type: "number,",74 groupPath: ["Performance"],75 },76 {77 id: "perf6m",78 cellRenderer: PercentCellPositiveNegative,79 headerRenderer: makePerfHeaderCell("Perf %", "6m"),80 name: "Perf % 6M",81 type: "number,",82 groupPath: ["Performance"],83 },84 {85 id: "perfYtd",86 cellRenderer: PercentCellPositiveNegative,87 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),88 name: "Perf % YTD",89 type: "number",90 groupPath: ["Performance"],91 },92 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },93 {94 id: "volatility1m",95 cellRenderer: PercentCell,96 headerRenderer: makePerfHeaderCell("Volatility", "1m"),97 name: "Volatility 1M",98 type: "number",99 },100];101
102const base: Grid.ColumnBase<GridSpec> = { width: 80 };103
104export default function ColumnDemo() {105 const ds = useClientDataSource({ data: data });106
107 return (108 <div109 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight ln-header-group:text-center ln-header-group:text-xs"110 style={{ height: 500 }}111 >112 <Grid columns={columns} columnBase={base} rowSource={ds} columnGroupRenderer={HeaderGroupCell} />113 </div>114 );115}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";12
13export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return null;15
16 const ticker = row.data.symbolTicker;17 const symbol = row.data.symbol;18 const image = symbols[row.data.symbolTicker];19
20 return (21 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">22 <div>23 <img24 src={image}25 alt={`Logo for symbol ${symbol}`}26 className="h-full w-full overflow-hidden rounded-full"27 />28 </div>29 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">30 {ticker}31 </div>32 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>33 </div>34 );35}36
37export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {38 if (!api.rowIsLeaf(row) || !row.data) return null;39
40 const name = row.data.network;41 const image = networks[name];42
43 return (44 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">45 <div>46 <img47 src={image}48 alt={`Logo for network ${name}`}49 className="h-full w-full overflow-hidden rounded-full"50 />51 </div>52 <div className="w-full overflow-hidden text-ellipsis">{name}</div>53 </div>54 );55}56
57export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {58 if (!api.rowIsLeaf(row) || !row.data) return null;59
60 const name = row.data.exchange;61 const image = exchanges[name];62
63 return (64 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">65 <div>66 <img67 src={image}68 alt={`Logo for exchange ${name}`}69 className="h-full w-full overflow-hidden rounded-full"70 />71 </div>72 <div className="w-full overflow-hidden text-ellipsis">{name}</div>73 </div>74 );75}76
77export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {78 if (!api.rowIsLeaf(row) || !row.data) return null;79
80 const field = api.columnField(column, row);81
82 if (typeof field !== "number") return "-";83
84 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";85
86 return (87 <div88 className={tw(89 "h-ful flex w-full items-center justify-end tabular-nums",90 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",91 )}92 >93 {value}94 </div>95 );96}97
98export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return null;100
101 const field = api.columnField(column, row);102
103 if (typeof field !== "number") return "-";104
105 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";106
107 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;108}109
110export const makePerfHeaderCell = (name: string, subname: string) => {111 return (_: Grid.T.HeaderParams<GridSpec>) => {112 return (113 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">114 <div>{name}</div>115 <div className="text-ln-text-light font-mono uppercase">{subname}</div>116 </div>117 );118 };119};120
121export function HeaderGroupCell({122 groupPath,123 api,124 collapsible,125 collapsed,126}: Grid.T.HeaderGroupParams<GridSpec>) {127 return (128 <>129 <div className="flex-1">{groupPath.at(-1)!}</div>130 <button131 data-ln-button="secondary"132 data-ln-icon133 data-ln-size="sm"134 onClick={() => api.columnToggleGroup(groupPath)}135 >136 {collapsible && collapsed && <ChevronRightIcon />}137 {collapsible && !collapsed && <ChevronLeftIcon />}138 </button>139 </>140 );141}Each grouping level creates a new header row. The longest groupPath
in the grid determines the number of header rows.
Column Groups & Pinned Columns
Pinned columns can still belong to a group. Pinned columns create a split boundary, so the grid treats them as visually separate even if they are adjacent to other group members. The example below shows this:
Pinned Column Groups
19 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import "@1771technologies/lytenyte-pro/pill-manager.css";3import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";4import {5 ExchangeCell,6 HeaderGroupCell,7 makePerfHeaderCell,8 NetworkCell,9 PercentCell,10 PercentCellPositiveNegative,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";15import { ViewportShadows } from "@1771technologies/lytenyte-pro/components";16
17export interface GridSpec {18 readonly data: DEXPerformanceData;19}20
21const columns: Grid.Column<GridSpec>[] = [22 {23 id: "symbol",24 cellRenderer: SymbolCell,25 width: 220,26 name: "Symbol",27 pin: "start",28 groupPath: ["Market Info"],29 },30 {31 id: "network",32 cellRenderer: NetworkCell,33 width: 220,34 name: "Network",35 groupPath: ["Market Info"],36 },37 {38 id: "exchange",39 cellRenderer: ExchangeCell,40 width: 220,41 name: "Exchange",42 groupPath: ["Market Info"],43 },44 {45 id: "change24h",46 cellRenderer: PercentCellPositiveNegative,47 headerRenderer: makePerfHeaderCell("Change", "24h"),48 name: "Change % 24h",49 type: "number,",50 groupPath: ["Performance"],51 },52 {53 id: "perf1w",54 cellRenderer: PercentCellPositiveNegative,55 headerRenderer: makePerfHeaderCell("Perf %", "1w"),56 name: "Perf % 1W",57 type: "number,",58 groupPath: ["Performance"],59 },60 {61 id: "perf1m",62 cellRenderer: PercentCellPositiveNegative,63 headerRenderer: makePerfHeaderCell("Perf %", "1m"),64 name: "Perf % 1M",65 type: "number,",66 groupPath: ["Performance"],67 },68 {69 id: "perf3m",70 cellRenderer: PercentCellPositiveNegative,71 headerRenderer: makePerfHeaderCell("Perf %", "3m"),72 name: "Perf % 3M",73 type: "number,",74 groupPath: ["Performance"],75 },76 {77 id: "perf6m",78 cellRenderer: PercentCellPositiveNegative,79 headerRenderer: makePerfHeaderCell("Perf %", "6m"),80 name: "Perf % 6M",81 type: "number,",82 groupPath: ["Performance"],83 },84 {85 id: "perfYtd",86 cellRenderer: PercentCellPositiveNegative,87 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),88 name: "Perf % YTD",89 type: "number",90 groupPath: ["Performance"],91 },92 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },93 {94 id: "volatility1m",95 cellRenderer: PercentCell,96 headerRenderer: makePerfHeaderCell("Volatility", "1m"),97 name: "Volatility 1M",98 type: "number",99 },100];101
102const base: Grid.ColumnBase<GridSpec> = { width: 80 };103
104export default function ColumnDemo() {105 const ds = useClientDataSource({ data: data });106
107 return (108 <div109 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight ln-header-group:text-center ln-header-group:text-xs"110 style={{ height: 500 }}111 >112 <Grid113 columns={columns}114 columnBase={base}115 rowSource={ds}116 columnGroupRenderer={HeaderGroupCell}117 slotShadows={() => <ViewportShadows start top={false} />}118 />119 </div>120 );121}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";12
13export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return null;15
16 const ticker = row.data.symbolTicker;17 const symbol = row.data.symbol;18 const image = symbols[row.data.symbolTicker];19
20 return (21 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">22 <div>23 <img24 src={image}25 alt={`Logo for symbol ${symbol}`}26 className="h-full w-full overflow-hidden rounded-full"27 />28 </div>29 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">30 {ticker}31 </div>32 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>33 </div>34 );35}36
37export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {38 if (!api.rowIsLeaf(row) || !row.data) return null;39
40 const name = row.data.network;41 const image = networks[name];42
43 return (44 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">45 <div>46 <img47 src={image}48 alt={`Logo for network ${name}`}49 className="h-full w-full overflow-hidden rounded-full"50 />51 </div>52 <div className="w-full overflow-hidden text-ellipsis">{name}</div>53 </div>54 );55}56
57export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {58 if (!api.rowIsLeaf(row) || !row.data) return null;59
60 const name = row.data.exchange;61 const image = exchanges[name];62
63 return (64 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">65 <div>66 <img67 src={image}68 alt={`Logo for exchange ${name}`}69 className="h-full w-full overflow-hidden rounded-full"70 />71 </div>72 <div className="w-full overflow-hidden text-ellipsis">{name}</div>73 </div>74 );75}76
77export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {78 if (!api.rowIsLeaf(row) || !row.data) return null;79
80 const field = api.columnField(column, row);81
82 if (typeof field !== "number") return "-";83
84 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";85
86 return (87 <div88 className={tw(89 "h-ful flex w-full items-center justify-end tabular-nums",90 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",91 )}92 >93 {value}94 </div>95 );96}97
98export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return null;100
101 const field = api.columnField(column, row);102
103 if (typeof field !== "number") return "-";104
105 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";106
107 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;108}109
110export const makePerfHeaderCell = (name: string, subname: string) => {111 return (_: Grid.T.HeaderParams<GridSpec>) => {112 return (113 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">114 <div>{name}</div>115 <div className="text-ln-text-light font-mono uppercase">{subname}</div>116 </div>117 );118 };119};120
121export function HeaderGroupCell({122 groupPath,123 api,124 collapsible,125 collapsed,126}: Grid.T.HeaderGroupParams<GridSpec>) {127 return (128 <>129 <div className="flex-1">{groupPath.at(-1)!}</div>130 <button131 data-ln-button="secondary"132 data-ln-icon133 data-ln-size="sm"134 onClick={() => api.columnToggleGroup(groupPath)}135 >136 {collapsible && collapsed && <ChevronRightIcon />}137 {collapsible && !collapsed && <ChevronLeftIcon />}138 </button>139 </>140 );141}Set the pin property to "start" or "end" to pin a column to the
start or end of the viewport. For details, see the
Column Pinning guide.
Column Group Expansions
Column groups become collapsible when you use the groupVisibility
property, which controls when each column is visible:
"open": Visible only when the group is expanded."closed": Visible only when the group is collapsed."always": Always visible.
A group becomes collapsible when at least one column is visible in the collapsed state and at least one is visible in the expanded state.
In the demo below, the Market Info and Performance groups are collapsible. The Market Info group starts collapsed. Click the chevron to expand it.
Column Group Expansion
19 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import "@1771technologies/lytenyte-pro/pill-manager.css";3import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";4import {5 ExchangeCell,6 HeaderGroupCell,7 makePerfHeaderCell,8 NetworkCell,9 PercentCell,10 PercentCellPositiveNegative,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";15import { useState } from "react";16
17export interface GridSpec {18 readonly data: DEXPerformanceData;19}20
21const columns: Grid.Column<GridSpec>[] = [22 {23 id: "symbol",24 cellRenderer: SymbolCell,25 width: 220,26 name: "Symbol",27 groupVisibility: "always",28 groupPath: ["Market Info"],29 },30 {31 id: "network",32 cellRenderer: NetworkCell,33 width: 220,34 name: "Network",35 groupVisibility: "open",36 groupPath: ["Market Info"],37 },38 {39 id: "exchange",40 cellRenderer: ExchangeCell,41 width: 220,42 name: "Exchange",43 groupVisibility: "open",44 groupPath: ["Market Info"],45 },46
47 {48 id: "change24h",49 cellRenderer: PercentCellPositiveNegative,50 headerRenderer: makePerfHeaderCell("Change", "24h"),51 name: "Change % 24h",52 type: "number,",53 groupVisibility: "always",54 groupPath: ["Performance"],55 },56
57 {58 id: "perf1w",59 cellRenderer: PercentCellPositiveNegative,60 headerRenderer: makePerfHeaderCell("Perf %", "1w"),61 name: "Perf % 1W",62 type: "number,",63 groupVisibility: "always",64 groupPath: ["Performance"],65 },66 {67 id: "perf1m",68 cellRenderer: PercentCellPositiveNegative,69 headerRenderer: makePerfHeaderCell("Perf %", "1m"),70 name: "Perf % 1M",71 type: "number,",72 groupVisibility: "open",73 groupPath: ["Performance"],74 },75 {76 id: "perf3m",77 cellRenderer: PercentCellPositiveNegative,78 headerRenderer: makePerfHeaderCell("Perf %", "3m"),79 name: "Perf % 3M",80 type: "number,",81 groupVisibility: "open",82 groupPath: ["Performance"],83 },84 {85 id: "perf6m",86 cellRenderer: PercentCellPositiveNegative,87 headerRenderer: makePerfHeaderCell("Perf %", "6m"),88 name: "Perf % 6M",89 type: "number,",90 groupVisibility: "open",91 groupPath: ["Performance"],92 },93 {94 id: "perfYtd",95 cellRenderer: PercentCellPositiveNegative,96 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),97 name: "Perf % YTD",98 type: "number",99 groupPath: ["Performance"],100 },101 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },102 {103 id: "volatility1m",104 cellRenderer: PercentCell,105 headerRenderer: makePerfHeaderCell("Volatility", "1m"),106 name: "Volatility 1M",107 type: "number",108 },109];110
111const base: Grid.ColumnBase<GridSpec> = { width: 80 };112
113export default function ColumnDemo() {114 const [expansions, setExpansions] = useState<Record<string, boolean>>({ "Market Info": false });115 const ds = useClientDataSource({ data: data });116
117 return (118 <div119 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight ln-header-group:text-xs"120 style={{ height: 500 }}121 >122 <Grid123 columns={columns}124 columnBase={base}125 rowSource={ds}126 columnGroupRenderer={HeaderGroupCell}127 columnGroupExpansions={expansions}128 onColumnGroupExpansionChange={setExpansions}129 />130 </div>131 );132}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";12
13export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return null;15
16 const ticker = row.data.symbolTicker;17 const symbol = row.data.symbol;18 const image = symbols[row.data.symbolTicker];19
20 return (21 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">22 <div>23 <img24 src={image}25 alt={`Logo for symbol ${symbol}`}26 className="h-full w-full overflow-hidden rounded-full"27 />28 </div>29 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">30 {ticker}31 </div>32 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>33 </div>34 );35}36
37export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {38 if (!api.rowIsLeaf(row) || !row.data) return null;39
40 const name = row.data.network;41 const image = networks[name];42
43 return (44 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">45 <div>46 <img47 src={image}48 alt={`Logo for network ${name}`}49 className="h-full w-full overflow-hidden rounded-full"50 />51 </div>52 <div className="w-full overflow-hidden text-ellipsis">{name}</div>53 </div>54 );55}56
57export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {58 if (!api.rowIsLeaf(row) || !row.data) return null;59
60 const name = row.data.exchange;61 const image = exchanges[name];62
63 return (64 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">65 <div>66 <img67 src={image}68 alt={`Logo for exchange ${name}`}69 className="h-full w-full overflow-hidden rounded-full"70 />71 </div>72 <div className="w-full overflow-hidden text-ellipsis">{name}</div>73 </div>74 );75}76
77export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {78 if (!api.rowIsLeaf(row) || !row.data) return null;79
80 const field = api.columnField(column, row);81
82 if (typeof field !== "number") return "-";83
84 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";85
86 return (87 <div88 className={tw(89 "h-ful flex w-full items-center justify-end tabular-nums",90 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",91 )}92 >93 {value}94 </div>95 );96}97
98export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return null;100
101 const field = api.columnField(column, row);102
103 if (typeof field !== "number") return "-";104
105 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";106
107 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;108}109
110export const makePerfHeaderCell = (name: string, subname: string) => {111 return (_: Grid.T.HeaderParams<GridSpec>) => {112 return (113 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">114 <div>{name}</div>115 <div className="text-ln-text-light font-mono uppercase">{subname}</div>116 </div>117 );118 };119};120
121export function HeaderGroupCell({122 groupPath,123 api,124 collapsible,125 collapsed,126}: Grid.T.HeaderGroupParams<GridSpec>) {127 return (128 <>129 <div className="flex-1">{groupPath.at(-1)!}</div>130 <button131 data-ln-button="secondary"132 data-ln-icon133 data-ln-size="sm"134 onClick={() => api.columnToggleGroup(groupPath)}135 >136 {collapsible && collapsed && <ChevronRightIcon />}137 {collapsible && !collapsed && <ChevronLeftIcon />}138 </button>139 </>140 );141}The demo uses a custom header renderer to manage the column group expansion state.
A simplified implementation is shown below. Notice the call
to api.columnToggleGroup. You can use this method to programmatically toggle the
column group expansion state.
1export function HeaderGroupCell({2 groupPath,3 api,4 collapsible,5 collapsed,6}: Grid.T.HeaderGroupParams<GridSpec>) {7 return (8 <>9 <div>{groupPath.at(-1)!}</div>10 <button onClick={() => api.columnToggleGroup(groupPath)}>11 {collapsible && collapsed && <ChevronRightIcon />}12 {collapsible && !collapsed && <ChevronLeftIcon />}13 </button>14 </>15 );16}Default Group Expansion
Collapsible groups are expanded by default. Use the
columnGroupDefaultExpansion property to change this. Setting it to
false will collapse column groups by default:
Default Expansion State
111 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import "@1771technologies/lytenyte-pro/pill-manager.css";3import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";4import {5 ExchangeCell,6 HeaderGroupCell,7 makePerfHeaderCell,8 NetworkCell,9 PercentCell,10 PercentCellPositiveNegative,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";15
16export interface GridSpec {17 readonly data: DEXPerformanceData;18}19
20const columns: Grid.Column<GridSpec>[] = [21 {22 id: "symbol",23 cellRenderer: SymbolCell,24 width: 220,25 name: "Symbol",26 groupVisibility: "always",27 groupPath: ["Market Info"],28 },29 {30 id: "network",31 cellRenderer: NetworkCell,32 width: 220,33 name: "Network",34 groupVisibility: "open",35 groupPath: ["Market Info"],36 },37 {38 id: "exchange",39 cellRenderer: ExchangeCell,40 width: 220,41 name: "Exchange",42 groupVisibility: "open",43 groupPath: ["Market Info"],44 },45
46 {47 id: "change24h",48 cellRenderer: PercentCellPositiveNegative,49 headerRenderer: makePerfHeaderCell("Change", "24h"),50 name: "Change % 24h",51 type: "number,",52 groupVisibility: "always",53 groupPath: ["Performance"],54 },55
56 {57 id: "perf1w",58 cellRenderer: PercentCellPositiveNegative,59 headerRenderer: makePerfHeaderCell("Perf %", "1w"),60 name: "Perf % 1W",61 type: "number,",62 groupVisibility: "always",63 groupPath: ["Performance"],64 },65 {66 id: "perf1m",67 cellRenderer: PercentCellPositiveNegative,68 headerRenderer: makePerfHeaderCell("Perf %", "1m"),69 name: "Perf % 1M",70 type: "number,",71 groupVisibility: "open",72 groupPath: ["Performance"],73 },74 {75 id: "perf3m",76 cellRenderer: PercentCellPositiveNegative,77 headerRenderer: makePerfHeaderCell("Perf %", "3m"),78 name: "Perf % 3M",79 type: "number,",80 groupVisibility: "open",81 groupPath: ["Performance"],82 },83 {84 id: "perf6m",85 cellRenderer: PercentCellPositiveNegative,86 headerRenderer: makePerfHeaderCell("Perf %", "6m"),87 name: "Perf % 6M",88 type: "number,",89 groupVisibility: "open",90 groupPath: ["Performance"],91 },92 {93 id: "perfYtd",94 cellRenderer: PercentCellPositiveNegative,95 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),96 name: "Perf % YTD",97 type: "number",98 groupPath: ["Performance"],99 },100 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },101 {102 id: "volatility1m",103 cellRenderer: PercentCell,104 headerRenderer: makePerfHeaderCell("Volatility", "1m"),105 name: "Volatility 1M",106 type: "number",107 },108];109
110const base: Grid.ColumnBase<GridSpec> = { width: 80 };111
112export default function ColumnDemo() {113 const ds = useClientDataSource({ data: data });114
115 return (116 <div117 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight ln-header-group:text-xs"118 style={{ height: 500 }}119 >120 <Grid121 columns={columns}122 columnBase={base}123 rowSource={ds}124 columnGroupRenderer={HeaderGroupCell}125 columnGroupDefaultExpansion={false}126 />127 </div>128 );129}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";12
13export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return null;15
16 const ticker = row.data.symbolTicker;17 const symbol = row.data.symbol;18 const image = symbols[row.data.symbolTicker];19
20 return (21 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">22 <div>23 <img24 src={image}25 alt={`Logo for symbol ${symbol}`}26 className="h-full w-full overflow-hidden rounded-full"27 />28 </div>29 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">30 {ticker}31 </div>32 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>33 </div>34 );35}36
37export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {38 if (!api.rowIsLeaf(row) || !row.data) return null;39
40 const name = row.data.network;41 const image = networks[name];42
43 return (44 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">45 <div>46 <img47 src={image}48 alt={`Logo for network ${name}`}49 className="h-full w-full overflow-hidden rounded-full"50 />51 </div>52 <div className="w-full overflow-hidden text-ellipsis">{name}</div>53 </div>54 );55}56
57export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {58 if (!api.rowIsLeaf(row) || !row.data) return null;59
60 const name = row.data.exchange;61 const image = exchanges[name];62
63 return (64 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">65 <div>66 <img67 src={image}68 alt={`Logo for exchange ${name}`}69 className="h-full w-full overflow-hidden rounded-full"70 />71 </div>72 <div className="w-full overflow-hidden text-ellipsis">{name}</div>73 </div>74 );75}76
77export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {78 if (!api.rowIsLeaf(row) || !row.data) return null;79
80 const field = api.columnField(column, row);81
82 if (typeof field !== "number") return "-";83
84 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";85
86 return (87 <div88 className={tw(89 "h-ful flex w-full items-center justify-end tabular-nums",90 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",91 )}92 >93 {value}94 </div>95 );96}97
98export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return null;100
101 const field = api.columnField(column, row);102
103 if (typeof field !== "number") return "-";104
105 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";106
107 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;108}109
110export const makePerfHeaderCell = (name: string, subname: string) => {111 return (_: Grid.T.HeaderParams<GridSpec>) => {112 return (113 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">114 <div>{name}</div>115 <div className="text-ln-text-light font-mono uppercase">{subname}</div>116 </div>117 );118 };119};120
121export function HeaderGroupCell({122 groupPath,123 api,124 collapsible,125 collapsed,126}: Grid.T.HeaderGroupParams<GridSpec>) {127 return (128 <>129 <div className="flex-1">{groupPath.at(-1)!}</div>130 <button131 data-ln-button="secondary"132 data-ln-icon133 data-ln-size="sm"134 onClick={() => api.columnToggleGroup(groupPath)}135 >136 {collapsible && collapsed && <ChevronRightIcon />}137 {collapsible && !collapsed && <ChevronLeftIcon />}138 </button>139 </>140 );141}You can also provide explicit initial expansion states. To do this, you must know how LyteNyte Grid generates group IDs.
Group IDs come from joining the groupPath array with a delimiter. The
default delimiter is -->, but you can change it using
columnGroupJoinDelimiter. For example:
1["Market Info"]; // "Market Info"2["Market Info", "Location"]; // "Market Info-->Location"With this information, you can set expansion states directly:
1const grid = Grid.useLyteNyte({2 // Other grid props3 columnGroupExpansions: { "Market Info": false },4});Sticky Group Labels
Some groups contain many columns, and horizontal scrolling can hide the group label. LyteNyte Grid allows you to style header cells so the label remains visible. The demo below shows this behavior. Try scrolling horizontally and observe that the Market Info label remains in view.
Sticky Group Labels
103 collapsed lines
1import "@1771technologies/lytenyte-pro/light-dark.css";2import "@1771technologies/lytenyte-pro/pill-manager.css";3import { Grid, useClientDataSource } from "@1771technologies/lytenyte-pro";4import {5 ExchangeCell,6 HeaderGroupCell,7 makePerfHeaderCell,8 NetworkCell,9 PercentCell,10 PercentCellPositiveNegative,11 SymbolCell,12} from "./components.jsx";13import type { DEXPerformanceData } from "@1771technologies/grid-sample-data/dex-pairs-performance";14import { data } from "@1771technologies/grid-sample-data/dex-pairs-performance";15
16export interface GridSpec {17 readonly data: DEXPerformanceData;18}19
20const columns: Grid.Column<GridSpec>[] = [21 {22 id: "symbol",23 cellRenderer: SymbolCell,24 width: 220,25 name: "Symbol",26 groupPath: ["Market Info"],27 },28 {29 id: "network",30 cellRenderer: NetworkCell,31 width: 220,32 name: "Network",33 groupPath: ["Market Info"],34 },35 {36 id: "exchange",37 cellRenderer: ExchangeCell,38 width: 220,39 name: "Exchange",40 groupPath: ["Market Info"],41 },42
43 {44 id: "change24h",45 cellRenderer: PercentCellPositiveNegative,46 headerRenderer: makePerfHeaderCell("Change", "24h"),47 name: "Change % 24h",48 type: "number,",49 groupPath: ["Performance"],50 },51
52 {53 id: "perf1w",54 cellRenderer: PercentCellPositiveNegative,55 headerRenderer: makePerfHeaderCell("Perf %", "1w"),56 name: "Perf % 1W",57 type: "number,",58 groupPath: ["Performance"],59 },60 {61 id: "perf1m",62 cellRenderer: PercentCellPositiveNegative,63 headerRenderer: makePerfHeaderCell("Perf %", "1m"),64 name: "Perf % 1M",65 type: "number,",66 groupPath: ["Performance"],67 },68 {69 id: "perf3m",70 cellRenderer: PercentCellPositiveNegative,71 headerRenderer: makePerfHeaderCell("Perf %", "3m"),72 name: "Perf % 3M",73 type: "number,",74 groupPath: ["Performance"],75 },76 {77 id: "perf6m",78 cellRenderer: PercentCellPositiveNegative,79 headerRenderer: makePerfHeaderCell("Perf %", "6m"),80 name: "Perf % 6M",81 type: "number,",82 groupPath: ["Performance"],83 },84 {85 id: "perfYtd",86 cellRenderer: PercentCellPositiveNegative,87 headerRenderer: makePerfHeaderCell("Perf %", "YTD"),88 name: "Perf % YTD",89 type: "number",90 groupPath: ["Performance"],91 },92 { id: "volatility", cellRenderer: PercentCell, name: "Volatility", type: "number" },93 {94 id: "volatility1m",95 cellRenderer: PercentCell,96 headerRenderer: makePerfHeaderCell("Volatility", "1m"),97 name: "Volatility 1M",98 type: "number",99 },100];101
102const base: Grid.ColumnBase<GridSpec> = { width: 80 };103
104export default function ColumnDemo() {105 const ds = useClientDataSource({ data: data });106
107 return (108 <div109 className="ln-grid ln-cell:text-xs ln-header:text-xs ln-header:text-ln-text-xlight ln-header-group:text-xs"110 style={{ height: 500 }}111 >112 <Grid113 columns={columns}114 columnBase={base}115 rowSource={ds}116 columnGroupRenderer={HeaderGroupCell}117 styles={{118 headerGroup: {119 style: { position: "sticky", insetInlineStart: "var(--ln-start-offset)", overflow: "unset" },120 },121 }}122 />123 </div>124 );125}1import type { ClassValue } from "clsx";2import clsx from "clsx";3import { twMerge } from "tailwind-merge";4import { exchanges, networks, symbols } from "@1771technologies/grid-sample-data/dex-pairs-performance";5
6export function tw(...c: ClassValue[]) {7 return twMerge(clsx(...c));8}9import type { Grid } from "@1771technologies/lytenyte-pro";10import type { GridSpec } from "./demo";11import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons";12
13export function SymbolCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {14 if (!api.rowIsLeaf(row) || !row.data) return null;15
16 const ticker = row.data.symbolTicker;17 const symbol = row.data.symbol;18 const image = symbols[row.data.symbolTicker];19
20 return (21 <div className="grid grid-cols-[20px_auto_auto] items-center gap-1.5">22 <div>23 <img24 src={image}25 alt={`Logo for symbol ${symbol}`}26 className="h-full w-full overflow-hidden rounded-full"27 />28 </div>29 <div className="bg-ln-gray-20 text-ln-text-dark flex h-fit items-center justify-center rounded-lg px-2 py-px text-[10px]">30 {ticker}31 </div>32 <div className="w-full overflow-hidden text-ellipsis">{symbol.split("/")[0]}</div>33 </div>34 );35}36
37export function NetworkCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {38 if (!api.rowIsLeaf(row) || !row.data) return null;39
40 const name = row.data.network;41 const image = networks[name];42
43 return (44 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">45 <div>46 <img47 src={image}48 alt={`Logo for network ${name}`}49 className="h-full w-full overflow-hidden rounded-full"50 />51 </div>52 <div className="w-full overflow-hidden text-ellipsis">{name}</div>53 </div>54 );55}56
57export function ExchangeCell({ api, row }: Grid.T.CellRendererParams<GridSpec>) {58 if (!api.rowIsLeaf(row) || !row.data) return null;59
60 const name = row.data.exchange;61 const image = exchanges[name];62
63 return (64 <div className="grid grid-cols-[20px_1fr] items-center gap-1.5">65 <div>66 <img67 src={image}68 alt={`Logo for exchange ${name}`}69 className="h-full w-full overflow-hidden rounded-full"70 />71 </div>72 <div className="w-full overflow-hidden text-ellipsis">{name}</div>73 </div>74 );75}76
77export function PercentCellPositiveNegative({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {78 if (!api.rowIsLeaf(row) || !row.data) return null;79
80 const field = api.columnField(column, row);81
82 if (typeof field !== "number") return "-";83
84 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";85
86 return (87 <div88 className={tw(89 "h-ful flex w-full items-center justify-end tabular-nums",90 field < 0 ? "text-red-600 dark:text-red-300" : "text-green-600 dark:text-green-300",91 )}92 >93 {value}94 </div>95 );96}97
98export function PercentCell({ api, column, row }: Grid.T.CellRendererParams<GridSpec>) {99 if (!api.rowIsLeaf(row) || !row.data) return null;100
101 const field = api.columnField(column, row);102
103 if (typeof field !== "number") return "-";104
105 const value = (field > 0 ? "+" : "") + (field * 100).toFixed(2) + "%";106
107 return <div className="h-ful flex w-full items-center justify-end tabular-nums">{value}</div>;108}109
110export const makePerfHeaderCell = (name: string, subname: string) => {111 return (_: Grid.T.HeaderParams<GridSpec>) => {112 return (113 <div className="flex h-full w-full flex-col items-end justify-center tabular-nums">114 <div>{name}</div>115 <div className="text-ln-text-light font-mono uppercase">{subname}</div>116 </div>117 );118 };119};120
121export function HeaderGroupCell({122 groupPath,123 api,124 collapsible,125 collapsed,126}: Grid.T.HeaderGroupParams<GridSpec>) {127 return (128 <>129 <div className="flex-1">{groupPath.at(-1)!}</div>130 <button131 data-ln-button="secondary"132 data-ln-icon133 data-ln-size="sm"134 onClick={() => api.columnToggleGroup(groupPath)}135 >136 {collapsible && collapsed && <ChevronRightIcon />}137 {collapsible && !collapsed && <ChevronLeftIcon />}138 </button>139 </>140 );141}The implementation is simple. The important part is the styles override.
Grid.HeaderGroupCell uses position: relative and overflow: hidden by default.
In this case, overriding those defaults is appropriate.
Since pinned columns can be pinned to the start, we use
the --ln-start-offset variable, which equals the total width in pixels of the
columns pinned to the start. LyteNyte Grid sets --ln-start-offset on the grid viewport.
1<Grid2 columns={columns}3 columnBase={base}4 rowSource={ds}5 columnGroupRenderer={HeaderGroupCell}6 styles={{7 headerGroup: {8 style: { position: "sticky", insetInlineStart: "var(--ln-start-offset)", overflow: "unset" },9 },10 }}11/>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 Pinning: Pin columns to the start or end of the viewport.
- Column Field: Control how a column retrieves its value for each cell.
