Line data Source code
1 : import * as arrow from 'apache-arrow';
2 :
3 : import { TableComputationState } from "../../compute/computation_state.js";
4 : import { ArrowTableFormatter } from "../../compute/arrow_formatter.js";
5 : import { ColumnGroup, LIST_COLUMN, ORDINAL_COLUMN, ROWNUMBER_COLUMN, SKIPPED_COLUMN, STRING_COLUMN } from "../../compute/computation_types.js";
6 :
7 : export interface DataTableLayout {
8 : columnCount: number;
9 : arrowFieldByColumnIndex: Uint32Array;
10 : columnXOffsets: Float64Array;
11 : columnAggregateByColumnIndex: Int32Array;
12 : columnGroupByColumnIndex: Uint32Array;
13 : isSystemColumn: Uint8Array;
14 : isTextColumn: Uint8Array;
15 : headerRowCount: number;
16 : }
17 :
18 1 : const MAX_VALUE_COLUMN_WIDTH = 300;
19 1 : const MIN_COLUMN_WIDTH = 120;
20 1 : const COLUMN_HEADER_ACTION_WIDTH = 24;
21 1 : const ROW_HEADER_WIDTH = 48;
22 1 : const FORMATTER_PIXEL_SCALING = 10;
23 :
24 1 : function computeColumnCount(columnGroups: ColumnGroup[], showMetaColumns: boolean, visibleColumnGroups?: ReadonlySet<number> | null): number {
25 1 : let columnCount = 0;
26 1 : for (let groupIndex = 0; groupIndex < columnGroups.length; ++groupIndex) {
27 1 : const columnGroup = columnGroups[groupIndex];
28 1 : if (columnGroup.type !== ROWNUMBER_COLUMN && visibleColumnGroups != null && !visibleColumnGroups.has(groupIndex)) {
29 1 : continue;
30 : }
31 1 : switch (columnGroup.type) {
32 1 : case ROWNUMBER_COLUMN:
33 1 : ++columnCount;
34 1 : break;
35 0 : case SKIPPED_COLUMN:
36 0 : break;
37 1 : case STRING_COLUMN:
38 1 : case LIST_COLUMN:
39 1 : ++columnCount;
40 1 : if (showMetaColumns && columnGroup.value.valueIdFieldName != null) {
41 1 : ++columnCount;
42 : }
43 1 : break;
44 1 : case ORDINAL_COLUMN:
45 1 : ++columnCount;
46 1 : if (showMetaColumns && columnGroup.value.binFieldName != null) {
47 1 : ++columnCount;
48 : }
49 : break;
50 1 : }
51 1 : }
52 1 : return columnCount;
53 1 : }
54 :
55 1 : export function computeTableLayout(formatter: ArrowTableFormatter, state: TableComputationState, showSystemColumns: boolean, headerRowCount: number, containerWidth: number = 0, visibleColumnGroups?: ReadonlySet<number> | null, resizedColumnWidths?: ReadonlyMap<string, number> | null): DataTableLayout {
56 : // Allocate column offsets
57 1 : let columnCount = computeColumnCount(state.columnGroups, showSystemColumns, visibleColumnGroups);
58 1 : const columnFields = new Uint32Array(columnCount);
59 1 : const columnOffsets = new Float64Array(columnCount + 1);
60 1 : const columnAggregateIndex = new Int32Array(columnCount);
61 1 : const columnGroupByColumnIndex = new Uint32Array(columnCount);
62 1 : const isSystemColumn = new Uint8Array(columnCount);
63 1 : const isTextColumn = new Uint8Array(columnCount);
64 1 : const tableSchema = state.dataTable.schema;
65 :
66 : // Index table fields by name
67 1 : const fieldIndexByName = new Map<string, number>();
68 1 : for (let i = 0; i < tableSchema.fields.length; ++i) {
69 1 : fieldIndexByName.set(tableSchema.fields[i].name, i);
70 : }
71 :
72 1 : for (let i = 0; i < columnCount; ++i) {
73 1 : columnAggregateIndex[i] = -1;
74 : }
75 :
76 : // Allocate column offsets
77 1 : let nextDisplayColumn = 0;
78 1 : let nextDisplayOffset = 0;
79 1 : for (let groupIndex = 0; groupIndex < state.columnGroups.length; ++groupIndex) {
80 1 : const columnGroup = state.columnGroups[groupIndex];
81 1 : if (columnGroup.type !== ROWNUMBER_COLUMN && visibleColumnGroups != null && !visibleColumnGroups.has(groupIndex)) {
82 1 : continue;
83 : }
84 1 : switch (columnGroup.type) {
85 1 : case ROWNUMBER_COLUMN: {
86 1 : const outputIndex = nextDisplayColumn++;
87 1 : columnFields[outputIndex] = fieldIndexByName.get(columnGroup.value.rowNumberFieldName)!;
88 1 : columnOffsets[outputIndex] = nextDisplayOffset;
89 1 : columnGroupByColumnIndex[outputIndex] = groupIndex;
90 1 : nextDisplayOffset += ROW_HEADER_WIDTH;
91 1 : break;
92 1 : }
93 0 : case SKIPPED_COLUMN:
94 0 : break;
95 1 : case ORDINAL_COLUMN:
96 1 : const outputIndex = nextDisplayColumn++;
97 1 : const valueColumnFormatter = formatter.columns[fieldIndexByName.get(columnGroup.value.inputFieldName)!];
98 1 : let valueColumnWidth = Math.max(
99 1 : COLUMN_HEADER_ACTION_WIDTH + Math.max(
100 1 : valueColumnFormatter.getLayoutInfo().valueAvgWidth,
101 1 : valueColumnFormatter.getColumnName().length) * FORMATTER_PIXEL_SCALING,
102 1 : MIN_COLUMN_WIDTH
103 1 : );
104 1 : valueColumnWidth = Math.min(valueColumnWidth, MAX_VALUE_COLUMN_WIDTH);
105 1 : columnFields[outputIndex] = fieldIndexByName.get(columnGroup.value.inputFieldName)!;
106 1 : columnOffsets[outputIndex] = nextDisplayOffset;
107 1 : columnAggregateIndex[outputIndex] = groupIndex;
108 1 : columnGroupByColumnIndex[outputIndex] = groupIndex;
109 1 : nextDisplayOffset += valueColumnWidth;
110 1 : if (showSystemColumns && columnGroup.value.binFieldName != null) {
111 1 : const idOutputIndex = nextDisplayColumn++;
112 1 : const idFieldIndex = fieldIndexByName.get(columnGroup.value.binFieldName)!;
113 1 : const idColumn = formatter.columns[idFieldIndex];
114 1 : const idColumnWidth = Math.max(
115 1 : COLUMN_HEADER_ACTION_WIDTH + Math.max(
116 1 : idColumn.getLayoutInfo().valueAvgWidth,
117 1 : idColumn.getColumnName().length) * FORMATTER_PIXEL_SCALING,
118 1 : MIN_COLUMN_WIDTH
119 1 : );
120 1 : columnFields[idOutputIndex] = idFieldIndex;
121 1 : columnOffsets[idOutputIndex] = nextDisplayOffset;
122 1 : columnGroupByColumnIndex[idOutputIndex] = groupIndex;
123 1 : isSystemColumn[idOutputIndex] = 1;
124 1 : nextDisplayOffset += idColumnWidth;
125 1 : }
126 1 : break;
127 1 : case STRING_COLUMN:
128 1 : case LIST_COLUMN: {
129 1 : const outputIndex = nextDisplayColumn++;
130 1 : const valueColumnFormatter = formatter.columns[fieldIndexByName.get(columnGroup.value.inputFieldName)!];
131 1 : let valueColumnWidth = Math.max(
132 1 : COLUMN_HEADER_ACTION_WIDTH + Math.max(
133 1 : valueColumnFormatter.getLayoutInfo().valueAvgWidth,
134 1 : valueColumnFormatter.getColumnName().length) * FORMATTER_PIXEL_SCALING,
135 1 : MIN_COLUMN_WIDTH
136 1 : );
137 1 : valueColumnWidth = Math.min(valueColumnWidth, MAX_VALUE_COLUMN_WIDTH);
138 1 : columnFields[outputIndex] = fieldIndexByName.get(columnGroup.value.inputFieldName)!;
139 1 : columnOffsets[outputIndex] = nextDisplayOffset;
140 1 : columnAggregateIndex[outputIndex] = groupIndex;
141 1 : columnGroupByColumnIndex[outputIndex] = groupIndex;
142 1 : nextDisplayOffset += valueColumnWidth;
143 1 : if (showSystemColumns && columnGroup.value.valueIdFieldName != null) {
144 1 : const idOutputIndex = nextDisplayColumn++;
145 1 : const idFieldIndex = fieldIndexByName.get(columnGroup.value.valueIdFieldName)!;
146 1 : const idColumn = formatter.columns[idFieldIndex];
147 1 : const idColumnWidth = Math.max(
148 1 : COLUMN_HEADER_ACTION_WIDTH + Math.max(
149 1 : idColumn.getLayoutInfo().valueAvgWidth,
150 1 : idColumn.getColumnName().length) * FORMATTER_PIXEL_SCALING,
151 1 : MIN_COLUMN_WIDTH
152 1 : );
153 1 : columnFields[idOutputIndex] = idFieldIndex;
154 1 : columnOffsets[idOutputIndex] = nextDisplayOffset;
155 1 : columnGroupByColumnIndex[idOutputIndex] = groupIndex;
156 1 : isSystemColumn[idOutputIndex] = 1;
157 1 : nextDisplayOffset += idColumnWidth;
158 1 : }
159 1 : break;
160 1 : }
161 1 : }
162 1 : }
163 1 : columnOffsets[nextDisplayColumn] = nextDisplayOffset;
164 :
165 : // Mark text columns for format peek hints
166 1 : for (let i = 0; i < nextDisplayColumn; ++i) {
167 1 : const typeId = tableSchema.fields[columnFields[i]].type.typeId;
168 1 : if (typeId === arrow.Type.Utf8 || typeId === arrow.Type.LargeUtf8) {
169 1 : isTextColumn[i] = 1;
170 : }
171 1 : }
172 :
173 : // Replace the widths of the columns that the user resized, keyed by field name
174 1 : const isResizedColumn = new Uint8Array(columnCount);
175 1 : if (resizedColumnWidths != null && resizedColumnWidths.size > 0) {
176 1 : let naturalStart = columnOffsets[0];
177 1 : for (let i = 0; i < nextDisplayColumn; ++i) {
178 1 : const naturalEnd = columnOffsets[i + 1];
179 1 : const resizedWidth = resizedColumnWidths.get(tableSchema.fields[columnFields[i]].name);
180 1 : let width = naturalEnd - naturalStart;
181 1 : if (resizedWidth !== undefined) {
182 1 : width = resizedWidth;
183 1 : isResizedColumn[i] = 1;
184 1 : }
185 1 : columnOffsets[i + 1] = columnOffsets[i] + width;
186 1 : naturalStart = naturalEnd;
187 1 : }
188 1 : nextDisplayOffset = columnOffsets[nextDisplayColumn];
189 1 : }
190 :
191 : // If columns don't fill the container, distribute extra space across data columns (not row-number or resized columns)
192 1 : if (containerWidth > 0 && nextDisplayOffset < containerWidth) {
193 1 : const stretchesColumn = (i: number) => isResizedColumn[i] == 0
194 1 : && state.columnGroups[columnGroupByColumnIndex[i]].type !== ROWNUMBER_COLUMN;
195 1 : let dataColumnCount = 0;
196 1 : for (let i = 0; i < nextDisplayColumn; ++i) {
197 1 : if (stretchesColumn(i)) {
198 1 : dataColumnCount++;
199 : }
200 : }
201 1 : if (dataColumnCount > 0) {
202 1 : const extraPerColumn = (containerWidth - nextDisplayOffset) / dataColumnCount;
203 1 : let accumulatedExtra = 0;
204 1 : for (let i = 0; i < nextDisplayColumn; ++i) {
205 1 : if (stretchesColumn(i)) {
206 1 : accumulatedExtra += extraPerColumn;
207 : }
208 1 : columnOffsets[i + 1] += accumulatedExtra;
209 1 : }
210 1 : }
211 1 : }
212 :
213 1 : return {
214 1 : columnCount,
215 1 : arrowFieldByColumnIndex: columnFields,
216 1 : columnXOffsets: columnOffsets,
217 1 : columnAggregateByColumnIndex: columnAggregateIndex,
218 1 : columnGroupByColumnIndex: columnGroupByColumnIndex,
219 1 : isSystemColumn: isSystemColumn,
220 1 : isTextColumn: isTextColumn,
221 1 : headerRowCount
222 1 : };
223 1 : }
224 :
225 : export function skipTableLayoutUpdate(old: DataTableLayout, next: DataTableLayout) {
226 : if (old.columnXOffsets.length != next.columnXOffsets.length) {
227 : return false;
228 : }
229 : for (let i = 0; i < old.columnXOffsets.length; ++i) {
230 : const delta = next.columnXOffsets[i] - old.columnXOffsets[i];
231 : if (Math.abs(delta) > 0.01) {
232 : return false;
233 : }
234 : }
235 : return true;
236 : }
|