table_model.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import _ from 'lodash';
  2. interface Column {
  3. text: string;
  4. title?: string;
  5. type?: string;
  6. sort?: boolean;
  7. desc?: boolean;
  8. filterable?: boolean;
  9. unit?: string;
  10. }
  11. export default class TableModel {
  12. columns: Column[];
  13. rows: any[];
  14. type: string;
  15. columnMap: any;
  16. constructor(table?: any) {
  17. this.columns = [];
  18. this.columnMap = {};
  19. this.rows = [];
  20. this.type = 'table';
  21. if (table) {
  22. if (table.columns) {
  23. table.columns.forEach(col => this.addColumn(col));
  24. }
  25. if (table.rows) {
  26. table.rows.forEach(row => this.addRow(row));
  27. }
  28. }
  29. }
  30. sort(options) {
  31. if (options.col === null || this.columns.length <= options.col) {
  32. return;
  33. }
  34. this.rows.sort((a, b) => {
  35. a = a[options.col];
  36. b = b[options.col];
  37. // Sort null or undefined seperately from comparable values
  38. return +(a == null) - +(b == null) || +(a > b) || -(a < b);
  39. });
  40. if (options.desc) {
  41. this.rows.reverse();
  42. }
  43. this.columns[options.col].sort = true;
  44. this.columns[options.col].desc = options.desc;
  45. }
  46. addColumn(col) {
  47. if (!this.columnMap[col.text]) {
  48. this.columns.push(col);
  49. this.columnMap[col.text] = col;
  50. }
  51. }
  52. addRow(row) {
  53. this.rows.push(row);
  54. }
  55. }
  56. // Returns true if both rows have matching non-empty fields as well as matching
  57. // indexes where one field is empty and the other is not
  58. function areRowsMatching(columns, row, otherRow) {
  59. let foundFieldToMatch = false;
  60. for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
  61. if (row[columnIndex] !== undefined && otherRow[columnIndex] !== undefined) {
  62. if (row[columnIndex] !== otherRow[columnIndex]) {
  63. return false;
  64. }
  65. } else if (row[columnIndex] === undefined || otherRow[columnIndex] === undefined) {
  66. foundFieldToMatch = true;
  67. }
  68. }
  69. return foundFieldToMatch;
  70. }
  71. export function mergeTablesIntoModel(dst?: TableModel, ...tables: TableModel[]): TableModel {
  72. const model = dst || new TableModel();
  73. if (arguments.length === 1) {
  74. return model;
  75. }
  76. // Single query returns data columns and rows as is
  77. if (arguments.length === 2) {
  78. model.columns = [...tables[0].columns];
  79. model.rows = [...tables[0].rows];
  80. return model;
  81. }
  82. // Track column indexes of union: name -> index
  83. const columnNames = {};
  84. // Union of all non-value columns
  85. const columnsUnion = tables.slice().reduce((acc, series) => {
  86. series.columns.forEach(col => {
  87. const { text } = col;
  88. if (columnNames[text] === undefined) {
  89. columnNames[text] = acc.length;
  90. acc.push(col);
  91. }
  92. });
  93. return acc;
  94. }, []);
  95. // Map old column index to union index per series, e.g.,
  96. // given columnNames {A: 0, B: 1} and
  97. // data [{columns: [{ text: 'A' }]}, {columns: [{ text: 'B' }]}] => [[0], [1]]
  98. const columnIndexMapper = tables.map(series => series.columns.map(col => columnNames[col.text]));
  99. // Flatten rows of all series and adjust new column indexes
  100. const flattenedRows = tables.reduce((acc, series, seriesIndex) => {
  101. const mapper = columnIndexMapper[seriesIndex];
  102. series.rows.forEach(row => {
  103. const alteredRow = [];
  104. // Shifting entries according to index mapper
  105. mapper.forEach((to, from) => {
  106. alteredRow[to] = row[from];
  107. });
  108. acc.push(alteredRow);
  109. });
  110. return acc;
  111. }, []);
  112. // Merge rows that have same values for columns
  113. const mergedRows = {};
  114. const compactedRows = flattenedRows.reduce((acc, row, rowIndex) => {
  115. if (!mergedRows[rowIndex]) {
  116. // Look from current row onwards
  117. let offset = rowIndex + 1;
  118. // More than one row can be merged into current row
  119. while (offset < flattenedRows.length) {
  120. // Find next row that could be merged
  121. const match = _.findIndex(flattenedRows, otherRow => areRowsMatching(columnsUnion, row, otherRow), offset);
  122. if (match > -1) {
  123. const matchedRow = flattenedRows[match];
  124. // Merge values from match into current row if there is a gap in the current row
  125. for (let columnIndex = 0; columnIndex < columnsUnion.length; columnIndex++) {
  126. if (row[columnIndex] === undefined && matchedRow[columnIndex] !== undefined) {
  127. row[columnIndex] = matchedRow[columnIndex];
  128. }
  129. }
  130. // Don't visit this row again
  131. mergedRows[match] = matchedRow;
  132. // Keep looking for more rows to merge
  133. offset = match + 1;
  134. } else {
  135. // No match found, stop looking
  136. break;
  137. }
  138. }
  139. acc.push(row);
  140. }
  141. return acc;
  142. }, []);
  143. model.columns = columnsUnion;
  144. model.rows = compactedRows;
  145. return model;
  146. }