22import memoize from 'memoize-one' ;
33import type { dh as DhType , Iterator } from '@deephaven/jsapi-types' ;
44import Log from '@deephaven/log' ;
5- import { Formatter } from '@deephaven/jsapi-utils' ;
5+ import { Formatter , TableUtils } from '@deephaven/jsapi-utils' ;
66import {
7+ assertNotNull ,
78 EventShimCustomEvent ,
89 PromiseUtils ,
910 type CancelablePromise ,
1011} from '@deephaven/utils' ;
11- import { type ModelIndex } from '@deephaven/grid' ;
12+ import { GridRange , type ModelIndex } from '@deephaven/grid' ;
1213import { type ColumnName , type UITotalsTableConfig } from './CommonTypes' ;
1314import type { DisplayColumn } from './IrisGridModel' ;
1415import ColumnHeaderGroup from './ColumnHeaderGroup' ;
1516import IrisGridModel from './IrisGridModel' ;
1617import IrisGridTableModel from './IrisGridTableModel' ;
1718import { isIrisGridTableModelTemplate } from './IrisGridTableModelTemplate' ;
19+ import IrisGridUtils from './IrisGridUtils' ;
20+ import type { IrisGridThemeType } from './IrisGridTheme' ;
1821
1922const log = Log . module ( 'IrisGridSimplePivotModel' ) ;
2023
@@ -37,8 +40,15 @@ export type KeyColumnArray = (readonly [string, string])[];
3740
3841export type SimplePivotColumnMap = ReadonlyMap < string , string > ;
3942
43+ const GRAND_TOTAL_VALUE = 'Grand Total' ;
44+
4045// TODO:
41- // - textSnapshot, snapshot for the totals row
46+ // - totals row formatting [DONE]
47+ // - totals row: copy cell unformatted
48+ // - copy selection with headers - fix column mapping, fix case with only totals row selected
49+ // - totals column move to back
50+ // - col based operations
51+ // - flags to hide unsupported table options - go to row, filters, search, organize columns, etc
4252
4353/**
4454 * Model which proxies calls to IrisGridModel.
@@ -284,6 +294,7 @@ class IrisGridSimplePivotModel extends IrisGridModel {
284294 }
285295
286296 sourceColumn ( x : ModelIndex , _ : ModelIndex ) : DhType . Column {
297+ // TODO:
287298 return this . columns [ x ] ; // - this.schema.rowColNames.length];
288299 }
289300
@@ -297,27 +308,32 @@ class IrisGridSimplePivotModel extends IrisGridModel {
297308 return undefined ;
298309 }
299310
311+ if ( ! isIrisGridTableModelTemplate ( this . model ) ) {
312+ throw new Error ( 'Invalid model, textValueForCell not available' ) ;
313+ }
314+
300315 const column = this . sourceColumn ( x , y ) ;
316+
301317 // TODO:
302- // const hasCustomColumnFormat = this.getCachedCustomColumnFormatFlag(
303- // this.formatter,
304- // column.name,
305- // column.type
306- // );
307- // let formatOverride;
308- // if (!hasCustomColumnFormat) {
309- // const formatForCell = this.formatForCell(x, y);
310- // if (formatForCell?.formatString != null) {
311- // formatOverride = formatForCell;
312- // }
313- // }
314- const text = this . displayString (
318+ const hasCustomColumnFormat = this . model . getCachedCustomColumnFormatFlag (
319+ this . formatter ,
320+ column . name ,
321+ column . type
322+ ) ;
323+ let formatOverride ;
324+ if ( ! hasCustomColumnFormat ) {
325+ const formatForCell = this . formatForCell ( x , y ) ;
326+ if ( formatForCell ?. formatString != null ) {
327+ formatOverride = formatForCell ;
328+ }
329+ }
330+ const text = this . model . displayString (
315331 value ,
316332 column . type ,
317- column . name
318- // formatOverride
333+ column . name ,
334+ formatOverride
319335 ) ;
320- // this.cacheFormattedValue(x, y, text);
336+ this . model . cacheFormattedValue ( x , y , text ) ;
321337 return text ;
322338 }
323339
@@ -326,7 +342,7 @@ class IrisGridSimplePivotModel extends IrisGridModel {
326342 if ( x >= this . schema . rowColNames . length ) {
327343 return this . textValueForCell ( x , y ) ?? '' ;
328344 }
329- return x === 0 ? 'Grand Total' : '' ;
345+ return x === 0 ? GRAND_TOTAL_VALUE : '' ;
330346 }
331347 return this . model . textForCell ( x , y ) ;
332348 }
@@ -630,6 +646,117 @@ class IrisGridSimplePivotModel extends IrisGridModel {
630646 } ) ;
631647 }
632648
649+ async snapshot (
650+ ranges : readonly GridRange [ ] ,
651+ includeHeaders = false ,
652+ formatValue : ( value : unknown , column : DhType . Column ) => unknown = value =>
653+ value ,
654+ consolidateRanges = true
655+ ) : Promise < unknown [ ] [ ] > {
656+ if ( ! isIrisGridTableModelTemplate ( this . model ) ) {
657+ throw new Error ( 'Invalid model, snapshot not available' ) ;
658+ }
659+
660+ const consolidated = consolidateRanges
661+ ? GridRange . consolidate ( ranges )
662+ : ranges ;
663+ if ( ! IrisGridUtils . isValidSnapshotRanges ( consolidated ) ) {
664+ throw new Error ( `Invalid snapshot ranges ${ ranges } ` ) ;
665+ }
666+
667+ let hasTotals = false ;
668+ const tableRanges : GridRange [ ] = [ ] ;
669+
670+ const tableSize = this . model . table . size ;
671+
672+ for ( let i = 0 ; i < consolidated . length ; i += 1 ) {
673+ const range = consolidated [ i ] ;
674+ assertNotNull ( range . endRow ) ;
675+ assertNotNull ( range . startRow ) ;
676+ // Separate out the range that is part of the actual table
677+ if ( range . endRow === tableSize ) {
678+ hasTotals = true ;
679+ if ( range . startRow < tableSize ) {
680+ tableRanges . push (
681+ new GridRange (
682+ range . startColumn ,
683+ range . startRow ,
684+ range . endColumn ,
685+ range . endRow - 1
686+ )
687+ ) ;
688+ }
689+ } else {
690+ tableRanges . push ( range ) ;
691+ }
692+ }
693+ const result =
694+ tableRanges . length === 0
695+ ? [ ]
696+ : await this . model . snapshot (
697+ tableRanges ,
698+ includeHeaders ,
699+ formatValue ,
700+ consolidateRanges
701+ ) ;
702+
703+ const columns = IrisGridUtils . columnsFromRanges ( consolidated , this . columns ) ;
704+
705+ if ( hasTotals ) {
706+ const rowData = columns . map ( column => {
707+ const index = this . getColumnIndexByName ( column . name ) ;
708+ assertNotNull ( index ) ;
709+ return index === 0
710+ ? GRAND_TOTAL_VALUE
711+ : formatValue ( this . valueForCell ( index , tableSize ) , column ) ;
712+ } ) ;
713+ result . push ( rowData ) ;
714+ }
715+
716+ return result ;
717+ }
718+
719+ colorForCell ( x : ModelIndex , y : ModelIndex , theme : IrisGridThemeType ) : string {
720+ if ( ! isIrisGridTableModelTemplate ( this . model ) ) {
721+ throw new Error ( 'Invalid model, colorForCell not available' ) ;
722+ }
723+
724+ if ( this . schema . hasTotals && y === this . rowCount - 1 ) {
725+ if ( x >= this . schema . rowColNames . length ) {
726+ const value = this . valueForCell ( x , y ) ;
727+ if ( value == null || value === '' ) {
728+ assertNotNull ( theme . nullStringColor ) ;
729+ return theme . nullStringColor ;
730+ }
731+
732+ // Format based on the value/type of the cell
733+ if ( value != null ) {
734+ const column = this . sourceColumn ( x , y ) ;
735+ if ( TableUtils . isDateType ( column . type ) || column . name === 'Date' ) {
736+ assertNotNull ( theme . dateColor ) ;
737+ return theme . dateColor ;
738+ }
739+ if ( TableUtils . isNumberType ( column . type ) ) {
740+ if ( ( value as number ) > 0 ) {
741+ assertNotNull ( theme . positiveNumberColor ) ;
742+ return theme . positiveNumberColor ;
743+ }
744+ if ( ( value as number ) < 0 ) {
745+ assertNotNull ( theme . negativeNumberColor ) ;
746+ return theme . negativeNumberColor ;
747+ }
748+ assertNotNull ( theme . zeroNumberColor ) ;
749+ return theme . zeroNumberColor ;
750+ }
751+ }
752+ }
753+
754+ return theme . textColor ;
755+ }
756+
757+ return this . model . colorForCell ( x , y , theme ) ;
758+ }
759+
633760 startListening ( ) : void {
634761 super . startListening ( ) ;
635762
0 commit comments