@malloydata/render 0.0.58-dev230714185813 → 0.0.58-dev230715003933

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,15 @@
1
- import { DataColumn } from '@malloydata/malloy';
1
+ import { DataColumn, Field, Tags } from '@malloydata/malloy';
2
2
  import { StyleDefaults } from '../data_styles';
3
3
  import { ContainerRenderer } from './container';
4
+ import { Renderer } from '../renderer';
4
5
  export declare class HTMLTableRenderer extends ContainerRenderer {
5
6
  protected childrenStyleDefaults: StyleDefaults;
6
7
  render(table: DataColumn): Promise<HTMLElement>;
8
+ generatePivotedCells(table: DataColumn, shouldTranspose: boolean): Promise<Map<string, Map<string, HTMLTableCellElement>>>;
9
+ generateNoValueCell(table: DataColumn, field: Field, shouldTranspose: boolean): HTMLTableCellElement;
10
+ tagIsPresent(tags: Tags | undefined, tag: string): boolean;
11
+ createCellAndRender(childRenderer: Renderer, value: DataColumn, shouldTranspose: boolean): Promise<HTMLTableCellElement>;
12
+ createHeaderCell(field: Field, shouldTranspose: boolean): HTMLTableCellElement;
13
+ createCell(childRenderer: Renderer, shouldTranspose: boolean): HTMLTableCellElement;
14
+ renderChild(value: DataColumn): Promise<HTMLElement>;
7
15
  }
@@ -28,6 +28,25 @@ const container_1 = require("./container");
28
28
  const number_1 = require("./number");
29
29
  const utils_1 = require("./utils");
30
30
  const tags_utils_1 = require("../tags_utils");
31
+ class PivotedField {
32
+ constructor(parentField, values) {
33
+ this.parentField = parentField;
34
+ this.values = values;
35
+ this.key = JSON.stringify({
36
+ parentField: this.parentField.name,
37
+ values: this.values.filter(v => (v.isScalar() ? v.key : '')),
38
+ });
39
+ }
40
+ }
41
+ class PivotedColumnField {
42
+ constructor(pivotedField, field) {
43
+ this.pivotedField = pivotedField;
44
+ this.field = field;
45
+ }
46
+ isPivotedColumnField() {
47
+ return this instanceof PivotedColumnField;
48
+ }
49
+ }
31
50
  class HTMLTableRenderer extends container_1.ContainerRenderer {
32
51
  constructor() {
33
52
  super(...arguments);
@@ -36,37 +55,69 @@ class HTMLTableRenderer extends container_1.ContainerRenderer {
36
55
  };
37
56
  }
38
57
  async render(table) {
58
+ var _a;
39
59
  if (!table.isArray() && !table.isRecord()) {
40
60
  throw new Error('Invalid type for Table Renderer');
41
61
  }
42
- const shouldTranspose = this.tags
43
- ? this.tags.getMalloyTags().properties['transpose'] === true
44
- : false;
62
+ const shouldTranspose = this.tagIsPresent(this.tags, 'transpose');
45
63
  if (shouldTranspose && table.field.intrinsicFields.length > 20) {
46
64
  throw new Error('Transpose limit of 20 columns exceeded.');
47
65
  }
48
66
  let rowIndex = 0;
49
67
  let columnIndex = 0;
50
- const cells = [];
68
+ let cells = [];
51
69
  cells[rowIndex] = [];
70
+ const columnFields = [];
71
+ let pivotDepth = 0;
52
72
  for (const field of table.field.intrinsicFields) {
53
73
  if ((0, tags_utils_1.isFieldHidden)(field)) {
54
74
  continue;
55
75
  }
56
- let name = (0, utils_1.formatTitle)(this.options, field, this.options.dataStyles[field.name], field.parentExplore.queryTimezone);
57
- const childRenderer = this.childRenderers[name];
58
- const isNumeric = childRenderer instanceof number_1.HTMLNumberRenderer;
59
- const headerCell = this.document.createElement('th');
60
- headerCell.style.cssText = `
61
- padding: 8px;
62
- color: var(--malloy-title-color, #505050);
63
- border-bottom: 1px solid var(--malloy-border-color, #eaeaea);
64
- text-align: ${!isNumeric || shouldTranspose ? 'left' : 'right'};
65
- `;
66
- name = name.replace(/_/g, '_&#8203;');
67
- headerCell.innerHTML = name;
68
- cells[rowIndex][columnIndex] = headerCell;
69
- columnIndex++;
76
+ const childRenderer = this.childRenderers[field.name];
77
+ const shouldPivot = childRenderer instanceof HTMLTableRenderer &&
78
+ this.tagIsPresent(childRenderer.tags, 'pivot');
79
+ if (shouldPivot) {
80
+ const pivotedFields = new Map();
81
+ let dimensions = undefined;
82
+ let nonDimensions = [];
83
+ for (const row of table) {
84
+ const dc = row.cell(field);
85
+ if (!dc.isArray() && !dc.isRecord()) {
86
+ throw new Error(`Cannot pivot field ${field.name}.`);
87
+ }
88
+ if (!dimensions) {
89
+ const fieldi = dc.field;
90
+ dimensions = fieldi.dimensions;
91
+ nonDimensions = fieldi.allFields.filter(f => dimensions.indexOf(f) < 0);
92
+ if (nonDimensions.length === 0) {
93
+ throw new Error(`Can not pivot ${field.name} since all of its fields are dimensions.`);
94
+ }
95
+ }
96
+ for (const innerRow of dc) {
97
+ const pivotedField = new PivotedField(field, dimensions.map(d => innerRow.cell(d)));
98
+ const pfKey = pivotedField.key;
99
+ if (!pivotedFields[pfKey]) {
100
+ pivotedFields.set(pfKey, pivotedField);
101
+ }
102
+ }
103
+ }
104
+ for (const pf of pivotedFields) {
105
+ for (const nonDimension of nonDimensions) {
106
+ columnFields.push(new PivotedColumnField(pf[1], nonDimension));
107
+ cells[rowIndex][columnIndex] = childRenderer.createHeaderCell(nonDimension, shouldTranspose);
108
+ columnIndex++;
109
+ }
110
+ }
111
+ pivotDepth = Math.max(pivotDepth, dimensions.length);
112
+ }
113
+ else {
114
+ cells[rowIndex][columnIndex] = this.createHeaderCell(field, shouldTranspose);
115
+ columnFields.push(field);
116
+ columnIndex++;
117
+ }
118
+ }
119
+ if (pivotDepth > 0 && columnFields.length > 30) {
120
+ throw new Error('Pivot limit of 30 columns exceeded.');
70
121
  }
71
122
  if (!shouldTranspose && this.options.isDrillingEnabled) {
72
123
  const drillHeader = this.document.createElement('th');
@@ -80,55 +131,109 @@ class HTMLTableRenderer extends container_1.ContainerRenderer {
80
131
  columnIndex++;
81
132
  }
82
133
  rowIndex++;
83
- for (const row of table) {
84
- cells[rowIndex] = [];
85
- columnIndex = 0;
86
- for (const field of table.field.intrinsicFields) {
87
- if ((0, tags_utils_1.isFieldHidden)(field)) {
88
- continue;
134
+ const pivotHeaderCells = [];
135
+ for (let r = 0; r < pivotDepth; r++) {
136
+ const row = [];
137
+ let lastPivotedColumnHash = null;
138
+ for (const field of columnFields) {
139
+ if (field instanceof PivotedColumnField) {
140
+ const headerCell = this.document.createElement('th');
141
+ headerCell.style.cssText = `
142
+ padding: 8px;
143
+ color: var(--malloy-title-color, #505050);
144
+ border-bottom: 1px solid var(--malloy-border-color, #eaeaea);
145
+ `;
146
+ const pfKey = field.pivotedField.key;
147
+ const valueIndex = field.pivotedField.values.length - pivotDepth + r;
148
+ if (lastPivotedColumnHash !== pfKey && valueIndex >= 0) {
149
+ const value = field.pivotedField.values[valueIndex];
150
+ headerCell.appendChild(await this.childRenderers[field.pivotedField.parentField.name].renderChild(value));
151
+ lastPivotedColumnHash = pfKey;
152
+ }
153
+ row.push(headerCell);
89
154
  }
90
- const childRenderer = this.childRenderers[field.name];
91
- const isNumeric = childRenderer instanceof number_1.HTMLNumberRenderer;
92
- await (0, utils_1.yieldTask)();
93
- const rendered = await childRenderer.render(row.cell(field));
94
- const cellElement = this.document.createElement('td');
95
- cellElement.style.cssText = `
96
- padding: ${childRenderer instanceof HTMLTableRenderer ? '0' : '8px'};
97
- vertical-align: top;
155
+ else {
156
+ const headerCell = this.document.createElement('th');
157
+ headerCell.style.cssText = `
158
+ padding: 8px;
159
+ color: var(--malloy-title-color, #505050);
98
160
  border-bottom: 1px solid var(--malloy-border-color, #eaeaea);
99
- ${isNumeric || shouldTranspose ? 'text-align: right;' : ''}
100
161
  `;
101
- cellElement.appendChild(rendered);
102
- cells[rowIndex][columnIndex] = cellElement;
103
- columnIndex++;
162
+ row.push(headerCell);
163
+ }
104
164
  }
105
- // TODO(figutierrez): Deal with drill when transpose is on.
106
- if (!shouldTranspose && this.options.isDrillingEnabled) {
107
- const drillCell = this.document.createElement('td');
108
- const drillIcon = (0, utils_1.createDrillIcon)(this.document);
109
- drillCell.appendChild(drillIcon);
110
- drillCell.style.cssText = `
165
+ pivotHeaderCells.push(row);
166
+ rowIndex++;
167
+ }
168
+ cells = [...pivotHeaderCells, ...cells];
169
+ try {
170
+ for (const row of table) {
171
+ cells[rowIndex] = [];
172
+ columnIndex = 0;
173
+ let currentPivotedFieldKey = '';
174
+ let pivotedCells = new Map();
175
+ for (const field of columnFields) {
176
+ if (field instanceof PivotedColumnField) {
177
+ const childRenderer = this.childRenderers[field.pivotedField.parentField.name];
178
+ const childTableRecord = row.cell(field.pivotedField.parentField);
179
+ await (0, utils_1.yieldTask)();
180
+ if (field.pivotedField.key !== currentPivotedFieldKey) {
181
+ pivotedCells = await childRenderer.generatePivotedCells(childTableRecord, shouldTranspose);
182
+ currentPivotedFieldKey = field.pivotedField.key;
183
+ }
184
+ const pfKey = field.pivotedField.key;
185
+ if (pivotedCells.has(pfKey) &&
186
+ ((_a = pivotedCells.get(pfKey)) === null || _a === void 0 ? void 0 : _a.has(field.field.name))) {
187
+ cells[rowIndex][columnIndex] = pivotedCells
188
+ .get(pfKey)
189
+ .get(field.field.name);
190
+ }
191
+ else {
192
+ cells[rowIndex][columnIndex] = childRenderer.generateNoValueCell(childTableRecord, field.field, shouldTranspose);
193
+ }
194
+ columnIndex++;
195
+ // back
196
+ }
197
+ else {
198
+ if ((0, tags_utils_1.isFieldHidden)(field)) {
199
+ continue;
200
+ }
201
+ const childRenderer = this.childRenderers[field.name];
202
+ cells[rowIndex][columnIndex] = await this.createCellAndRender(childRenderer, row.cell(field), shouldTranspose);
203
+ columnIndex++;
204
+ }
205
+ }
206
+ // TODO(figutierrez): Deal with drill when transpose is on.
207
+ if (!shouldTranspose && this.options.isDrillingEnabled) {
208
+ const drillCell = this.document.createElement('td');
209
+ const drillIcon = (0, utils_1.createDrillIcon)(this.document);
210
+ drillCell.appendChild(drillIcon);
211
+ drillCell.style.cssText = `
111
212
  padding: 8px;
112
213
  vertical-align: top;
113
214
  border-bottom: 1px solid var(--malloy-border-color, #eaeaea);
114
215
  width: 25px;
115
216
  cursor: pointer
116
217
  `;
117
- drillCell.onclick = () => {
118
- if (this.options.onDrill) {
119
- const { drillQuery, drillFilters } = (0, drill_1.getDrillQuery)(row);
120
- this.options.onDrill(drillQuery, drillIcon, drillFilters);
121
- }
122
- };
123
- cells[rowIndex][columnIndex] = drillCell;
124
- columnIndex++;
218
+ drillCell.onclick = () => {
219
+ if (this.options.onDrill) {
220
+ const { drillQuery, drillFilters } = (0, drill_1.getDrillQuery)(row);
221
+ this.options.onDrill(drillQuery, drillIcon, drillFilters);
222
+ }
223
+ };
224
+ cells[rowIndex][columnIndex] = drillCell;
225
+ columnIndex++;
226
+ }
227
+ rowIndex++;
125
228
  }
126
- rowIndex++;
229
+ }
230
+ catch (e) {
231
+ throw new Error(`${e.toString()} ${e.stack}`);
127
232
  }
128
233
  const tableElement = this.document.createElement('table');
129
234
  let tableSection = this.document.createElement('thead');
130
235
  for (let row = 0; row < (shouldTranspose ? columnIndex : rowIndex); row++) {
131
- if (row === 1) {
236
+ if (row === 1 + pivotDepth) {
132
237
  tableElement.appendChild(tableSection);
133
238
  tableSection = this.document.createElement('tbody');
134
239
  }
@@ -148,6 +253,68 @@ class HTMLTableRenderer extends container_1.ContainerRenderer {
148
253
  `;
149
254
  return tableElement;
150
255
  }
256
+ async generatePivotedCells(table, shouldTranspose) {
257
+ const result = new Map();
258
+ if (!table.isArray() && !table.isRecord()) {
259
+ throw new Error(`Could not pivot ${table.field.name}`);
260
+ }
261
+ const dimensions = table.field.dimensions;
262
+ for (const row of table) {
263
+ const pf = new PivotedField(table.field, dimensions.map(f => row.cell(f.name)));
264
+ const renderedCells = new Map();
265
+ for (const nonDimension of table.field.allFields.filter(f => dimensions.indexOf(f) < 0)) {
266
+ const childRenderer = this.childRenderers[nonDimension.name];
267
+ renderedCells.set(nonDimension.name, await this.createCellAndRender(childRenderer, row.cell(nonDimension.name), shouldTranspose));
268
+ }
269
+ result.set(pf.key, renderedCells);
270
+ }
271
+ return result;
272
+ }
273
+ generateNoValueCell(table, field, shouldTranspose) {
274
+ if (!table.isArray() && !table.isRecord()) {
275
+ throw new Error(`Could not pivot ${table.field.name}`);
276
+ }
277
+ const cell = this.createCell(this.childRenderers[field.name], shouldTranspose);
278
+ cell.innerHTML = '-';
279
+ return cell;
280
+ }
281
+ tagIsPresent(tags, tag) {
282
+ return tags ? tags.getMalloyTags().properties[tag] === true : false;
283
+ }
284
+ async createCellAndRender(childRenderer, value, shouldTranspose) {
285
+ const cell = this.createCell(childRenderer, shouldTranspose);
286
+ cell.appendChild(await childRenderer.render(value));
287
+ return cell;
288
+ }
289
+ createHeaderCell(field, shouldTranspose) {
290
+ let name = (0, utils_1.formatTitle)(this.options, field, this.options.dataStyles[field.name], field.parentExplore.queryTimezone);
291
+ const isNumeric = this.childRenderers[field.name] instanceof number_1.HTMLNumberRenderer;
292
+ const headerCell = this.document.createElement('th');
293
+ headerCell.style.cssText = `
294
+ padding: 8px;
295
+ color: var(--malloy-title-color, #505050);
296
+ border-bottom: 1px solid var(--malloy-border-color, #eaeaea);
297
+ text-align: ${isNumeric || shouldTranspose ? 'right' : 'left'};
298
+ `;
299
+ name = name.replace(/_/g, '_&#8203;');
300
+ headerCell.innerHTML = name;
301
+ return headerCell;
302
+ }
303
+ createCell(childRenderer, shouldTranspose) {
304
+ const isNumeric = childRenderer instanceof number_1.HTMLNumberRenderer;
305
+ (0, utils_1.yieldTask)();
306
+ const cellElement = this.document.createElement('td');
307
+ cellElement.style.cssText = `
308
+ padding: ${childRenderer instanceof HTMLTableRenderer ? '0' : '8px'};
309
+ vertical-align: top;
310
+ border-bottom: 1px solid var(--malloy-border-color, #eaeaea);
311
+ ${isNumeric || shouldTranspose ? 'text-align: right;' : ''}
312
+ `;
313
+ return cellElement;
314
+ }
315
+ async renderChild(value) {
316
+ return this.childRenderers[value.field.name].render(value);
317
+ }
151
318
  }
152
319
  exports.HTMLTableRenderer = HTMLTableRenderer;
153
320
  //# sourceMappingURL=table.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/render",
3
- "version": "0.0.58-dev230714185813",
3
+ "version": "0.0.58-dev230715003933",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,7 +18,7 @@
18
18
  "prepublishOnly": "npm run build"
19
19
  },
20
20
  "dependencies": {
21
- "@malloydata/malloy": "^0.0.58-dev230714185813",
21
+ "@malloydata/malloy": "^0.0.58-dev230715003933",
22
22
  "@types/luxon": "^2.4.0",
23
23
  "lodash": "^4.17.20",
24
24
  "luxon": "^2.4.0",