diff --git a/lib/features/workspace/grid_groupings_engine.dart b/lib/features/workspace/grid_groupings_engine.dart index e117df1..215689e 100644 --- a/lib/features/workspace/grid_groupings_engine.dart +++ b/lib/features/workspace/grid_groupings_engine.dart @@ -218,20 +218,38 @@ abstract final class GridGroupingsEngine { } } - /// Exports pivot summary to CSV format. + /// Exports pivot summary to CSV format, recursively including nested sub-groups. static String exportPivotToCsv({ required List groups, required String groupByColumnName, GroupAggregationConfig aggConfig = const GroupAggregationConfig(), + bool useCompoundKeys = false, }) { final buffer = StringBuffer(); buffer.writeln('Group Key,Count,Percentage,Aggregate'); - for (final g in groups) { + void writeCategory(GroupedCategory g, [String parentPath = '']) { final aggStr = g.aggValue != null ? g.aggValue!.toStringAsFixed(2) : '-'; + final String keyText; + if (useCompoundKeys && parentPath.isNotEmpty) { + keyText = '$parentPath > ${g.groupKey}'; + } else { + final indent = ' ' * g.level; + keyText = '$indent${g.groupKey}'; + } + final escapedKey = keyText.replaceAll('"', '""'); buffer.writeln( - '"${g.groupKey.replaceAll('"', '""')}",${g.count},${g.percentage.toStringAsFixed(2)}%,$aggStr', + '"$escapedKey",${g.count},${g.percentage.toStringAsFixed(2)}%,$aggStr', ); + + for (final sub in g.subGroups) { + final nextParent = parentPath.isEmpty ? g.groupKey : '$parentPath > ${g.groupKey}'; + writeCategory(sub, nextParent); + } + } + + for (final g in groups) { + writeCategory(g); } return buffer.toString(); diff --git a/test/features/workspace/data_grid_engines_test.dart b/test/features/workspace/data_grid_engines_test.dart index be209d3..9711cb2 100644 --- a/test/features/workspace/data_grid_engines_test.dart +++ b/test/features/workspace/data_grid_engines_test.dart @@ -312,5 +312,49 @@ void main() { expect(csv, contains('Group Key,Count,Percentage,Aggregate')); expect(csv, contains('"ACTIVE",3,60.00%,600.00')); }); + + test('exports multi-level hierarchical pivot table to CSV with nested sub-groups', () { + final multiLevelRows = [ + ['USA', 'NY', '100'], + ['USA', 'CA', '200'], + ['UK', 'London', '300'], + ]; + + final groups = GridGroupingsEngine.buildGroups( + groupColIndices: [0, 1], + rows: multiLevelRows, + aggConfig: const GroupAggregationConfig( + aggType: GroupingAggType.sum, + targetColIndex: 2, + ), + ); + + expect(groups.first.hasSubGroups, isTrue); + + // Default indented export + final csvIndented = GridGroupingsEngine.exportPivotToCsv( + groups: groups, + groupByColumnName: 'Country', + ); + + expect(csvIndented, contains('"USA",2,66.67%,300.00')); + expect(csvIndented, contains('" CA",1,33.33%,200.00')); + expect(csvIndented, contains('" NY",1,33.33%,100.00')); + expect(csvIndented, contains('"UK",1,33.33%,300.00')); + expect(csvIndented, contains('" London",1,33.33%,300.00')); + + // Compound keys export + final csvCompound = GridGroupingsEngine.exportPivotToCsv( + groups: groups, + groupByColumnName: 'Country', + useCompoundKeys: true, + ); + + expect(csvCompound, contains('"USA",2,66.67%,300.00')); + expect(csvCompound, contains('"USA > CA",1,33.33%,200.00')); + expect(csvCompound, contains('"USA > NY",1,33.33%,100.00')); + expect(csvCompound, contains('"UK",1,33.33%,300.00')); + expect(csvCompound, contains('"UK > London",1,33.33%,300.00')); + }); }); }