Line data Source code
1 : import * as React from 'react';
2 : import * as styles from './data_table.module.css';
3 : import symbols from '@ankoh/dashql-svg-symbols';
4 :
5 : import { classNames } from '../../utils/classnames.js';
6 : import { ColumnAggregationTask, ColumnAggregationVariant, ColumnGroup, LIST_COLUMN, ORDINAL_COLUMN, SKIPPED_COLUMN, STRING_COLUMN, TableAggregation, TaskStatus, WithFilter, WithFilterEpoch, WithProgress, ComputationStateVersion } from '../../compute/computation_types.js';
7 : import { RectangleWaveSpinner } from '../../ui/foundations/spinners.js';
8 : import { BrushingStateCallback, HistogramCell, HistogramFilterCallback } from './histogram_cell.js';
9 : import { MostFrequentCell, MostFrequentValueFilterCallback } from './mostfrequent_cell.js';
10 : import { DataTableLayout } from './data_table_layout.js';
11 : import { arePropsEqualWithStyle } from './memo_props.js';
12 : import { CrossFilters, HISTOGRAM_FILTER } from '../../compute/cross_filters.js';
13 :
14 : /// ---------------------------------------------------------------------------
15 : /// Header Plots Cell (row 1)
16 : /// ---------------------------------------------------------------------------
17 :
18 : export interface HeaderPlotsCellProps {
19 : columnIndex: number;
20 : style: React.CSSProperties;
21 : gridLayout: DataTableLayout;
22 : columnGroups: ColumnGroup[];
23 : columnAggregations: (ColumnAggregationVariant | null)[];
24 : columnAggregationTasks: (WithProgress<ColumnAggregationTask> | null)[];
25 : filteredColumnAggregations: (WithFilterEpoch<ColumnAggregationVariant> | null)[];
26 : filteredColumnAggregationTasks: (WithProgress<WithFilter<ColumnAggregationTask>> | null)[];
27 : filteredColumnAggregationOutdated: boolean[];
28 : tableAggregation: TableAggregation | null;
29 : selectionEpoch: ComputationStateVersion;
30 : dataSearchRequestId: number | null;
31 : crossFilters: CrossFilters;
32 : isVisible: boolean;
33 : rightmostVisibleColumn: number;
34 : onRequestFilteredColumnAggregation: (columnId: number) => void;
35 : onHistogramFilter: HistogramFilterCallback;
36 : onBrushingChange: BrushingStateCallback;
37 : onMostFrequentValueFilter: MostFrequentValueFilterCallback;
38 : }
39 :
40 1 : function HeaderPlotsCellImpl(props: HeaderPlotsCellProps): React.ReactElement {
41 1 : if (props.columnIndex >= props.gridLayout.arrowFieldByColumnIndex.length) {
42 1 : return <div style={props.style} />;
43 : }
44 :
45 : // Resolve the column summary
46 1 : let columnAggregate: ColumnAggregationVariant | null = null;
47 1 : let columnAggregationTask: WithProgress<ColumnAggregationTask> | null = null;
48 1 : let filteredColumnAggregate: WithFilterEpoch<ColumnAggregationVariant> | null = null;
49 1 : let filteredColumnAggregationTask: WithProgress<WithFilter<ColumnAggregationTask>> | null = null;
50 1 : let filteredColumnAggregationOutdated = false;
51 1 : const columnAggregateId = props.gridLayout.columnAggregateByColumnIndex[props.columnIndex];
52 1 : if (columnAggregateId != -1) {
53 1 : columnAggregate = props.columnAggregations[columnAggregateId];
54 1 : columnAggregationTask = props.columnAggregationTasks[columnAggregateId];
55 1 : filteredColumnAggregate = props.filteredColumnAggregations[columnAggregateId];
56 1 : filteredColumnAggregationTask = props.filteredColumnAggregationTasks[columnAggregateId];
57 1 : filteredColumnAggregationOutdated = props.filteredColumnAggregationOutdated[columnAggregateId] ?? false;
58 1 : }
59 :
60 1 : React.useEffect(() => {
61 1 : if (columnAggregateId == -1 || !props.isVisible || !filteredColumnAggregationOutdated) {
62 1 : return;
63 : }
64 1 : const hasUpToDateRunningTask = filteredColumnAggregationTask?.progress.status === TaskStatus.TASK_RUNNING
65 1 : && filteredColumnAggregationTask.tableVersion.filterMatches(props.selectionEpoch)
66 1 : && (filteredColumnAggregationTask.dataSearchTable?.requestId ?? null) === props.dataSearchRequestId;
67 1 : if (hasUpToDateRunningTask) {
68 1 : return;
69 : }
70 1 : props.onRequestFilteredColumnAggregation(columnAggregateId);
71 1 : }, [
72 1 : columnAggregateId,
73 1 : filteredColumnAggregationOutdated,
74 1 : filteredColumnAggregationTask,
75 1 : props.selectionEpoch,
76 1 : props.dataSearchRequestId,
77 1 : props.isVisible,
78 1 : props.onRequestFilteredColumnAggregation,
79 1 : ]);
80 :
81 1 : const isRightmost = props.columnIndex === props.rightmostVisibleColumn;
82 :
83 1 : if (!props.isVisible) {
84 1 : return <div style={props.style} />;
85 : }
86 :
87 : // Some columns carry no summary by design — most notably LIST columns (e.g. float32
88 : // embedding arrays), for which aggregating value identifiers / frequent values is
89 : // very expensive and not rendered. Show a "no summary" indicator on the primary
90 : // value column of such groups (meta/system columns stay blank).
91 1 : const columnGroupId = props.gridLayout.columnGroupByColumnIndex[props.columnIndex];
92 1 : const columnGroup = props.columnGroups[columnGroupId] as ColumnGroup | undefined;
93 1 : const crossFilter = props.crossFilters.columnFilters[columnGroupId];
94 1 : const histogramSelection = crossFilter?.type === HISTOGRAM_FILTER
95 1 : ? crossFilter.value.selection
96 1 : : null;
97 1 : const isValueColumn = props.gridLayout.isSystemColumn[props.columnIndex] == 0;
98 1 : const hasNoSummaryByDesign = isValueColumn && columnGroup?.type == LIST_COLUMN;
99 :
100 : // Special case, corner cell, top-left
101 1 : if (props.columnIndex == 0) {
102 1 : return <div className={styles.plots_corner_cell} style={props.style} />;
103 1 : } else if (hasNoSummaryByDesign) {
104 : // Cell without a summary by design: show a muted "no summary" indicator.
105 1 : return (
106 1 : <div
107 1 : className={classNames(styles.plots_cell, styles.plots_no_summary_cell, {
108 1 : [styles.plots_cell_rightmost]: isRightmost
109 1 : })}
110 1 : style={props.style}
111 1 : title="No column summary"
112 : >
113 1 : <span className={styles.plots_no_summary_label}>No summary</span>
114 1 : </div>
115 : );
116 1 : } else if (columnAggregate == null) {
117 : // Special case, cell without summary
118 1 : return <div className={classNames(styles.plots_cell, styles.plots_empty_cell, {
119 1 : [styles.plots_cell_rightmost]: isRightmost
120 1 : })} style={props.style} />;
121 1 : } else {
122 : // Check summary status
123 1 : const tableAggregation = props.tableAggregation;
124 1 : if (tableAggregation == null) {
125 1 : return (
126 1 : <div className={classNames(styles.plots_cell, {
127 1 : [styles.plots_cell_rightmost]: isRightmost
128 1 : })} style={props.style}>
129 : Table summary is null
130 1 : </div>
131 : );
132 : }
133 1 : switch (columnAggregationTask?.progress.status) {
134 1 : case TaskStatus.TASK_RUNNING:
135 1 : return (
136 1 : <div className={classNames(styles.plots_cell, styles.plots_progress, {
137 1 : [styles.plots_cell_rightmost]: isRightmost
138 1 : })} style={props.style}>
139 1 : <RectangleWaveSpinner
140 1 : className={styles.plots_progress_spinner}
141 1 : active={true}
142 1 : color={"var(--color_border)"}
143 1 : />
144 1 : </div>
145 : );
146 1 : case TaskStatus.TASK_FAILED:
147 1 : return (
148 1 : <div className={classNames(styles.plots_cell, {
149 1 : [styles.plots_cell_rightmost]: isRightmost
150 1 : })} style={props.style}>
151 : Failed
152 1 : </div>
153 : );
154 1 : case TaskStatus.TASK_SUCCEEDED:
155 1 : switch (columnAggregate?.type) {
156 1 : case ORDINAL_COLUMN:
157 1 : return (
158 1 : <HistogramCell
159 1 : className={classNames(styles.plots_cell, {
160 1 : [styles.plots_cell_rightmost]: isRightmost
161 1 : })}
162 1 : style={props.style}
163 1 : tableAggregation={tableAggregation}
164 1 : filteredColumnAggregation={filteredColumnAggregate}
165 1 : columnIndex={props.columnIndex}
166 1 : columnAggregate={columnAggregate.value}
167 1 : selection={histogramSelection}
168 1 : onFilter={props.onHistogramFilter}
169 1 : onBrushingChange={props.onBrushingChange}
170 1 : />
171 : );
172 1 : case STRING_COLUMN:
173 1 : return (
174 1 : <MostFrequentCell
175 1 : className={classNames(styles.plots_cell, {
176 1 : [styles.plots_cell_rightmost]: isRightmost
177 1 : })}
178 1 : style={props.style}
179 1 : tableAggregation={tableAggregation}
180 1 : filteredColumnAggregation={filteredColumnAggregate}
181 1 : columnIndex={props.columnIndex}
182 1 : columnAggregate={columnAggregate.value}
183 1 : onFilter={props.onMostFrequentValueFilter}
184 1 : />
185 : );
186 0 : case LIST_COLUMN:
187 0 : case SKIPPED_COLUMN:
188 : break;
189 1 : }
190 1 : }
191 1 : }
192 : // Fallback for unhandled cases
193 1 : return <div className={classNames(styles.plots_cell, {
194 1 : [styles.plots_cell_rightmost]: isRightmost
195 1 : })} style={props.style} />;
196 1 : }
197 :
198 1 : export const HeaderPlotsCell = React.memo(HeaderPlotsCellImpl, arePropsEqualWithStyle);
|