LCOV - code coverage report
Current view: top level - dashql-app/src/views/query_result - data_table_cell_header_plots.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 98.4 % 127 125
Test Date: 2026-11-06 19:30:32

            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);
        

Generated by: LCOV version 2.0-1