@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/README.md ADDED
@@ -0,0 +1,295 @@
1
+ # @vue-pivottable/subtotal-renderer
2
+
3
+ Subtotal renderer for [vue-pivottable](https://github.com/Seungwoo321/vue-pivottable) with expand/collapse support. Inspired by [subtotal.js](https://github.com/nagarajanchinnasamy/subtotal).
4
+
5
+ Supports both **Vue 2** and **Vue 3**.
6
+
7
+ ## Screenshots
8
+
9
+ ### Vue 3 - Subtotal Table
10
+
11
+ ![Vue 3 Example](docs/screenshot-vue3.png)
12
+
13
+ ### Vue 3 - With UI
14
+
15
+ ![Vue 3 UI Example](docs/screenshot-vue3-ui.png)
16
+
17
+ ### Vue 2 - Subtotal Table
18
+
19
+ ![Vue 2 Example](docs/screenshot-vue2.png)
20
+
21
+ ### Vue 2 - With UI
22
+
23
+ ![Vue 2 UI Example](docs/screenshot-vue2-ui.png)
24
+
25
+ ## Features
26
+
27
+ - 📊 **Subtotal rows and columns** - Automatically calculates and displays subtotals for hierarchical data
28
+ - 🔄 **Expand/Collapse** - Click to expand or collapse row/column groups
29
+ - 🎨 **Styled subtotal rows** - Visual distinction for subtotal rows
30
+ - 🖱️ **Click callbacks** - Supports the same click callback as the standard renderer
31
+ - 📝 **Labels support** - Custom label formatting for row/column values
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npm install @vue-pivottable/subtotal-renderer
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ### Vue 3
42
+
43
+ ```vue
44
+ <template>
45
+ <VuePivottable
46
+ :data="data"
47
+ :rows="['Category', 'Subcategory', 'Product']"
48
+ :cols="['Region', 'City']"
49
+ :vals="['Sales']"
50
+ aggregatorName="Sum"
51
+ :renderers="SubtotalRenderers"
52
+ rendererName="Subtotal Table"
53
+ />
54
+ </template>
55
+
56
+ <script setup>
57
+ import { VuePivottable, PivotUtilities } from 'vue-pivottable'
58
+ import { createSubtotalRenderers } from '@vue-pivottable/subtotal-renderer'
59
+ import 'vue-pivottable/dist/vue-pivottable.css'
60
+
61
+ // Create SubtotalRenderers with PivotData from vue-pivottable
62
+ const SubtotalRenderers = createSubtotalRenderers(PivotUtilities.PivotData)
63
+
64
+ const data = [
65
+ { Category: 'Electronics', Subcategory: 'Phones', Product: 'iPhone', Region: 'North', City: 'Seoul', Sales: 1000 },
66
+ { Category: 'Electronics', Subcategory: 'Phones', Product: 'Samsung', Region: 'North', City: 'Seoul', Sales: 800 },
67
+ { Category: 'Electronics', Subcategory: 'Laptops', Product: 'MacBook', Region: 'North', City: 'Busan', Sales: 1500 },
68
+ { Category: 'Clothing', Subcategory: 'Shirts', Product: 'T-Shirt', Region: 'South', City: 'Daegu', Sales: 200 },
69
+ // ... more data
70
+ ]
71
+ </script>
72
+ ```
73
+
74
+ ### Vue 2
75
+
76
+ ```vue
77
+ <template>
78
+ <VuePivottable
79
+ :data="data"
80
+ :rows="['Category', 'Subcategory', 'Product']"
81
+ :cols="['Region', 'City']"
82
+ :vals="['Sales']"
83
+ aggregatorName="Sum"
84
+ :renderers="SubtotalRenderers"
85
+ rendererName="Subtotal Table"
86
+ />
87
+ </template>
88
+
89
+ <script>
90
+ import { VuePivottable, PivotUtilities } from 'vue-pivottable'
91
+ import { createSubtotalRenderers } from '@vue-pivottable/subtotal-renderer/vue2'
92
+ import 'vue-pivottable/dist/vue-pivottable.css'
93
+
94
+ // Create SubtotalRenderers with PivotData from vue-pivottable
95
+ const SubtotalRenderers = createSubtotalRenderers(PivotUtilities.PivotData)
96
+
97
+ export default {
98
+ components: { VuePivottable },
99
+ data() {
100
+ return {
101
+ SubtotalRenderers,
102
+ data: [
103
+ // ... your data
104
+ ]
105
+ }
106
+ }
107
+ }
108
+ </script>
109
+ ```
110
+
111
+ ## With PivotTable UI
112
+
113
+ You can also use the subtotal renderer with the interactive PivotTable UI:
114
+
115
+ ### Vue 3
116
+
117
+ ```vue
118
+ <template>
119
+ <VuePivottableUi
120
+ :data="data"
121
+ :rows="['Category', 'Subcategory']"
122
+ :cols="['Region']"
123
+ :vals="['Sales']"
124
+ aggregatorName="Sum"
125
+ :renderers="SubtotalRenderers"
126
+ rendererName="Subtotal Table"
127
+ />
128
+ </template>
129
+
130
+ <script setup>
131
+ import { VuePivottableUi, PivotUtilities } from 'vue-pivottable'
132
+ import { createSubtotalRenderers } from '@vue-pivottable/subtotal-renderer'
133
+ import 'vue-pivottable/dist/vue-pivottable.css'
134
+
135
+ const SubtotalRenderers = createSubtotalRenderers(PivotUtilities.PivotData)
136
+ </script>
137
+ ```
138
+
139
+ ## Subtotal Options
140
+
141
+ You can customize the subtotal behavior using the `subtotalOptions` prop:
142
+
143
+ ```vue
144
+ <VuePivottable
145
+ :data="data"
146
+ :renderers="SubtotalRenderers"
147
+ rendererName="Subtotal Table"
148
+ :subtotalOptions="{
149
+ rowSubtotalDisplay: {
150
+ displayOnTop: false,
151
+ enabled: true,
152
+ hideOnExpand: false
153
+ },
154
+ colSubtotalDisplay: {
155
+ displayOnTop: false,
156
+ enabled: true,
157
+ hideOnExpand: false
158
+ },
159
+ arrowCollapsed: '▶',
160
+ arrowExpanded: '▼'
161
+ }"
162
+ />
163
+ ```
164
+
165
+ ### Options
166
+
167
+ | Option | Type | Default | Description |
168
+ |--------|------|---------|-------------|
169
+ | `rowSubtotalDisplay.enabled` | `boolean` | `true` | Enable row subtotals |
170
+ | `rowSubtotalDisplay.displayOnTop` | `boolean` | `false` | Display subtotals above the group |
171
+ | `rowSubtotalDisplay.hideOnExpand` | `boolean` | `false` | Hide subtotals when group is expanded |
172
+ | `colSubtotalDisplay.enabled` | `boolean` | `true` | Enable column subtotals |
173
+ | `colSubtotalDisplay.displayOnTop` | `boolean` | `false` | Display subtotals to the left of the group |
174
+ | `colSubtotalDisplay.hideOnExpand` | `boolean` | `false` | Hide subtotals when group is expanded |
175
+ | `arrowCollapsed` | `string` | `'▶'` | Arrow character for collapsed state |
176
+ | `arrowExpanded` | `string` | `'▼'` | Arrow character for expanded state |
177
+
178
+ ## Click Callback
179
+
180
+ The subtotal renderer supports the same `clickCallback` as the standard renderer:
181
+
182
+ ```vue
183
+ <VuePivottable
184
+ :data="data"
185
+ :renderers="SubtotalRenderers"
186
+ rendererName="Subtotal Table"
187
+ :tableOptions="{
188
+ clickCallback: (event, value, filters, pivotData) => {
189
+ console.log('Clicked value:', value)
190
+ console.log('Filters:', filters)
191
+ }
192
+ }"
193
+ />
194
+ ```
195
+
196
+ ## Custom Labels
197
+
198
+ Use the `labels` prop to customize how values are displayed:
199
+
200
+ ```vue
201
+ <VuePivottable
202
+ :data="data"
203
+ :renderers="SubtotalRenderers"
204
+ rendererName="Subtotal Table"
205
+ :labels="{
206
+ Category: (val) => val.toUpperCase(),
207
+ Region: (val) => `Region: ${val}`
208
+ }"
209
+ />
210
+ ```
211
+
212
+ ## Styling
213
+
214
+ The renderer adds specific CSS classes for styling:
215
+
216
+ ```css
217
+ /* Subtotal rows */
218
+ .pvtSubtotalRow {
219
+ background-color: #f5f5f5;
220
+ }
221
+
222
+ /* Subtotal label cells */
223
+ .pvtSubtotalLabel {
224
+ font-weight: bold;
225
+ background-color: #e8e8e8;
226
+ }
227
+
228
+ /* Subtotal value cells */
229
+ .pvtSubtotalVal {
230
+ font-weight: bold;
231
+ background-color: #f0f0f0;
232
+ }
233
+
234
+ /* Collapse toggle */
235
+ .pvtCollapseToggle {
236
+ cursor: pointer;
237
+ user-select: none;
238
+ }
239
+
240
+ .pvtCollapseToggle:hover {
241
+ color: #0066cc;
242
+ }
243
+ ```
244
+
245
+ ## How It Works
246
+
247
+ 1. **Hierarchical Keys**: When you have multiple row/column attributes (e.g., `['Category', 'Subcategory', 'Product']`), the renderer creates subtotal rows for each level of the hierarchy.
248
+
249
+ 2. **Subtotal Calculation**: For each subtotal row, the renderer sums up all values in the child rows. For example, the "Electronics" subtotal includes all phones and laptops.
250
+
251
+ 3. **Expand/Collapse**: Clicking the arrow icon collapses or expands the child rows. When collapsed, only the subtotal row is visible.
252
+
253
+ ## Comparison with Standard Renderer
254
+
255
+ | Feature | Standard Renderer | Subtotal Renderer |
256
+ |---------|-------------------|-------------------|
257
+ | Row/Column totals | ✅ | ✅ |
258
+ | Grand total | ✅ | ✅ |
259
+ | Subtotals | ❌ | ✅ |
260
+ | Expand/Collapse | ❌ | ✅ |
261
+ | Click callback | ✅ | ✅ |
262
+ | Labels | ✅ | ✅ |
263
+ | Heatmap | ✅ | ❌ (planned) |
264
+
265
+ ## Running Examples
266
+
267
+ ```bash
268
+ # Clone the repository
269
+ git clone https://github.com/user/vue3-pivottable.git
270
+
271
+ # Navigate to the subtotal-renderer package
272
+ cd vue3-pivottable/subtotal-renderer
273
+
274
+ # Install dependencies
275
+ npm install
276
+
277
+ # Run Vue 3 example
278
+ cd example/vue3
279
+ npm install
280
+ npm run dev
281
+
282
+ # Run Vue 2 example (in another terminal)
283
+ cd example/vue2
284
+ npm install
285
+ npm run dev
286
+ ```
287
+
288
+ ## Requirements
289
+
290
+ - Vue 2.6+ or Vue 3.0+
291
+ - vue-pivottable (matching version)
292
+
293
+ ## License
294
+
295
+ MIT
package/dist/core.js ADDED
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ function getSubtotalKeys(key) {
4
+ const result = [];
5
+ for (let i = 1; i <= key.length; i++) {
6
+ result.push(key.slice(0, i));
7
+ }
8
+ return result;
9
+ }
10
+ function isSubtotalKey(key, maxDepth) {
11
+ return key.length < maxDepth;
12
+ }
13
+ function generateRowKeysWithSubtotals(rowKeys, rowAttrsCount, collapsedKeys = /* @__PURE__ */ new Set()) {
14
+ if (rowAttrsCount <= 1) {
15
+ return rowKeys;
16
+ }
17
+ const result = [];
18
+ const addedSubtotals = /* @__PURE__ */ new Set();
19
+ for (const rowKey of rowKeys) {
20
+ for (let level = 1; level < rowKey.length; level++) {
21
+ const subtotalKey = rowKey.slice(0, level);
22
+ const subtotalKeyStr = JSON.stringify(subtotalKey);
23
+ if (!addedSubtotals.has(subtotalKeyStr)) {
24
+ addedSubtotals.add(subtotalKeyStr);
25
+ const isCollapsed = collapsedKeys.has(subtotalKeyStr);
26
+ result.push({
27
+ key: subtotalKey,
28
+ isSubtotal: true,
29
+ level,
30
+ isCollapsed
31
+ });
32
+ }
33
+ }
34
+ let isHidden = false;
35
+ for (let level = 1; level < rowKey.length; level++) {
36
+ const parentKey = rowKey.slice(0, level);
37
+ if (collapsedKeys.has(JSON.stringify(parentKey))) {
38
+ isHidden = true;
39
+ break;
40
+ }
41
+ }
42
+ if (!isHidden) {
43
+ result.push({
44
+ key: rowKey,
45
+ isSubtotal: false,
46
+ level: rowKey.length,
47
+ isCollapsed: false
48
+ });
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+ function generateColKeysWithSubtotals(colKeys, colAttrsCount, collapsedKeys = /* @__PURE__ */ new Set()) {
54
+ if (colAttrsCount <= 1) {
55
+ return colKeys;
56
+ }
57
+ const result = [];
58
+ const grouped = /* @__PURE__ */ new Map();
59
+ for (const colKey of colKeys) {
60
+ for (let level = 1; level <= colKey.length; level++) {
61
+ const partialKey = colKey.slice(0, level);
62
+ const keyStr = JSON.stringify(partialKey);
63
+ if (!grouped.has(keyStr)) {
64
+ grouped.set(keyStr, {
65
+ key: partialKey,
66
+ isSubtotal: level < colKey.length,
67
+ level
68
+ });
69
+ }
70
+ }
71
+ }
72
+ const sortedKeys = Array.from(grouped.values()).sort((a, b) => {
73
+ for (let i = 0; i < Math.min(a.key.length, b.key.length); i++) {
74
+ if (a.key[i] < b.key[i])
75
+ return -1;
76
+ if (a.key[i] > b.key[i])
77
+ return 1;
78
+ }
79
+ return a.key.length - b.key.length;
80
+ });
81
+ for (const item of sortedKeys) {
82
+ const keyStr = JSON.stringify(item.key);
83
+ const isCollapsed = collapsedKeys.has(keyStr);
84
+ let isHidden = false;
85
+ for (let level = 1; level < item.key.length; level++) {
86
+ const parentKey = item.key.slice(0, level);
87
+ if (collapsedKeys.has(JSON.stringify(parentKey))) {
88
+ isHidden = true;
89
+ break;
90
+ }
91
+ }
92
+ if (!isHidden) {
93
+ result.push({
94
+ key: item.key,
95
+ isSubtotal: item.isSubtotal,
96
+ level: item.level,
97
+ isCollapsed
98
+ });
99
+ }
100
+ }
101
+ return result;
102
+ }
103
+ function createSubtotalAggregatorGetter(pivotData) {
104
+ const subtotalCache = /* @__PURE__ */ new Map();
105
+ return function getAggregator(rowKey, colKey) {
106
+ const isFullRowKey = rowKey.length === pivotData.props.rows.length;
107
+ const isFullColKey = colKey.length === pivotData.props.cols.length;
108
+ if (isFullRowKey && isFullColKey) {
109
+ return pivotData.getAggregator(rowKey, colKey);
110
+ }
111
+ const cacheKey = JSON.stringify({ row: rowKey, col: colKey });
112
+ if (subtotalCache.has(cacheKey)) {
113
+ return subtotalCache.get(cacheKey);
114
+ }
115
+ const matchingRowKeys = pivotData.getRowKeys().filter((rk) => {
116
+ if (rowKey.length === 0)
117
+ return true;
118
+ for (let i = 0; i < rowKey.length; i++) {
119
+ if (rk[i] !== rowKey[i])
120
+ return false;
121
+ }
122
+ return true;
123
+ });
124
+ const matchingColKeys = pivotData.getColKeys().filter((ck) => {
125
+ if (colKey.length === 0)
126
+ return true;
127
+ for (let i = 0; i < colKey.length; i++) {
128
+ if (ck[i] !== colKey[i])
129
+ return false;
130
+ }
131
+ return true;
132
+ });
133
+ let totalValue = 0;
134
+ let count = 0;
135
+ for (const rk of matchingRowKeys) {
136
+ for (const ck of matchingColKeys) {
137
+ const agg = pivotData.getAggregator(rk, ck);
138
+ const val = agg.value();
139
+ if (val !== null && val !== void 0 && !isNaN(val)) {
140
+ totalValue += val;
141
+ count++;
142
+ }
143
+ }
144
+ }
145
+ const subtotalAgg = {
146
+ value: () => count > 0 ? totalValue : null,
147
+ format: pivotData.getAggregator([], []).format || ((v) => v)
148
+ };
149
+ subtotalCache.set(cacheKey, subtotalAgg);
150
+ return subtotalAgg;
151
+ };
152
+ }
153
+ exports.createSubtotalAggregatorGetter = createSubtotalAggregatorGetter;
154
+ exports.generateColKeysWithSubtotals = generateColKeysWithSubtotals;
155
+ exports.generateRowKeysWithSubtotals = generateRowKeysWithSubtotals;
156
+ exports.getSubtotalKeys = getSubtotalKeys;
157
+ exports.isSubtotalKey = isSubtotalKey;
package/dist/core.mjs ADDED
@@ -0,0 +1,157 @@
1
+ function getSubtotalKeys(key) {
2
+ const result = [];
3
+ for (let i = 1; i <= key.length; i++) {
4
+ result.push(key.slice(0, i));
5
+ }
6
+ return result;
7
+ }
8
+ function isSubtotalKey(key, maxDepth) {
9
+ return key.length < maxDepth;
10
+ }
11
+ function generateRowKeysWithSubtotals(rowKeys, rowAttrsCount, collapsedKeys = /* @__PURE__ */ new Set()) {
12
+ if (rowAttrsCount <= 1) {
13
+ return rowKeys;
14
+ }
15
+ const result = [];
16
+ const addedSubtotals = /* @__PURE__ */ new Set();
17
+ for (const rowKey of rowKeys) {
18
+ for (let level = 1; level < rowKey.length; level++) {
19
+ const subtotalKey = rowKey.slice(0, level);
20
+ const subtotalKeyStr = JSON.stringify(subtotalKey);
21
+ if (!addedSubtotals.has(subtotalKeyStr)) {
22
+ addedSubtotals.add(subtotalKeyStr);
23
+ const isCollapsed = collapsedKeys.has(subtotalKeyStr);
24
+ result.push({
25
+ key: subtotalKey,
26
+ isSubtotal: true,
27
+ level,
28
+ isCollapsed
29
+ });
30
+ }
31
+ }
32
+ let isHidden = false;
33
+ for (let level = 1; level < rowKey.length; level++) {
34
+ const parentKey = rowKey.slice(0, level);
35
+ if (collapsedKeys.has(JSON.stringify(parentKey))) {
36
+ isHidden = true;
37
+ break;
38
+ }
39
+ }
40
+ if (!isHidden) {
41
+ result.push({
42
+ key: rowKey,
43
+ isSubtotal: false,
44
+ level: rowKey.length,
45
+ isCollapsed: false
46
+ });
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+ function generateColKeysWithSubtotals(colKeys, colAttrsCount, collapsedKeys = /* @__PURE__ */ new Set()) {
52
+ if (colAttrsCount <= 1) {
53
+ return colKeys;
54
+ }
55
+ const result = [];
56
+ const grouped = /* @__PURE__ */ new Map();
57
+ for (const colKey of colKeys) {
58
+ for (let level = 1; level <= colKey.length; level++) {
59
+ const partialKey = colKey.slice(0, level);
60
+ const keyStr = JSON.stringify(partialKey);
61
+ if (!grouped.has(keyStr)) {
62
+ grouped.set(keyStr, {
63
+ key: partialKey,
64
+ isSubtotal: level < colKey.length,
65
+ level
66
+ });
67
+ }
68
+ }
69
+ }
70
+ const sortedKeys = Array.from(grouped.values()).sort((a, b) => {
71
+ for (let i = 0; i < Math.min(a.key.length, b.key.length); i++) {
72
+ if (a.key[i] < b.key[i])
73
+ return -1;
74
+ if (a.key[i] > b.key[i])
75
+ return 1;
76
+ }
77
+ return a.key.length - b.key.length;
78
+ });
79
+ for (const item of sortedKeys) {
80
+ const keyStr = JSON.stringify(item.key);
81
+ const isCollapsed = collapsedKeys.has(keyStr);
82
+ let isHidden = false;
83
+ for (let level = 1; level < item.key.length; level++) {
84
+ const parentKey = item.key.slice(0, level);
85
+ if (collapsedKeys.has(JSON.stringify(parentKey))) {
86
+ isHidden = true;
87
+ break;
88
+ }
89
+ }
90
+ if (!isHidden) {
91
+ result.push({
92
+ key: item.key,
93
+ isSubtotal: item.isSubtotal,
94
+ level: item.level,
95
+ isCollapsed
96
+ });
97
+ }
98
+ }
99
+ return result;
100
+ }
101
+ function createSubtotalAggregatorGetter(pivotData) {
102
+ const subtotalCache = /* @__PURE__ */ new Map();
103
+ return function getAggregator(rowKey, colKey) {
104
+ const isFullRowKey = rowKey.length === pivotData.props.rows.length;
105
+ const isFullColKey = colKey.length === pivotData.props.cols.length;
106
+ if (isFullRowKey && isFullColKey) {
107
+ return pivotData.getAggregator(rowKey, colKey);
108
+ }
109
+ const cacheKey = JSON.stringify({ row: rowKey, col: colKey });
110
+ if (subtotalCache.has(cacheKey)) {
111
+ return subtotalCache.get(cacheKey);
112
+ }
113
+ const matchingRowKeys = pivotData.getRowKeys().filter((rk) => {
114
+ if (rowKey.length === 0)
115
+ return true;
116
+ for (let i = 0; i < rowKey.length; i++) {
117
+ if (rk[i] !== rowKey[i])
118
+ return false;
119
+ }
120
+ return true;
121
+ });
122
+ const matchingColKeys = pivotData.getColKeys().filter((ck) => {
123
+ if (colKey.length === 0)
124
+ return true;
125
+ for (let i = 0; i < colKey.length; i++) {
126
+ if (ck[i] !== colKey[i])
127
+ return false;
128
+ }
129
+ return true;
130
+ });
131
+ let totalValue = 0;
132
+ let count = 0;
133
+ for (const rk of matchingRowKeys) {
134
+ for (const ck of matchingColKeys) {
135
+ const agg = pivotData.getAggregator(rk, ck);
136
+ const val = agg.value();
137
+ if (val !== null && val !== void 0 && !isNaN(val)) {
138
+ totalValue += val;
139
+ count++;
140
+ }
141
+ }
142
+ }
143
+ const subtotalAgg = {
144
+ value: () => count > 0 ? totalValue : null,
145
+ format: pivotData.getAggregator([], []).format || ((v) => v)
146
+ };
147
+ subtotalCache.set(cacheKey, subtotalAgg);
148
+ return subtotalAgg;
149
+ };
150
+ }
151
+ export {
152
+ createSubtotalAggregatorGetter,
153
+ generateColKeysWithSubtotals,
154
+ generateRowKeysWithSubtotals,
155
+ getSubtotalKeys,
156
+ isSubtotalKey
157
+ };