@vue-pivottable/multi-value-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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Seungwoo321
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # @vue-pivottable/multi-value-renderer
2
+
3
+ Multi-value aggregator renderer for vue-pivottable. Display multiple aggregated values per cell, each with its own aggregation function.
4
+
5
+ ## Features
6
+
7
+ - **Multiple Values per Cell**: Display sales (Sum), quantity (Average), and more in a single pivot table cell
8
+ - **Vue 2 & Vue 3 Support**: Works with both vue-pivottable (Vue 2) and vue3-pivottable (Vue 3)
9
+ - **Flexible Layout**: Choose between vertical, horizontal, or compact cell layouts
10
+ - **Custom Value Labels**: Display user-friendly labels for each value column
11
+ - **Full Integration**: Works with existing vue-pivottable features like click callbacks, labels, and totals
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @vue-pivottable/multi-value-renderer
17
+
18
+ # or
19
+ pnpm add @vue-pivottable/multi-value-renderer
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### Vue 3
25
+
26
+ ```vue
27
+ <template>
28
+ <VuePivottable
29
+ :data="data"
30
+ :rows="['region']"
31
+ :cols="['product']"
32
+ :vals="['sales', 'quantity']"
33
+ :renderers="renderers"
34
+ renderer-name="Multi-Value Table"
35
+ :aggregator-map="aggregatorMap"
36
+ />
37
+ </template>
38
+
39
+ <script setup>
40
+ import { VuePivottable, PivotUtilities } from 'vue-pivottable'
41
+ import { MultiValueRenderers } from '@vue-pivottable/multi-value-renderer'
42
+
43
+ const data = [
44
+ { region: 'East', product: 'Apple', sales: 100, quantity: 10 },
45
+ { region: 'East', product: 'Banana', sales: 80, quantity: 20 },
46
+ // ...
47
+ ]
48
+
49
+ const renderers = {
50
+ ...MultiValueRenderers
51
+ }
52
+
53
+ // Different aggregation for each value
54
+ const aggregatorMap = {
55
+ sales: 'Sum',
56
+ quantity: 'Average'
57
+ }
58
+ </script>
59
+ ```
60
+
61
+ ### Vue 2
62
+
63
+ ```vue
64
+ <template>
65
+ <VuePivottable
66
+ :data="data"
67
+ :rows="['region']"
68
+ :cols="['product']"
69
+ :vals="['sales', 'quantity']"
70
+ :renderers="renderers"
71
+ renderer-name="Multi-Value Table"
72
+ :aggregator-map="aggregatorMap"
73
+ />
74
+ </template>
75
+
76
+ <script>
77
+ import { VuePivottable } from 'vue-pivottable'
78
+ import { MultiValueRenderers } from '@vue-pivottable/multi-value-renderer/vue2'
79
+
80
+ export default {
81
+ components: { VuePivottable },
82
+ data() {
83
+ return {
84
+ data: [
85
+ { region: 'East', product: 'Apple', sales: 100, quantity: 10 },
86
+ // ...
87
+ ],
88
+ renderers: { ...MultiValueRenderers },
89
+ aggregatorMap: {
90
+ sales: 'Sum',
91
+ quantity: 'Average'
92
+ }
93
+ }
94
+ }
95
+ }
96
+ </script>
97
+ ```
98
+
99
+ ## Props
100
+
101
+ ### aggregatorMap
102
+
103
+ Map of value column names to aggregator names.
104
+
105
+ ```js
106
+ {
107
+ sales: 'Sum',
108
+ quantity: 'Average',
109
+ profit: 'Maximum'
110
+ }
111
+ ```
112
+
113
+ ### cellLayout
114
+
115
+ How to display multiple values in each cell:
116
+
117
+ - `'vertical'` (default): Stack values vertically
118
+ - `'horizontal'`: Display values side by side
119
+ - `'compact'`: Show values separated by " / "
120
+
121
+ ### showValueLabels
122
+
123
+ Whether to show labels before each value (default: `true`).
124
+
125
+ ### valueLabels
126
+
127
+ Custom display labels for value columns:
128
+
129
+ ```js
130
+ {
131
+ sales: 'Total Sales',
132
+ quantity: 'Avg Qty'
133
+ }
134
+ ```
135
+
136
+ ## Styling
137
+
138
+ Import the included CSS for default styling:
139
+
140
+ ```js
141
+ import '@vue-pivottable/multi-value-renderer/dist/styles.css'
142
+ ```
143
+
144
+ Or customize with your own CSS targeting these classes:
145
+
146
+ - `.multi-value-cell` - Cell container
147
+ - `.multi-value-item` - Individual value row
148
+ - `.multi-value-label` - Value label (e.g., "Sales:")
149
+ - `.multi-value-value` - The actual value
150
+ - `.layout-vertical`, `.layout-horizontal` - Layout modifiers
151
+
152
+ ## Available Aggregators
153
+
154
+ The renderer uses aggregators from vue-pivottable. Common options:
155
+
156
+ - `Count`
157
+ - `Sum`
158
+ - `Average`
159
+ - `Minimum`
160
+ - `Maximum`
161
+ - `Median`
162
+ - `Sample Variance`
163
+ - `Sample Standard Deviation`
164
+
165
+ ## License
166
+
167
+ MIT
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ function createMultiValueAggregator(aggregatorMap, aggregators, vals) {
3
+ return function multiValueAggregatorFactory(data, rowKey, colKey) {
4
+ const subAggregators = {};
5
+ vals.forEach((val) => {
6
+ const aggName = aggregatorMap[val] || "Sum";
7
+ const aggFactory = aggregators[aggName];
8
+ if (aggFactory) {
9
+ subAggregators[val] = aggFactory([val])(data, rowKey, colKey);
10
+ }
11
+ });
12
+ return {
13
+ push(record) {
14
+ vals.forEach((val) => {
15
+ if (subAggregators[val]) {
16
+ subAggregators[val].push(record);
17
+ }
18
+ });
19
+ },
20
+ value() {
21
+ const results = {};
22
+ vals.forEach((val) => {
23
+ if (subAggregators[val]) {
24
+ results[val] = subAggregators[val].value();
25
+ }
26
+ });
27
+ return results;
28
+ },
29
+ valueOf(valName) {
30
+ if (subAggregators[valName]) {
31
+ return subAggregators[valName].value();
32
+ }
33
+ return null;
34
+ },
35
+ format(values) {
36
+ if (typeof values !== "object") {
37
+ return String(values ?? "");
38
+ }
39
+ return vals.map((val) => {
40
+ const v = values[val];
41
+ const subAgg = subAggregators[val];
42
+ return subAgg && subAgg.format ? subAgg.format(v) : String(v ?? "");
43
+ }).join(" / ");
44
+ },
45
+ formatOf(valName, value) {
46
+ if (subAggregators[valName] && subAggregators[valName].format) {
47
+ return subAggregators[valName].format(value);
48
+ }
49
+ return String(value ?? "");
50
+ },
51
+ getSubAggregator(valName) {
52
+ return subAggregators[valName] || null;
53
+ },
54
+ numInputs: vals.length
55
+ };
56
+ };
57
+ }
58
+ function extendPivotData(BasePivotData) {
59
+ return class MultiValuePivotData extends BasePivotData {
60
+ constructor(inputProps = {}) {
61
+ const { aggregatorMap = {}, ...restProps } = inputProps;
62
+ const multiValueAgg = createMultiValueAggregator(
63
+ aggregatorMap,
64
+ restProps.aggregators || BasePivotData.defaultProps.aggregators,
65
+ restProps.vals || []
66
+ );
67
+ const modifiedProps = {
68
+ ...restProps,
69
+ aggregators: {
70
+ ...restProps.aggregators,
71
+ "Multi-Value": () => multiValueAgg
72
+ },
73
+ aggregatorName: "Multi-Value"
74
+ };
75
+ super(modifiedProps);
76
+ this.aggregatorMap = aggregatorMap;
77
+ }
78
+ };
79
+ }
80
+ exports.createMultiValueAggregator = createMultiValueAggregator;
81
+ exports.extendPivotData = extendPivotData;
@@ -0,0 +1,364 @@
1
+ "use strict";
2
+ const vue = require("vue");
3
+ const vuePivottable = require("vue-pivottable");
4
+ const MultiValuePivotData = require("./MultiValuePivotData.js");
5
+ const { PivotData } = vuePivottable.PivotUtilities;
6
+ function redColorScaleGenerator(values) {
7
+ const numericValues = values.filter((v) => typeof v === "number" && !isNaN(v));
8
+ if (numericValues.length === 0) {
9
+ return () => ({});
10
+ }
11
+ const min = Math.min(...numericValues);
12
+ const max = Math.max(...numericValues);
13
+ return (x) => {
14
+ if (typeof x !== "number" || isNaN(x) || max === min) {
15
+ return {};
16
+ }
17
+ const nonRed = 255 - Math.round(255 * (x - min) / (max - min));
18
+ return { backgroundColor: `rgb(255,${nonRed},${nonRed})` };
19
+ };
20
+ }
21
+ function makeMultiValueRenderer(opts = {}) {
22
+ return vue.defineComponent({
23
+ name: opts.name || "vue3-multi-value-table",
24
+ props: {
25
+ // Data props
26
+ data: {
27
+ type: [Array, Object, Function],
28
+ required: true
29
+ },
30
+ rows: {
31
+ type: Array,
32
+ default: () => []
33
+ },
34
+ cols: {
35
+ type: Array,
36
+ default: () => []
37
+ },
38
+ vals: {
39
+ type: Array,
40
+ default: () => []
41
+ },
42
+ // Multi-value specific props
43
+ aggregatorMap: {
44
+ type: Object,
45
+ default: () => ({})
46
+ },
47
+ aggregators: {
48
+ type: Object,
49
+ required: true
50
+ },
51
+ // Filter and sort props
52
+ valueFilter: {
53
+ type: Object,
54
+ default: () => ({})
55
+ },
56
+ sorters: {
57
+ type: [Object, Function],
58
+ default: () => ({})
59
+ },
60
+ derivedAttributes: {
61
+ type: [Object, Function],
62
+ default: () => ({})
63
+ },
64
+ rowOrder: {
65
+ type: String,
66
+ default: "key_a_to_z"
67
+ },
68
+ colOrder: {
69
+ type: String,
70
+ default: "key_a_to_z"
71
+ },
72
+ // Display props
73
+ rowTotal: {
74
+ type: Boolean,
75
+ default: true
76
+ },
77
+ colTotal: {
78
+ type: Boolean,
79
+ default: true
80
+ },
81
+ tableColorScaleGenerator: {
82
+ type: Function,
83
+ default: () => redColorScaleGenerator
84
+ },
85
+ tableOptions: {
86
+ type: Object,
87
+ default: () => ({})
88
+ },
89
+ localeStrings: {
90
+ type: Object,
91
+ default: () => ({ totals: "Totals" })
92
+ },
93
+ labels: {
94
+ type: Object,
95
+ default: () => ({})
96
+ },
97
+ // Multi-value display options
98
+ cellLayout: {
99
+ type: String,
100
+ default: "vertical",
101
+ validator: (v) => ["vertical", "horizontal", "compact"].includes(v)
102
+ },
103
+ showValueLabels: {
104
+ type: Boolean,
105
+ default: true
106
+ },
107
+ valueLabels: {
108
+ type: Object,
109
+ default: () => ({})
110
+ }
111
+ },
112
+ setup(props) {
113
+ const applyLabel = (attr, value) => {
114
+ if (props.labels && typeof props.labels[attr] === "function") {
115
+ return props.labels[attr](value);
116
+ }
117
+ return value;
118
+ };
119
+ const getValueLabel = (valName) => {
120
+ if (props.valueLabels && props.valueLabels[valName]) {
121
+ return props.valueLabels[valName];
122
+ }
123
+ return valName;
124
+ };
125
+ const spanSize = (arr, i, j) => {
126
+ let x;
127
+ if (i !== 0) {
128
+ let noDraw = true;
129
+ for (x = 0; x <= j; x++) {
130
+ if (arr[i - 1][x] !== arr[i][x]) {
131
+ noDraw = false;
132
+ }
133
+ }
134
+ if (noDraw) return -1;
135
+ }
136
+ let len = 0;
137
+ while (i + len < arr.length) {
138
+ let stop = false;
139
+ for (x = 0; x <= j; x++) {
140
+ if (arr[i][x] !== arr[i + len][x]) {
141
+ stop = true;
142
+ }
143
+ }
144
+ if (stop) break;
145
+ len++;
146
+ }
147
+ return len;
148
+ };
149
+ const formatValue = (valName, value) => {
150
+ const aggName = props.aggregatorMap[valName] || "Sum";
151
+ const agg = props.aggregators[aggName];
152
+ if (agg) {
153
+ try {
154
+ const instance = agg([valName])();
155
+ if (instance && instance.format) {
156
+ return instance.format(value);
157
+ }
158
+ } catch (e) {
159
+ }
160
+ }
161
+ if (value === null || value === void 0) return "";
162
+ if (typeof value === "number") {
163
+ return value.toLocaleString();
164
+ }
165
+ return String(value);
166
+ };
167
+ const renderMultiValueCell = (values) => {
168
+ if (!values || typeof values !== "object") {
169
+ return String(values || "");
170
+ }
171
+ const items = props.vals.map((val) => {
172
+ const value = values[val];
173
+ const formatted = formatValue(val, value);
174
+ const label = getValueLabel(val);
175
+ const aggName = props.aggregatorMap[val] || "Sum";
176
+ if (props.cellLayout === "compact") {
177
+ return formatted;
178
+ }
179
+ return vue.h("div", {
180
+ class: "multi-value-item",
181
+ key: val,
182
+ "data-value": val,
183
+ "data-aggregator": aggName
184
+ }, [
185
+ props.showValueLabels ? vue.h("span", { class: "multi-value-label" }, `${label}: `) : null,
186
+ vue.h("span", { class: "multi-value-value" }, formatted)
187
+ ]);
188
+ });
189
+ if (props.cellLayout === "compact") {
190
+ return items.join(" / ");
191
+ }
192
+ return vue.h("div", {
193
+ class: ["multi-value-cell", `layout-${props.cellLayout}`]
194
+ }, items);
195
+ };
196
+ const createPivotData = () => {
197
+ const multiValueAgg = MultiValuePivotData.createMultiValueAggregator(
198
+ props.aggregatorMap,
199
+ props.aggregators,
200
+ props.vals
201
+ );
202
+ const modifiedAggregators = {
203
+ ...props.aggregators,
204
+ "Multi-Value": () => multiValueAgg
205
+ };
206
+ return new PivotData({
207
+ data: props.data,
208
+ rows: props.rows,
209
+ cols: props.cols,
210
+ vals: props.vals,
211
+ aggregators: modifiedAggregators,
212
+ aggregatorName: "Multi-Value",
213
+ valueFilter: props.valueFilter,
214
+ sorters: props.sorters,
215
+ derivedAttributes: props.derivedAttributes,
216
+ rowOrder: props.rowOrder,
217
+ colOrder: props.colOrder
218
+ });
219
+ };
220
+ return {
221
+ applyLabel,
222
+ getValueLabel,
223
+ spanSize,
224
+ formatValue,
225
+ renderMultiValueCell,
226
+ createPivotData
227
+ };
228
+ },
229
+ render() {
230
+ let pivotData;
231
+ try {
232
+ pivotData = this.createPivotData();
233
+ } catch (error) {
234
+ console.error("Multi-Value Renderer Error:", error);
235
+ return vue.h("div", { class: "pvtError" }, `Error: ${error.message}`);
236
+ }
237
+ const colAttrs = pivotData.props.cols;
238
+ const rowAttrs = pivotData.props.rows;
239
+ const rowKeys = pivotData.getRowKeys();
240
+ const colKeys = pivotData.getColKeys();
241
+ const grandTotalAggregator = pivotData.getAggregator([], []);
242
+ const getClickHandler = (value, rowValues, colValues) => {
243
+ var _a;
244
+ if ((_a = this.tableOptions) == null ? void 0 : _a.clickCallback) {
245
+ const filters = {};
246
+ colAttrs.forEach((attr, i) => {
247
+ if (colValues[i] !== null) {
248
+ filters[attr] = colValues[i];
249
+ }
250
+ });
251
+ rowAttrs.forEach((attr, i) => {
252
+ if (rowValues[i] !== null) {
253
+ filters[attr] = rowValues[i];
254
+ }
255
+ });
256
+ return (e) => this.tableOptions.clickCallback(e, value, filters, pivotData);
257
+ }
258
+ return null;
259
+ };
260
+ return vue.h("table", { class: ["pvtTable", "pvtMultiValueTable"] }, [
261
+ // THEAD
262
+ vue.h("thead", [
263
+ // Column attribute headers
264
+ ...colAttrs.map((c, j) => {
265
+ return vue.h("tr", { key: `colAttrs${j}` }, [
266
+ // Top-left corner cell
267
+ j === 0 && rowAttrs.length !== 0 ? vue.h("th", { colSpan: rowAttrs.length, rowSpan: colAttrs.length }) : void 0,
268
+ // Column attribute label
269
+ vue.h("th", { class: "pvtAxisLabel" }, c),
270
+ // Column keys
271
+ ...colKeys.map((colKey, i) => {
272
+ const x = this.spanSize(colKeys, i, j);
273
+ if (x === -1) return null;
274
+ return vue.h("th", {
275
+ class: "pvtColLabel",
276
+ key: `colKey${i}`,
277
+ colSpan: x,
278
+ rowSpan: j === colAttrs.length - 1 && rowAttrs.length !== 0 ? 2 : 1
279
+ }, this.applyLabel(colAttrs[j], colKey[j]));
280
+ }),
281
+ // Totals header
282
+ j === 0 && this.rowTotal ? vue.h("th", {
283
+ class: "pvtTotalLabel",
284
+ rowSpan: colAttrs.length + (rowAttrs.length === 0 ? 0 : 1)
285
+ }, this.localeStrings.totals) : void 0
286
+ ].filter(Boolean));
287
+ }),
288
+ // Row attribute labels row
289
+ rowAttrs.length !== 0 ? vue.h("tr", [
290
+ ...rowAttrs.map((r, i) => {
291
+ return vue.h("th", { class: "pvtAxisLabel", key: `rowAttr${i}` }, r);
292
+ }),
293
+ this.rowTotal ? vue.h(
294
+ "th",
295
+ { class: "pvtTotalLabel" },
296
+ colAttrs.length === 0 ? this.localeStrings.totals : null
297
+ ) : colAttrs.length === 0 ? void 0 : vue.h("th")
298
+ ].filter(Boolean)) : void 0
299
+ ].filter(Boolean)),
300
+ // TBODY
301
+ vue.h("tbody", [
302
+ // Data rows
303
+ ...rowKeys.map((rowKey, i) => {
304
+ const totalAggregator = pivotData.getAggregator(rowKey, []);
305
+ return vue.h("tr", { key: `rowKeyRow${i}` }, [
306
+ // Row labels
307
+ ...rowKey.map((text, j) => {
308
+ const x = this.spanSize(rowKeys, i, j);
309
+ if (x === -1) return null;
310
+ return vue.h("th", {
311
+ class: "pvtRowLabel",
312
+ key: `rowKeyLabel${i}-${j}`,
313
+ rowSpan: x,
314
+ colSpan: j === rowAttrs.length - 1 && colAttrs.length !== 0 ? 2 : 1
315
+ }, this.applyLabel(rowAttrs[j], text));
316
+ }),
317
+ // Data cells
318
+ ...colKeys.map((colKey, j) => {
319
+ const aggregator = pivotData.getAggregator(rowKey, colKey);
320
+ const value = aggregator.value();
321
+ const clickHandler = getClickHandler(value, rowKey, colKey);
322
+ return vue.h("td", {
323
+ class: ["pvVal", "pvtMultiVal"],
324
+ key: `pvtVal${i}-${j}`,
325
+ onClick: clickHandler
326
+ }, [this.renderMultiValueCell(value)]);
327
+ }),
328
+ // Row total
329
+ this.rowTotal ? vue.h("td", {
330
+ class: ["pvtTotal", "pvtMultiVal"],
331
+ onClick: getClickHandler(totalAggregator.value(), rowKey, [])
332
+ }, [this.renderMultiValueCell(totalAggregator.value())]) : void 0
333
+ ].filter(Boolean));
334
+ }),
335
+ // Column totals row
336
+ vue.h("tr", [
337
+ this.colTotal ? vue.h("th", {
338
+ class: "pvtTotalLabel",
339
+ colSpan: rowAttrs.length + (colAttrs.length === 0 ? 0 : 1)
340
+ }, this.localeStrings.totals) : void 0,
341
+ ...this.colTotal ? colKeys.map((colKey, i) => {
342
+ const totalAggregator = pivotData.getAggregator([], colKey);
343
+ const clickHandler = getClickHandler(totalAggregator.value(), [], colKey);
344
+ return vue.h("td", {
345
+ class: ["pvtTotal", "pvtMultiVal"],
346
+ key: `total${i}`,
347
+ onClick: clickHandler
348
+ }, [this.renderMultiValueCell(totalAggregator.value())]);
349
+ }) : [],
350
+ this.colTotal && this.rowTotal ? vue.h("td", {
351
+ class: ["pvtGrandTotal", "pvtMultiVal"],
352
+ onClick: getClickHandler(grandTotalAggregator.value(), [], [])
353
+ }, [this.renderMultiValueCell(grandTotalAggregator.value())]) : void 0
354
+ ].filter(Boolean))
355
+ ])
356
+ ]);
357
+ }
358
+ });
359
+ }
360
+ const MultiValueTableRenderer = vue.markRaw({
361
+ "Multi-Value Table": makeMultiValueRenderer({ name: "vue3-multi-value-table" })
362
+ });
363
+ exports.MultiValueTableRenderer = MultiValueTableRenderer;
364
+ exports.makeMultiValueRenderer = makeMultiValueRenderer;