@vue-pivottable/subtotal-renderer 0.1.0

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.
package/dist/vue2.mjs ADDED
@@ -0,0 +1,453 @@
1
+ import { createSubtotalAggregatorGetter } from "./core.mjs";
2
+ import { generateColKeysWithSubtotals, generateRowKeysWithSubtotals, getSubtotalKeys, isSubtotalKey } from "./core.mjs";
3
+ function redColorScaleGenerator(values) {
4
+ const min = Math.min.apply(Math, values);
5
+ const max = Math.max.apply(Math, values);
6
+ return (x) => {
7
+ const nonRed = 255 - Math.round(255 * (x - min) / (max - min));
8
+ return { backgroundColor: `rgb(255,${nonRed},${nonRed})` };
9
+ };
10
+ }
11
+ let _PivotData = null;
12
+ function createSubtotalRenderers(PivotData) {
13
+ _PivotData = PivotData;
14
+ return {
15
+ "Subtotal Table": makeSubtotalRenderer({ name: "vue-subtotal-table" })
16
+ };
17
+ }
18
+ function generateRowKeysWithSubtotals2(rowKeys, depth) {
19
+ if (depth <= 1) {
20
+ return rowKeys.map((key) => ({ key, isSubtotal: false, level: key.length }));
21
+ }
22
+ const result = [];
23
+ for (let i = 0; i < rowKeys.length; i++) {
24
+ const rowKey = rowKeys[i];
25
+ const nextRowKey = rowKeys[i + 1];
26
+ result.push({ key: rowKey, isSubtotal: false, level: rowKey.length });
27
+ for (let level = depth - 1; level >= 1; level--) {
28
+ const currentPrefix = rowKey.slice(0, level);
29
+ const nextPrefix = nextRowKey ? nextRowKey.slice(0, level) : null;
30
+ const levelChanges = !nextPrefix || JSON.stringify(currentPrefix) !== JSON.stringify(nextPrefix);
31
+ if (levelChanges) {
32
+ result.push({
33
+ key: currentPrefix,
34
+ isSubtotal: true,
35
+ level,
36
+ subtotalLabel: `${currentPrefix[level - 1]} Subtotal`
37
+ });
38
+ }
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+ function spanSize(rowItems, i, j) {
44
+ const arr = rowItems.map((item) => item.key);
45
+ if (rowItems[i].isSubtotal) {
46
+ return 1;
47
+ }
48
+ if (i !== 0 && !rowItems[i - 1].isSubtotal) {
49
+ let asPrevious = true;
50
+ for (let x = 0; x <= j; x++) {
51
+ if (arr[i - 1][x] !== arr[i][x]) {
52
+ asPrevious = false;
53
+ break;
54
+ }
55
+ }
56
+ if (asPrevious) {
57
+ return -1;
58
+ }
59
+ }
60
+ let len = 0;
61
+ while (i + len < arr.length) {
62
+ if (rowItems[i + len].isSubtotal)
63
+ break;
64
+ let same = true;
65
+ for (let x = 0; x <= j; x++) {
66
+ if (arr[i][x] !== arr[i + len][x]) {
67
+ same = false;
68
+ break;
69
+ }
70
+ }
71
+ if (!same)
72
+ break;
73
+ len++;
74
+ }
75
+ return len;
76
+ }
77
+ function colSpanSize(arr, i, j) {
78
+ if (i !== 0) {
79
+ let asPrevious = true;
80
+ for (let x = 0; x <= j; x++) {
81
+ if (arr[i - 1][x] !== arr[i][x]) {
82
+ asPrevious = false;
83
+ break;
84
+ }
85
+ }
86
+ if (asPrevious) {
87
+ return -1;
88
+ }
89
+ }
90
+ let len = 0;
91
+ while (i + len < arr.length) {
92
+ let same = true;
93
+ for (let x = 0; x <= j; x++) {
94
+ if (arr[i][x] !== arr[i + len][x]) {
95
+ same = false;
96
+ break;
97
+ }
98
+ }
99
+ if (!same)
100
+ break;
101
+ len++;
102
+ }
103
+ return len;
104
+ }
105
+ function makeSubtotalRenderer(opts = {}) {
106
+ return {
107
+ name: opts.name || "vue-subtotal-table",
108
+ props: {
109
+ data: {
110
+ type: [Array, Object, Function],
111
+ required: true
112
+ },
113
+ aggregators: Object,
114
+ aggregatorName: {
115
+ type: String,
116
+ default: "Count"
117
+ },
118
+ cols: {
119
+ type: Array,
120
+ default: () => []
121
+ },
122
+ rows: {
123
+ type: Array,
124
+ default: () => []
125
+ },
126
+ vals: {
127
+ type: Array,
128
+ default: () => []
129
+ },
130
+ valueFilter: {
131
+ type: Object,
132
+ default: () => ({})
133
+ },
134
+ sorters: {
135
+ type: [Function, Object],
136
+ default: () => ({})
137
+ },
138
+ derivedAttributes: {
139
+ type: [Function, Object],
140
+ default: () => ({})
141
+ },
142
+ rowOrder: {
143
+ type: String,
144
+ default: "key_a_to_z"
145
+ },
146
+ colOrder: {
147
+ type: String,
148
+ default: "key_a_to_z"
149
+ },
150
+ tableColorScaleGenerator: {
151
+ type: Function,
152
+ default: redColorScaleGenerator
153
+ },
154
+ tableOptions: {
155
+ type: Object,
156
+ default: () => ({})
157
+ },
158
+ localeStrings: {
159
+ type: Object,
160
+ default: () => ({
161
+ totals: "Totals"
162
+ })
163
+ },
164
+ rowTotal: {
165
+ type: Boolean,
166
+ default: true
167
+ },
168
+ colTotal: {
169
+ type: Boolean,
170
+ default: true
171
+ },
172
+ labels: {
173
+ type: Object,
174
+ default: () => ({})
175
+ },
176
+ // Subtotal specific props
177
+ subtotalOptions: {
178
+ type: Object,
179
+ default: () => ({
180
+ colSubtotalDisplay: {
181
+ displayOnTop: false,
182
+ enabled: true,
183
+ hideOnExpand: false
184
+ },
185
+ rowSubtotalDisplay: {
186
+ displayOnTop: false,
187
+ enabled: true,
188
+ hideOnExpand: false
189
+ },
190
+ arrowCollapsed: "▶",
191
+ arrowExpanded: "▼"
192
+ })
193
+ }
194
+ },
195
+ data() {
196
+ return {
197
+ collapsedRowKeys: /* @__PURE__ */ new Set(),
198
+ collapsedColKeys: /* @__PURE__ */ new Set()
199
+ };
200
+ },
201
+ methods: {
202
+ toggleRowCollapse(keyStr) {
203
+ if (this.collapsedRowKeys.has(keyStr)) {
204
+ this.collapsedRowKeys.delete(keyStr);
205
+ } else {
206
+ this.collapsedRowKeys.add(keyStr);
207
+ }
208
+ this.collapsedRowKeys = new Set(this.collapsedRowKeys);
209
+ this.$forceUpdate();
210
+ },
211
+ toggleColCollapse(keyStr) {
212
+ if (this.collapsedColKeys.has(keyStr)) {
213
+ this.collapsedColKeys.delete(keyStr);
214
+ } else {
215
+ this.collapsedColKeys.add(keyStr);
216
+ }
217
+ this.collapsedColKeys = new Set(this.collapsedColKeys);
218
+ this.$forceUpdate();
219
+ },
220
+ applyLabel(attr, value) {
221
+ if (this.labels && typeof this.labels[attr] === "function") {
222
+ return this.labels[attr](value);
223
+ }
224
+ return value;
225
+ }
226
+ },
227
+ render(h) {
228
+ if (!_PivotData) {
229
+ console.error("PivotData not initialized. Please use createSubtotalRenderers(PivotData) first.");
230
+ return h("div", "Error: PivotData not initialized. Use createSubtotalRenderers(PivotData).");
231
+ }
232
+ let pivotData;
233
+ try {
234
+ pivotData = new _PivotData(this.$props);
235
+ } catch (error) {
236
+ console.error(error);
237
+ return h("span", "Error rendering pivot table");
238
+ }
239
+ const colAttrs = pivotData.props.cols;
240
+ const rowAttrs = pivotData.props.rows;
241
+ const rowKeys = pivotData.getRowKeys();
242
+ const colKeys = pivotData.getColKeys();
243
+ const grandTotalAggregator = pivotData.getAggregator([], []);
244
+ const getAggregator = createSubtotalAggregatorGetter(pivotData);
245
+ const rowKeysWithSubtotals = generateRowKeysWithSubtotals2(rowKeys, rowAttrs.length);
246
+ const getClickHandler = (value, rowValues, colValues) => {
247
+ if (this.tableOptions && this.tableOptions.clickCallback) {
248
+ const filters = {};
249
+ colAttrs.forEach((attr, i) => {
250
+ if (colValues[i] !== void 0 && colValues[i] !== null) {
251
+ filters[attr] = colValues[i];
252
+ }
253
+ });
254
+ rowAttrs.forEach((attr, i) => {
255
+ if (rowValues[i] !== void 0 && rowValues[i] !== null) {
256
+ filters[attr] = rowValues[i];
257
+ }
258
+ });
259
+ return (e) => this.tableOptions.clickCallback(e, value, filters, pivotData);
260
+ }
261
+ return null;
262
+ };
263
+ const renderHeader = () => {
264
+ const headerRows = [];
265
+ colAttrs.forEach((c, j) => {
266
+ const cells = [];
267
+ if (j === 0 && rowAttrs.length !== 0) {
268
+ cells.push(h("th", {
269
+ attrs: {
270
+ colSpan: rowAttrs.length,
271
+ rowSpan: colAttrs.length
272
+ },
273
+ style: {
274
+ backgroundColor: "#f5f5f5"
275
+ }
276
+ }));
277
+ }
278
+ cells.push(h("th", { class: "pvtAxisLabel" }, c));
279
+ colKeys.forEach((colKey, i) => {
280
+ const colSpan = colSpanSize(colKeys, i, j);
281
+ if (colSpan !== -1) {
282
+ const label = this.applyLabel(colAttrs[j], colKey[j]);
283
+ cells.push(h("th", {
284
+ class: "pvtColLabel",
285
+ key: `colKey${i}-${j}`,
286
+ attrs: {
287
+ colSpan,
288
+ rowSpan: j === colAttrs.length - 1 && rowAttrs.length !== 0 ? 2 : 1
289
+ }
290
+ }, label));
291
+ }
292
+ });
293
+ if (j === 0 && this.rowTotal) {
294
+ cells.push(h("th", {
295
+ class: "pvtTotalLabel",
296
+ attrs: {
297
+ rowSpan: colAttrs.length + (rowAttrs.length === 0 ? 0 : 1)
298
+ }
299
+ }, this.localeStrings.totals));
300
+ }
301
+ headerRows.push(h("tr", { key: `colAttr${j}` }, cells));
302
+ });
303
+ if (rowAttrs.length !== 0) {
304
+ const cells = rowAttrs.map((r, i) => h("th", {
305
+ class: "pvtAxisLabel",
306
+ key: `rowAttr${i}`
307
+ }, r));
308
+ if (colAttrs.length === 0 && this.rowTotal) {
309
+ cells.push(h("th", { class: "pvtTotalLabel" }, this.localeStrings.totals));
310
+ }
311
+ headerRows.push(h("tr", cells));
312
+ }
313
+ return h("thead", headerRows);
314
+ };
315
+ const renderBody = () => {
316
+ const bodyRows = [];
317
+ rowKeysWithSubtotals.forEach((rowItem, i) => {
318
+ const { key: rowKey, isSubtotal, level, subtotalLabel } = rowItem;
319
+ const cells = [];
320
+ if (isSubtotal) {
321
+ const subtotalText = subtotalLabel || `${rowKey[rowKey.length - 1]} Subtotal`;
322
+ cells.push(h("th", {
323
+ class: "pvtRowLabel pvtSubtotalLabel",
324
+ attrs: {
325
+ colSpan: rowAttrs.length + (colAttrs.length !== 0 ? 1 : 0)
326
+ },
327
+ style: {
328
+ fontWeight: "bold",
329
+ backgroundColor: "#f0f0f0"
330
+ }
331
+ }, subtotalText));
332
+ colKeys.forEach((colKey, j) => {
333
+ const aggregator = getAggregator(rowKey, colKey);
334
+ const val = aggregator.value();
335
+ const formattedVal = aggregator.format ? aggregator.format(val) : val;
336
+ const clickHandler = getClickHandler(val, rowKey, colKey);
337
+ cells.push(h("td", {
338
+ class: "pvtVal pvtSubtotalVal",
339
+ key: `subtotal-val${i}-${j}`,
340
+ style: {
341
+ fontWeight: "bold",
342
+ backgroundColor: "#f0f0f0"
343
+ },
344
+ on: clickHandler ? { click: clickHandler } : {}
345
+ }, formattedVal));
346
+ });
347
+ if (this.rowTotal) {
348
+ const totalAggregator = getAggregator(rowKey, []);
349
+ const totalVal = totalAggregator.value();
350
+ const formattedTotal = totalAggregator.format ? totalAggregator.format(totalVal) : totalVal;
351
+ const clickHandler = getClickHandler(totalVal, rowKey, []);
352
+ cells.push(h("td", {
353
+ class: "pvtTotal pvtSubtotalTotal",
354
+ style: {
355
+ fontWeight: "bold",
356
+ backgroundColor: "#e0e0e0"
357
+ },
358
+ on: clickHandler ? { click: clickHandler } : {}
359
+ }, formattedTotal));
360
+ }
361
+ bodyRows.push(h("tr", {
362
+ key: `subtotal-row${i}`,
363
+ class: "pvtSubtotalRow"
364
+ }, cells));
365
+ } else {
366
+ rowKey.forEach((text, j) => {
367
+ const rowSpan = spanSize(rowKeysWithSubtotals, i, j);
368
+ if (rowSpan !== -1) {
369
+ const label = this.applyLabel(rowAttrs[j], text);
370
+ cells.push(h("th", {
371
+ class: "pvtRowLabel",
372
+ key: `rowLabel${i}-${j}`,
373
+ attrs: {
374
+ rowSpan,
375
+ colSpan: j === rowAttrs.length - 1 && colAttrs.length !== 0 ? 2 : 1
376
+ }
377
+ }, label));
378
+ }
379
+ });
380
+ colKeys.forEach((colKey, j) => {
381
+ const aggregator = getAggregator(rowKey, colKey);
382
+ const val = aggregator.value();
383
+ const formattedVal = aggregator.format ? aggregator.format(val) : val;
384
+ const clickHandler = getClickHandler(val, rowKey, colKey);
385
+ cells.push(h("td", {
386
+ class: "pvtVal",
387
+ key: `val${i}-${j}`,
388
+ on: clickHandler ? { click: clickHandler } : {}
389
+ }, formattedVal));
390
+ });
391
+ if (this.rowTotal) {
392
+ const totalAggregator = getAggregator(rowKey, []);
393
+ const totalVal = totalAggregator.value();
394
+ const formattedTotal = totalAggregator.format ? totalAggregator.format(totalVal) : totalVal;
395
+ const clickHandler = getClickHandler(totalVal, rowKey, []);
396
+ cells.push(h("td", {
397
+ class: "pvtTotal",
398
+ on: clickHandler ? { click: clickHandler } : {}
399
+ }, formattedTotal));
400
+ }
401
+ bodyRows.push(h("tr", { key: `row${i}` }, cells));
402
+ }
403
+ });
404
+ if (this.colTotal) {
405
+ const cells = [];
406
+ cells.push(h("th", {
407
+ class: "pvtTotalLabel",
408
+ attrs: {
409
+ colSpan: rowAttrs.length + (colAttrs.length === 0 ? 0 : 1)
410
+ }
411
+ }, this.localeStrings.totals));
412
+ colKeys.forEach((colKey, i) => {
413
+ const totalAggregator = getAggregator([], colKey);
414
+ const totalVal = totalAggregator.value();
415
+ const formattedTotal = totalAggregator.format ? totalAggregator.format(totalVal) : totalVal;
416
+ const clickHandler = getClickHandler(totalVal, [], colKey);
417
+ cells.push(h("td", {
418
+ class: "pvtTotal",
419
+ key: `colTotal${i}`,
420
+ on: clickHandler ? { click: clickHandler } : {}
421
+ }, formattedTotal));
422
+ });
423
+ if (this.rowTotal) {
424
+ const clickHandler = getClickHandler(grandTotalAggregator.value(), [], []);
425
+ cells.push(h("td", {
426
+ class: "pvtGrandTotal",
427
+ on: clickHandler ? { click: clickHandler } : {}
428
+ }, grandTotalAggregator.format(grandTotalAggregator.value())));
429
+ }
430
+ bodyRows.push(h("tr", cells));
431
+ }
432
+ return h("tbody", bodyRows);
433
+ };
434
+ return h("table", { class: "pvtTable" }, [
435
+ renderHeader(),
436
+ renderBody()
437
+ ]);
438
+ }
439
+ };
440
+ }
441
+ const SubtotalRenderers = {
442
+ "Subtotal Table": makeSubtotalRenderer({ name: "vue-subtotal-table" })
443
+ };
444
+ export {
445
+ SubtotalRenderers,
446
+ createSubtotalAggregatorGetter,
447
+ createSubtotalRenderers,
448
+ SubtotalRenderers as default,
449
+ generateColKeysWithSubtotals,
450
+ generateRowKeysWithSubtotals,
451
+ getSubtotalKeys,
452
+ isSubtotalKey
453
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@vue-pivottable/subtotal-renderer",
3
+ "version": "0.1.0",
4
+ "description": "Subtotal renderer for vue-pivottable with expand/collapse support. Supports Vue 2 and Vue 3.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./types/index.d.ts"
13
+ },
14
+ "./vue2": {
15
+ "import": "./dist/vue2.mjs",
16
+ "require": "./dist/vue2.js",
17
+ "types": "./types/index.d.ts"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "types"
23
+ ],
24
+ "scripts": {
25
+ "build": "vite build",
26
+ "dev": "vite"
27
+ },
28
+ "keywords": [
29
+ "vue",
30
+ "vue2",
31
+ "vue3",
32
+ "pivottable",
33
+ "pivot",
34
+ "subtotal",
35
+ "renderer",
36
+ "expand",
37
+ "collapse"
38
+ ],
39
+ "author": "Seungwoo321",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/vue-pivottable/subtotal-renderer.git"
44
+ },
45
+ "peerDependencies": {
46
+ "vue": "^2.6.0 || ^3.0.0",
47
+ "vue-pivottable": ">=0.4.0"
48
+ },
49
+ "devDependencies": {
50
+ "release-it": "^17.0.0",
51
+ "vite": "^4.5.0"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ }
56
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Type definitions for @vue-pivottable/subtotal-renderer
3
+ */
4
+
5
+ import { Component } from 'vue'
6
+
7
+ // ============================================================================
8
+ // Subtotal Options
9
+ // ============================================================================
10
+
11
+ export interface SubtotalDisplayOptions {
12
+ /** Display subtotals on top of the group (default: false) */
13
+ displayOnTop?: boolean
14
+ /** Enable subtotals (default: true) */
15
+ enabled?: boolean
16
+ /** Hide subtotals when group is expanded (default: false) */
17
+ hideOnExpand?: boolean
18
+ }
19
+
20
+ export interface SubtotalOptions {
21
+ /** Column subtotal display options */
22
+ colSubtotalDisplay?: SubtotalDisplayOptions
23
+ /** Row subtotal display options */
24
+ rowSubtotalDisplay?: SubtotalDisplayOptions
25
+ /** Arrow character for collapsed state (default: '▶') */
26
+ arrowCollapsed?: string
27
+ /** Arrow character for expanded state (default: '▼') */
28
+ arrowExpanded?: string
29
+ }
30
+
31
+ // ============================================================================
32
+ // Core Functions
33
+ // ============================================================================
34
+
35
+ /**
36
+ * Generate all possible subtotal key combinations for a given key
37
+ * @param key - The full key array
38
+ * @returns Array of partial keys for subtotals
39
+ */
40
+ export function getSubtotalKeys(key: string[]): string[][]
41
+
42
+ /**
43
+ * Check if a key represents a subtotal row/column
44
+ * @param key - The key to check
45
+ * @param maxDepth - Maximum depth (number of attributes)
46
+ */
47
+ export function isSubtotalKey(key: string[], maxDepth: number): boolean
48
+
49
+ /**
50
+ * Row/Column key with subtotal information
51
+ */
52
+ export interface SubtotalKeyInfo {
53
+ key: string[]
54
+ isSubtotal: boolean
55
+ level: number
56
+ isCollapsed: boolean
57
+ }
58
+
59
+ /**
60
+ * Generate row keys with subtotals inserted
61
+ * @param rowKeys - Original row keys from PivotData
62
+ * @param rowAttrsCount - Number of row attributes
63
+ * @param collapsedKeys - Set of collapsed key strings
64
+ */
65
+ export function generateRowKeysWithSubtotals(
66
+ rowKeys: string[][],
67
+ rowAttrsCount: number,
68
+ collapsedKeys?: Set<string>
69
+ ): SubtotalKeyInfo[]
70
+
71
+ /**
72
+ * Generate column keys with subtotals inserted
73
+ * @param colKeys - Original column keys from PivotData
74
+ * @param colAttrsCount - Number of column attributes
75
+ * @param collapsedKeys - Set of collapsed key strings
76
+ */
77
+ export function generateColKeysWithSubtotals(
78
+ colKeys: string[][],
79
+ colAttrsCount: number,
80
+ collapsedKeys?: Set<string>
81
+ ): SubtotalKeyInfo[]
82
+
83
+ /**
84
+ * Create a subtotal-aware aggregator getter
85
+ * @param pivotData - The PivotData instance
86
+ */
87
+ export function createSubtotalAggregatorGetter(pivotData: any): (
88
+ rowKey: string[],
89
+ colKey: string[]
90
+ ) => { value: () => any; format: (val: any) => string }
91
+
92
+ // ============================================================================
93
+ // Renderer Props
94
+ // ============================================================================
95
+
96
+ export interface SubtotalRendererProps {
97
+ data: any[] | object | Function
98
+ aggregators?: Record<string, Function>
99
+ aggregatorName?: string
100
+ cols?: string[]
101
+ rows?: string[]
102
+ vals?: string[]
103
+ valueFilter?: Record<string, Record<string, boolean>>
104
+ sorters?: Function | Record<string, Function>
105
+ derivedAttributes?: Function | Record<string, Function>
106
+ rowOrder?: 'key_a_to_z' | 'value_a_to_z' | 'value_z_to_a'
107
+ colOrder?: 'key_a_to_z' | 'value_a_to_z' | 'value_z_to_a'
108
+ tableColorScaleGenerator?: (values: number[]) => (value: number) => { backgroundColor: string }
109
+ tableOptions?: {
110
+ clickCallback?: (
111
+ event: MouseEvent,
112
+ value: any,
113
+ filters: Record<string, any>,
114
+ pivotData: any
115
+ ) => void
116
+ }
117
+ localeStrings?: {
118
+ totals?: string
119
+ }
120
+ rowTotal?: boolean
121
+ colTotal?: boolean
122
+ labels?: Record<string, (value: any) => string>
123
+ subtotalOptions?: SubtotalOptions
124
+ }
125
+
126
+ // ============================================================================
127
+ // Renderers
128
+ // ============================================================================
129
+
130
+ export interface SubtotalRendererComponent extends Component {
131
+ name: string
132
+ props: SubtotalRendererProps
133
+ }
134
+
135
+ export interface SubtotalRenderersType {
136
+ 'Subtotal Table': SubtotalRendererComponent
137
+ }
138
+
139
+ export const SubtotalRenderers: SubtotalRenderersType
140
+
141
+ export default SubtotalRenderers