@dou-ui/pivot 0.0.0-stage → 0.1.0-alpha.1
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 +21 -0
- package/README.md +10 -2
- package/dist/assets/default-PXde2wzm.js.map +1 -0
- package/dist/core/format.js +116 -0
- package/dist/core/format.js.map +1 -0
- package/dist/core/labels.js +28 -0
- package/dist/core/labels.js.map +1 -0
- package/dist/core/layout.js +190 -0
- package/dist/core/layout.js.map +1 -0
- package/dist/core/source.js +46 -0
- package/dist/core/source.js.map +1 -0
- package/dist/core/table.js +154 -0
- package/dist/core/table.js.map +1 -0
- package/dist/engine/aggregate.js +51 -0
- package/dist/engine/aggregate.js.map +1 -0
- package/dist/engine/compute.js +735 -0
- package/dist/engine/compute.js.map +1 -0
- package/dist/engine/engine.js +46 -0
- package/dist/engine/engine.js.map +1 -0
- package/dist/engine/formula.js +225 -0
- package/dist/engine/formula.js.map +1 -0
- package/dist/engine/group.js +107 -0
- package/dist/engine/group.js.map +1 -0
- package/dist/engine/hash.js +108 -0
- package/dist/engine/hash.js.map +1 -0
- package/dist/engine/index.js +8 -0
- package/dist/engine/show.js +132 -0
- package/dist/engine/show.js.map +1 -0
- package/dist/engine/source.js +339 -0
- package/dist/engine/source.js.map +1 -0
- package/dist/engine/tree.js +226 -0
- package/dist/engine/tree.js.map +1 -0
- package/dist/fields/fields.js +6 -0
- package/dist/fields/fields.js.map +1 -0
- package/dist/fields/fields.vue_vue_type_script_setup_true_lang.js +881 -0
- package/dist/fields/fields.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/fields/filter-list.js +6 -0
- package/dist/fields/filter-list.js.map +1 -0
- package/dist/fields/filter-list.vue_vue_type_script_setup_true_lang.js +184 -0
- package/dist/fields/filter-list.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/fields/formula-dialog.js +6 -0
- package/dist/fields/formula-dialog.js.map +1 -0
- package/dist/fields/formula-dialog.vue_vue_type_script_setup_true_lang.js +165 -0
- package/dist/fields/formula-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/fields/model.js +200 -0
- package/dist/fields/model.js.map +1 -0
- package/dist/fields/top-dialog.js +6 -0
- package/dist/fields/top-dialog.js.map +1 -0
- package/dist/fields/top-dialog.vue_vue_type_script_setup_true_lang.js +174 -0
- package/dist/fields/top-dialog.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/locale.js +508 -0
- package/dist/locale.js.map +1 -0
- package/dist/pivot.js +6 -0
- package/dist/pivot.js.map +1 -0
- package/dist/pivot.vue_vue_type_script_setup_true_lang.js +1711 -0
- package/dist/pivot.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/style.d.ts +1 -0
- package/dist/style.js +13 -0
- package/dist/types/core/format.d.ts +26 -0
- package/dist/types/core/labels.d.ts +14 -0
- package/dist/types/core/layout.d.ts +72 -0
- package/dist/types/core/source.d.ts +7 -0
- package/dist/types/core/table.d.ts +73 -0
- package/dist/types/engine/aggregate.d.ts +35 -0
- package/dist/types/engine/compute.d.ts +173 -0
- package/dist/types/engine/engine.d.ts +29 -0
- package/dist/types/engine/formula.d.ts +44 -0
- package/dist/types/engine/group.d.ts +34 -0
- package/dist/types/engine/hash.d.ts +36 -0
- package/dist/types/engine/index.d.ts +15 -0
- package/dist/types/engine/show.d.ts +27 -0
- package/dist/types/engine/source.d.ts +108 -0
- package/dist/types/engine/tree.d.ts +87 -0
- package/dist/types/engine/types.d.ts +148 -0
- package/dist/types/fields/fields.vue.d.ts +26 -0
- package/dist/types/fields/filter-list.vue.d.ts +20 -0
- package/dist/types/fields/formula-dialog.vue.d.ts +30 -0
- package/dist/types/fields/model.d.ts +52 -0
- package/dist/types/fields/top-dialog.vue.d.ts +25 -0
- package/dist/types/index.d.ts +458 -0
- package/dist/types/locale.d.ts +184 -0
- package/dist/types/pivot.vue.d.ts +29 -0
- package/dist/types/types.d.ts +199 -0
- package/dist/types/worker/client.d.ts +19 -0
- package/dist/types/worker/default.d.ts +1 -0
- package/dist/types/worker/index.d.ts +5 -0
- package/dist/types/worker/protocol.d.ts +60 -0
- package/dist/types/worker/serve.d.ts +22 -0
- package/dist/worker/client.js +191 -0
- package/dist/worker/client.js.map +1 -0
- package/dist/worker/default.js +20 -0
- package/dist/worker/default.js.map +1 -0
- package/dist/worker/index.js +3 -0
- package/dist/worker/serve.js +101 -0
- package/dist/worker/serve.js.map +1 -0
- package/package.json +64 -4
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { IntIndex, typed } from "./hash.js";
|
|
2
|
+
import { finder } from "./source.js";
|
|
3
|
+
// src/engine/tree.ts
|
|
4
|
+
/**
|
|
5
|
+
* The groups along one axis. Node 0 is the whole axis (the grand total);
|
|
6
|
+
* a node at depth `d` is a group of the `d`-th field inside its parent.
|
|
7
|
+
* Children are kept in label order, as compressed lists: the children of
|
|
8
|
+
* `node` are `children[childStart[node]]` up to `children[childStart[node + 1]]`.
|
|
9
|
+
*/
|
|
10
|
+
var PivotTree = class PivotTree {
|
|
11
|
+
/** The root: the grand total, as a row or column group. */
|
|
12
|
+
total = 0;
|
|
13
|
+
size;
|
|
14
|
+
levels;
|
|
15
|
+
parent;
|
|
16
|
+
depth;
|
|
17
|
+
/** The group number within its field (`-1` for the root). */
|
|
18
|
+
code;
|
|
19
|
+
childStart;
|
|
20
|
+
children;
|
|
21
|
+
/** Where each node starts and ends in a walk of the tree, to tell descendants apart. */
|
|
22
|
+
enter;
|
|
23
|
+
exit;
|
|
24
|
+
index;
|
|
25
|
+
stride;
|
|
26
|
+
constructor(builder, levels) {
|
|
27
|
+
const size = builder.size;
|
|
28
|
+
this.size = size;
|
|
29
|
+
this.levels = levels;
|
|
30
|
+
this.parent = builder.parent.slice(0, size);
|
|
31
|
+
this.depth = builder.depth.slice(0, size);
|
|
32
|
+
this.code = builder.code.slice(0, size);
|
|
33
|
+
this.index = builder.index;
|
|
34
|
+
this.stride = builder.stride;
|
|
35
|
+
const childStart = new Int32Array(size + 1);
|
|
36
|
+
for (let node = 1; node < size; node++) childStart[this.parent[node] + 1]++;
|
|
37
|
+
for (let node = 0; node < size; node++) childStart[node + 1] += childStart[node];
|
|
38
|
+
const fill = childStart.slice(0, size);
|
|
39
|
+
const children = new Int32Array(Math.max(0, size - 1));
|
|
40
|
+
for (let node = 1; node < size; node++) children[fill[this.parent[node]]++] = node;
|
|
41
|
+
this.childStart = childStart;
|
|
42
|
+
this.children = children;
|
|
43
|
+
for (let node = 0; node < size; node++) {
|
|
44
|
+
const from = childStart[node];
|
|
45
|
+
const to = childStart[node + 1];
|
|
46
|
+
if (to - from < 2) continue;
|
|
47
|
+
const level = levels[this.depth[node]];
|
|
48
|
+
const { rank } = level.column;
|
|
49
|
+
const sign = level.sort === "desc" ? -1 : 1;
|
|
50
|
+
const code = this.code;
|
|
51
|
+
children.subarray(from, to).sort((a, b) => {
|
|
52
|
+
const ra = rank[code[a]];
|
|
53
|
+
const rb = rank[code[b]];
|
|
54
|
+
if (level.column.empty >= 0) {
|
|
55
|
+
if (code[a] === level.column.empty) return 1;
|
|
56
|
+
if (code[b] === level.column.empty) return -1;
|
|
57
|
+
}
|
|
58
|
+
return sign * (ra - rb);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
this.enter = new Int32Array(size);
|
|
62
|
+
this.exit = new Int32Array(size);
|
|
63
|
+
this.walk();
|
|
64
|
+
}
|
|
65
|
+
/** The tree as data: typed arrays to move to another thread, or to write as JSON. */
|
|
66
|
+
pack() {
|
|
67
|
+
return {
|
|
68
|
+
size: this.size,
|
|
69
|
+
stride: this.stride,
|
|
70
|
+
parent: this.parent,
|
|
71
|
+
depth: this.depth,
|
|
72
|
+
code: this.code,
|
|
73
|
+
childStart: this.childStart,
|
|
74
|
+
children: this.children,
|
|
75
|
+
enter: this.enter,
|
|
76
|
+
exit: this.exit,
|
|
77
|
+
index: this.index.pack(),
|
|
78
|
+
levels: this.levels.map((l) => ({
|
|
79
|
+
field: l.field.name,
|
|
80
|
+
sort: l.sort,
|
|
81
|
+
subtotal: l.subtotal,
|
|
82
|
+
...l.by === void 0 ? {} : { by: l.by },
|
|
83
|
+
labels: l.column.labels,
|
|
84
|
+
empty: l.column.empty,
|
|
85
|
+
rank: l.column.rank
|
|
86
|
+
}))
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** A tree made from `pack()`'s output, with fields described by `field`. */
|
|
90
|
+
static unpack(packet, field) {
|
|
91
|
+
const tree = Object.create(PivotTree.prototype);
|
|
92
|
+
const levels = packet.levels.map((l) => {
|
|
93
|
+
const f = field(l.field);
|
|
94
|
+
const labels = f.type === "date" && l.by === void 0 ? l.labels.map((v) => typeof v === "string" ? new Date(v) : v) : l.labels;
|
|
95
|
+
return {
|
|
96
|
+
field: f,
|
|
97
|
+
sort: l.sort,
|
|
98
|
+
subtotal: l.subtotal,
|
|
99
|
+
by: l.by,
|
|
100
|
+
column: {
|
|
101
|
+
codes: /* @__PURE__ */ new Uint32Array(0),
|
|
102
|
+
labels,
|
|
103
|
+
empty: l.empty,
|
|
104
|
+
rank: typed(Uint32Array, l.rank),
|
|
105
|
+
find: finder(labels, l.empty)
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
Object.assign(tree, {
|
|
110
|
+
total: 0,
|
|
111
|
+
size: packet.size,
|
|
112
|
+
levels,
|
|
113
|
+
parent: typed(Int32Array, packet.parent),
|
|
114
|
+
depth: typed(Uint8Array, packet.depth),
|
|
115
|
+
code: typed(Int32Array, packet.code),
|
|
116
|
+
childStart: typed(Int32Array, packet.childStart),
|
|
117
|
+
children: typed(Int32Array, packet.children),
|
|
118
|
+
enter: typed(Int32Array, packet.enter),
|
|
119
|
+
exit: typed(Int32Array, packet.exit),
|
|
120
|
+
index: IntIndex.unpack(packet.index),
|
|
121
|
+
stride: packet.stride
|
|
122
|
+
});
|
|
123
|
+
return tree;
|
|
124
|
+
}
|
|
125
|
+
/** The node's own value (its group label); `undefined` for the root. */
|
|
126
|
+
label(node) {
|
|
127
|
+
if (node <= 0) return void 0;
|
|
128
|
+
return this.levels[this.depth[node] - 1].column.labels[this.code[node]];
|
|
129
|
+
}
|
|
130
|
+
/** The labels from the outermost group down to `node`. */
|
|
131
|
+
path(node) {
|
|
132
|
+
const path = [];
|
|
133
|
+
for (let at = node; at > 0; at = this.parent[at]) path.push(this.label(at));
|
|
134
|
+
return path.reverse();
|
|
135
|
+
}
|
|
136
|
+
/** The node with these labels, or -1. */
|
|
137
|
+
find(path) {
|
|
138
|
+
let node = 0;
|
|
139
|
+
for (let d = 0; d < path.length; d++) {
|
|
140
|
+
const level = this.levels[d];
|
|
141
|
+
if (!level) return -1;
|
|
142
|
+
const code = level.column.find(path[d]);
|
|
143
|
+
if (code < 0) return -1;
|
|
144
|
+
const id = this.index.get(node * this.stride + code);
|
|
145
|
+
if (id < 0) return -1;
|
|
146
|
+
node = id + 1;
|
|
147
|
+
}
|
|
148
|
+
return node;
|
|
149
|
+
}
|
|
150
|
+
/** Whether `node` has groups inside it. */
|
|
151
|
+
isGroup(node) {
|
|
152
|
+
return this.childStart[node + 1] > this.childStart[node];
|
|
153
|
+
}
|
|
154
|
+
/** Whether `node` is `ancestor` or inside it. */
|
|
155
|
+
within(node, ancestor) {
|
|
156
|
+
return this.enter[node] >= this.enter[ancestor] && this.enter[node] < this.exit[ancestor];
|
|
157
|
+
}
|
|
158
|
+
walk() {
|
|
159
|
+
const { childStart, children, enter, exit } = this;
|
|
160
|
+
const stack = new Int32Array(this.levels.length + 2);
|
|
161
|
+
const next = new Int32Array(this.levels.length + 2);
|
|
162
|
+
let top = 0;
|
|
163
|
+
let clock = 0;
|
|
164
|
+
stack[0] = 0;
|
|
165
|
+
next[0] = childStart[0];
|
|
166
|
+
enter[0] = clock++;
|
|
167
|
+
while (top >= 0) {
|
|
168
|
+
const node = stack[top];
|
|
169
|
+
if (next[top] < childStart[node + 1]) {
|
|
170
|
+
const child = children[next[top]++];
|
|
171
|
+
top++;
|
|
172
|
+
stack[top] = child;
|
|
173
|
+
next[top] = childStart[child];
|
|
174
|
+
enter[child] = clock++;
|
|
175
|
+
} else {
|
|
176
|
+
exit[node] = clock;
|
|
177
|
+
top--;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
/** Grows a tree one record at a time: each record walks down its groups. */
|
|
183
|
+
var PivotTreeBuilder = class {
|
|
184
|
+
stride;
|
|
185
|
+
parent = /* @__PURE__ */ new Int32Array(64);
|
|
186
|
+
depth = /* @__PURE__ */ new Uint8Array(64);
|
|
187
|
+
code = /* @__PURE__ */ new Int32Array(64);
|
|
188
|
+
size = 1;
|
|
189
|
+
index;
|
|
190
|
+
/** `stride` is more than any group number, so (parent, group) makes a unique key. */
|
|
191
|
+
constructor(stride) {
|
|
192
|
+
this.stride = stride;
|
|
193
|
+
this.parent[0] = -1;
|
|
194
|
+
this.code[0] = -1;
|
|
195
|
+
this.index = new IntIndex(256);
|
|
196
|
+
}
|
|
197
|
+
/** The child of `node` for group `code`, made when new. */
|
|
198
|
+
child(node, code) {
|
|
199
|
+
const index = this.index;
|
|
200
|
+
const before = index.size;
|
|
201
|
+
const child = index.add(node * this.stride + code) + 1;
|
|
202
|
+
if (index.size !== before) {
|
|
203
|
+
if (child >= this.parent.length) this.grow();
|
|
204
|
+
this.parent[child] = node;
|
|
205
|
+
this.depth[child] = this.depth[node] + 1;
|
|
206
|
+
this.code[child] = code;
|
|
207
|
+
this.size = child + 1;
|
|
208
|
+
}
|
|
209
|
+
return child;
|
|
210
|
+
}
|
|
211
|
+
grow() {
|
|
212
|
+
const capacity = this.parent.length * 2;
|
|
213
|
+
const parent = new Int32Array(capacity);
|
|
214
|
+
parent.set(this.parent);
|
|
215
|
+
const depth = new Uint8Array(capacity);
|
|
216
|
+
depth.set(this.depth);
|
|
217
|
+
const code = new Int32Array(capacity);
|
|
218
|
+
code.set(this.code);
|
|
219
|
+
this.parent = parent;
|
|
220
|
+
this.depth = depth;
|
|
221
|
+
this.code = code;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
export { PivotTree, PivotTreeBuilder };
|
|
225
|
+
|
|
226
|
+
//# sourceMappingURL=tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree.js","names":[],"sources":["../../src/engine/tree.ts"],"sourcesContent":["import type { PivotGroupBy } from './group'\nimport { IntIndex, typed, type PivotIndexPacket } from './hash'\nimport { finder, type PivotDimensionColumn, type PivotResolvedField } from './source'\n\n/** A field on an axis, with what the config asked of it. */\nexport interface PivotLevel {\n field: PivotResolvedField<unknown>\n column: PivotDimensionColumn\n sort: 'asc' | 'desc'\n subtotal: boolean\n /** How the field's values are grouped, when not by each value. */\n by?: PivotGroupBy\n}\n\n/**\n * The groups along one axis. Node 0 is the whole axis (the grand total);\n * a node at depth `d` is a group of the `d`-th field inside its parent.\n * Children are kept in label order, as compressed lists: the children of\n * `node` are `children[childStart[node]]` up to `children[childStart[node + 1]]`.\n */\nexport class PivotTree {\n /** The root: the grand total, as a row or column group. */\n readonly total = 0\n readonly size: number\n readonly levels: readonly PivotLevel[]\n readonly parent: Int32Array\n readonly depth: Uint8Array\n /** The group number within its field (`-1` for the root). */\n readonly code: Int32Array\n readonly childStart: Int32Array\n readonly children: Int32Array\n /** Where each node starts and ends in a walk of the tree, to tell descendants apart. */\n readonly enter: Int32Array\n readonly exit: Int32Array\n private index: IntIndex\n private stride: number\n\n constructor(builder: PivotTreeBuilder, levels: readonly PivotLevel[]) {\n const size = builder.size\n this.size = size\n this.levels = levels\n this.parent = builder.parent.slice(0, size)\n this.depth = builder.depth.slice(0, size)\n this.code = builder.code.slice(0, size)\n this.index = builder.index\n this.stride = builder.stride\n // children by parent, in the order they were made, then sorted by label\n const childStart = new Int32Array(size + 1)\n for (let node = 1; node < size; node++) childStart[this.parent[node]! + 1]!++\n for (let node = 0; node < size; node++) childStart[node + 1]! += childStart[node]!\n const fill = childStart.slice(0, size)\n const children = new Int32Array(Math.max(0, size - 1))\n for (let node = 1; node < size; node++) children[fill[this.parent[node]!]!++] = node\n this.childStart = childStart\n this.children = children\n for (let node = 0; node < size; node++) {\n const from = childStart[node]!\n const to = childStart[node + 1]!\n if (to - from < 2) continue\n const level = levels[this.depth[node]!]!\n const { rank } = level.column\n const sign = level.sort === 'desc' ? -1 : 1\n const code = this.code\n children.subarray(from, to).sort((a, b) => {\n const ra = rank[code[a]!]!\n const rb = rank[code[b]!]!\n // the empty group stays last either way\n if (level.column.empty >= 0) {\n if (code[a] === level.column.empty) return 1\n if (code[b] === level.column.empty) return -1\n }\n return sign * (ra - rb)\n })\n }\n this.enter = new Int32Array(size)\n this.exit = new Int32Array(size)\n this.walk()\n }\n\n /** The tree as data: typed arrays to move to another thread, or to write as JSON. */\n pack(): PivotTreePacket {\n return {\n size: this.size,\n stride: this.stride,\n parent: this.parent,\n depth: this.depth,\n code: this.code,\n childStart: this.childStart,\n children: this.children,\n enter: this.enter,\n exit: this.exit,\n index: this.index.pack(),\n levels: this.levels.map((l) => ({\n field: l.field.name,\n sort: l.sort,\n subtotal: l.subtotal,\n ...(l.by === undefined ? {} : { by: l.by }),\n labels: l.column.labels,\n empty: l.column.empty,\n rank: l.column.rank,\n })),\n }\n }\n\n /** A tree made from `pack()`'s output, with fields described by `field`. */\n static unpack(\n packet: PivotTreePacket,\n field: (name: string) => PivotResolvedField<unknown>,\n ): PivotTree {\n const tree = Object.create(PivotTree.prototype) as PivotTree\n const levels: PivotLevel[] = packet.levels.map((l) => {\n const f = field(l.field)\n // JSON writes dates as text\n const labels =\n f.type === 'date' && l.by === undefined\n ? l.labels.map((v) => (typeof v === 'string' ? new Date(v) : v))\n : l.labels\n return {\n field: f,\n sort: l.sort,\n subtotal: l.subtotal,\n by: l.by,\n column: {\n codes: new Uint32Array(0),\n labels,\n empty: l.empty,\n rank: typed(Uint32Array, l.rank),\n find: finder(labels, l.empty),\n },\n }\n })\n Object.assign(tree, {\n total: 0,\n size: packet.size,\n levels,\n parent: typed(Int32Array, packet.parent),\n depth: typed(Uint8Array, packet.depth),\n code: typed(Int32Array, packet.code),\n childStart: typed(Int32Array, packet.childStart),\n children: typed(Int32Array, packet.children),\n enter: typed(Int32Array, packet.enter),\n exit: typed(Int32Array, packet.exit),\n index: IntIndex.unpack(packet.index),\n stride: packet.stride,\n })\n return tree\n }\n\n /** The node's own value (its group label); `undefined` for the root. */\n label(node: number): unknown {\n if (node <= 0) return undefined\n return this.levels[this.depth[node]! - 1]!.column.labels[this.code[node]!]\n }\n\n /** The labels from the outermost group down to `node`. */\n path(node: number): unknown[] {\n const path: unknown[] = []\n for (let at = node; at > 0; at = this.parent[at]!) path.push(this.label(at))\n // oxlint-disable-next-line unicorn/no-array-reverse -- reverses its own array\n return path.reverse()\n }\n\n /** The node with these labels, or -1. */\n find(path: readonly unknown[]): number {\n let node = 0\n for (let d = 0; d < path.length; d++) {\n const level = this.levels[d]\n if (!level) return -1\n const code = level.column.find(path[d])\n if (code < 0) return -1\n const id = this.index.get(node * this.stride + code)\n if (id < 0) return -1\n node = id + 1\n }\n return node\n }\n\n /** Whether `node` has groups inside it. */\n isGroup(node: number) {\n return this.childStart[node + 1]! > this.childStart[node]!\n }\n\n /** Whether `node` is `ancestor` or inside it. */\n within(node: number, ancestor: number) {\n return this.enter[node]! >= this.enter[ancestor]! && this.enter[node]! < this.exit[ancestor]!\n }\n\n private walk() {\n const { childStart, children, enter, exit } = this\n const stack = new Int32Array(this.levels.length + 2)\n const next = new Int32Array(this.levels.length + 2)\n let top = 0\n let clock = 0\n stack[0] = 0\n next[0] = childStart[0]!\n enter[0] = clock++\n while (top >= 0) {\n const node = stack[top]!\n if (next[top]! < childStart[node + 1]!) {\n const child = children[next[top]!++]!\n top++\n stack[top] = child\n next[top] = childStart[child]!\n enter[child] = clock++\n } else {\n exit[node] = clock\n top--\n }\n }\n }\n}\n\n/** A `PivotTree` as data. */\nexport interface PivotTreePacket {\n size: number\n stride: number\n parent: ArrayLike<number>\n depth: ArrayLike<number>\n code: ArrayLike<number>\n childStart: ArrayLike<number>\n children: ArrayLike<number>\n enter: ArrayLike<number>\n exit: ArrayLike<number>\n index: PivotIndexPacket\n levels: {\n field: string\n sort: 'asc' | 'desc'\n subtotal: boolean\n by?: PivotGroupBy\n labels: unknown[]\n empty: number\n rank: ArrayLike<number>\n }[]\n}\n\n/** Grows a tree one record at a time: each record walks down its groups. */\nexport class PivotTreeBuilder {\n parent = new Int32Array(64)\n depth = new Uint8Array(64)\n code = new Int32Array(64)\n size = 1\n readonly index: IntIndex\n\n /** `stride` is more than any group number, so (parent, group) makes a unique key. */\n constructor(readonly stride: number) {\n this.parent[0] = -1\n this.code[0] = -1\n this.index = new IntIndex(256)\n }\n\n /** The child of `node` for group `code`, made when new. */\n child(node: number, code: number) {\n const index = this.index\n const before = index.size\n const id = index.add(node * this.stride + code)\n const child = id + 1\n if (index.size !== before) {\n if (child >= this.parent.length) this.grow()\n this.parent[child] = node\n this.depth[child] = this.depth[node]! + 1\n this.code[child] = code\n this.size = child + 1\n }\n return child\n }\n\n private grow() {\n const capacity = this.parent.length * 2\n const parent = new Int32Array(capacity)\n parent.set(this.parent)\n const depth = new Uint8Array(capacity)\n depth.set(this.depth)\n const code = new Int32Array(capacity)\n code.set(this.code)\n this.parent = parent\n this.depth = depth\n this.code = code\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import fields_vue_vue_type_script_setup_true_lang_default from "./fields.vue_vue_type_script_setup_true_lang.js";
|
|
2
|
+
// src/fields/fields.vue
|
|
3
|
+
var fields_default = fields_vue_vue_type_script_setup_true_lang_default;
|
|
4
|
+
export { fields_default as default };
|
|
5
|
+
|
|
6
|
+
//# sourceMappingURL=fields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fields.js","names":["_sfc_main"],"sources":["../../src/fields/fields.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"Row = Record<string, unknown>\">\nimport type { HTMLAttributes } from 'vue'\nimport type { ForwardedAttributes } from 'dou-ui'\nimport {\n DouButton,\n DouCheckbox,\n DouDrawer,\n DouDropdown,\n DouDropdownItem,\n DouDropdownRadioGroup,\n DouDropdownSub,\n DouInput,\n DouPopover,\n DouSortable,\n useBreakpoints,\n} from 'dou-ui'\nimport {\n CalendarIcon,\n ChevronDownIcon,\n FilterIcon,\n HashIcon,\n LayoutThreeColumnsIcon,\n LayoutThreeRowsIcon,\n PlusIcon,\n SearchIcon,\n SigmaIcon,\n TablePivotIcon,\n ToggleLeftIcon,\n TypeIcon,\n} from '@dou-ui/icons'\nimport { computed, nextTick, onMounted, ref, useId, type Component } from 'vue'\nimport {\n resolveConfig,\n type PivotAggregator,\n type PivotResolvedField,\n type PivotShowAs,\n type PivotSource,\n} from '../engine'\nimport { BUILTIN_AGGREGATORS } from '../engine/aggregate'\nimport { DATE_PERIODS, groupKey, type PivotGroupBy } from '../engine/group'\nimport type { PivotConfig, PivotDimension, PivotFieldType, PivotFilter } from '../engine/types'\nimport { valueLabel } from '../core/labels'\nimport { usePivotSource } from '../core/source'\nimport { format, usePivotMessages } from '../locale'\nimport type { PivotFieldsProps } from '../types'\nimport FilterList from './filter-list.vue'\nimport FormulaDialog from './formula-dialog.vue'\nimport TopDialog from './top-dialog.vue'\nimport {\n areasOf,\n AREAS,\n configOf,\n isFiltering,\n moveItem,\n stepsFor,\n withAggregate,\n withDimension,\n withFilter,\n withRank,\n withValue,\n type PivotArea,\n type PivotAreas,\n type PivotFieldItem,\n type PivotRank,\n} from './model'\n\ndefineOptions({ name: 'DouPivotFields' })\n\nconst props = withDefaults(\n defineProps<\n PivotFieldsProps<Row> &\n /* @vue-ignore */ ForwardedAttributes<HTMLAttributes, PivotFieldsProps<Row>>\n >(),\n { drawer: 'auto' },\n)\nconst config = defineModel<PivotConfig>('config', { default: () => ({}) })\n\nconst messages = usePivotMessages(() => props.messages)\nconst source = usePivotSource(\n () => props.data,\n () => props.fields,\n)\nconst group = `dou-pivot-fields-${useId()}`\n\n// ---- the fields ----\n\nconst TYPE_ICON: Record<PivotFieldType, Component> = {\n string: TypeIcon,\n number: HashIcon,\n date: CalendarIcon,\n boolean: ToggleLeftIcon,\n}\nconst AREA_ICON: Record<PivotArea, Component> = {\n filters: FilterIcon,\n columns: LayoutThreeColumnsIcon,\n rows: LayoutThreeRowsIcon,\n values: SigmaIcon,\n}\n\nconst allFields = computed<PivotResolvedField<unknown>[]>(() => {\n const s = source.value\n // the source knows each listed field, and makes one up for any other key the config names\n return [...s.fields.values()] as PivotResolvedField<unknown>[]\n})\nconst field = (name: string) => source.value.field(name) as PivotResolvedField<unknown>\n\nconst query = ref('')\nconst listItems = computed<PivotFieldItem[]>({\n get: () => {\n const q = query.value.trim().toLocaleLowerCase()\n return allFields.value\n .filter((f) => !q || f.label.toLocaleLowerCase().includes(q) || f.name.includes(q))\n .map((f, index): PivotFieldItem => ({\n key: `l:${f.name}`,\n kind: 'field',\n field: f.name,\n index,\n }))\n },\n // the list always shows every field: dragging one out copies it\n set: () => {},\n})\n\nconst areas = computed(() => areasOf(config.value))\nconst used = computed(() => {\n const set = new Set<string>()\n for (const area of AREAS) for (const item of areas.value[area]) set.add(item.field)\n return set\n})\n\n// ---- moving fields ----\n\nlet pending: PivotAreas | null = null\n/** One drop updates two lists (the one left and the one entered); both land in one change. */\nfunction setArea(area: PivotArea, items: PivotFieldItem[]) {\n if (!pending) {\n pending = { ...areas.value }\n queueMicrotask(() => {\n const next = pending!\n pending = null\n config.value = configOf(next, config.value)\n })\n }\n pending[area] = items\n}\n\nconst areaModel = (area: PivotArea) =>\n computed<PivotFieldItem[]>({\n get: () => areas.value[area],\n set: (items) => setArea(area, items),\n })\nconst models = Object.fromEntries(AREAS.map((a) => [a, areaModel(a)])) as Record<\n PivotArea,\n ReturnType<typeof areaModel>\n>\n\nconst areaOf = (item: PivotFieldItem): PivotArea | null =>\n AREAS.find((a) => areas.value[a].some((i) => i.key === item.key)) ?? null\n\nfunction moveTo(item: PivotFieldItem, to: PivotArea | null) {\n config.value = configOf(moveItem(areas.value, areaOf(item), item, to), config.value)\n}\n\n/** Moves an item one place up or down in its area. */\nfunction shift(item: PivotFieldItem, by: -1 | 1) {\n const area = areaOf(item)\n if (!area) return\n const list = [...areas.value[area]]\n const at = list.findIndex((i) => i.key === item.key)\n const to = at + by\n if (to < 0 || to >= list.length) return\n list.splice(at, 1)\n list.splice(to, 0, item)\n config.value = configOf({ ...areas.value, [area]: list }, config.value)\n}\nconst placeOf = (item: PivotFieldItem) => {\n const area = areaOf(item)\n const list = area ? areas.value[area] : []\n return { first: list[0]?.key === item.key, last: list[list.length - 1]?.key === item.key }\n}\n\n/** The checkbox in the list: adds a field where it fits (numbers to the values), or takes it out everywhere. */\nfunction toggleField(name: string, on: boolean) {\n if (!on) {\n let next = areas.value\n for (const area of AREAS) next = { ...next, [area]: next[area].filter((i) => i.field !== name) }\n config.value = configOf(next, config.value)\n return\n }\n const item: PivotFieldItem = { key: `l:${name}`, kind: 'field', field: name, index: 0 }\n moveTo(item, field(name).type === 'number' ? 'values' : 'rows')\n}\n\nconst itemLabel = (item: PivotFieldItem) => (item.field ? field(item.field).label : '')\n\n// ---- what each chip shows ----\n\nconst resolved = computed(() => {\n try {\n return resolveConfig(source.value as never, config.value, { aggregators: aggregatorMap.value })\n } catch {\n return null\n }\n})\nconst aggregatorMap = computed(() => {\n const map: Record<string, PivotAggregator> = {}\n for (const a of props.aggregators ?? []) map[a.name] = a\n return map\n})\n\n/** How a grouping reads, as in the grouping menu. */\nfunction groupText(by: PivotGroupBy) {\n const m = messages.value.fields\n if (typeof by === 'string') return m.periods[by]\n const write = new Intl.NumberFormat(undefined, { maximumFractionDigits: 6 })\n return format(m.step, { step: write.format(by.step) })\n}\n\n/** A chip's grouping, shown apart from its name so it can go on a line of its own. */\nfunction chipGroup(item: PivotFieldItem) {\n if (item.kind !== 'dimension') return ''\n const by = dimensionOf(axisOf(item), item.field)?.by\n const top = topText(item.field)\n return [by === undefined ? '' : groupText(by), top].filter(Boolean).join(' · ')\n}\n\n/** How a field's top N or range reads (\"前 10 项\", \"合计 ≥ 100\"), or ''. */\nfunction topText(name: string) {\n const filter = filterOf(name)\n const m = messages.value.fields\n if (filter?.top) {\n return format(filter.top.order === 'asc' ? m.bottomN : m.topN, { count: filter.top.count })\n }\n if (filter?.between) {\n const write = new Intl.NumberFormat(undefined, { maximumFractionDigits: 6 })\n const { min, max } = filter.between\n const range =\n min !== undefined && max !== undefined\n ? format(messages.value.rangeLabel, { from: write.format(min), to: write.format(max) })\n : min !== undefined\n ? `≥ ${write.format(min)}`\n : `≤ ${write.format(max ?? 0)}`\n return format(m.between, { range })\n }\n return ''\n}\n\nconst chipName = (item: PivotFieldItem) =>\n item.kind === 'dimension' ? itemLabel(item) : chipText(item)\n\nfunction chipText(item: PivotFieldItem) {\n if (item.kind === 'dimension') {\n const extra = chipGroup(item)\n return extra\n ? format(messages.value.fields.grouped, { field: itemLabel(item), group: extra })\n : itemLabel(item)\n }\n if (item.kind !== 'value') return itemLabel(item)\n const value = resolved.value?.values[item.index]\n const given = config.value.values?.[item.index]\n if (!value) return item.field ? itemLabel(item) : messages.value.aggregates.count!\n return valueLabel(value, typeof given === 'object' ? given.label : undefined, messages.value)\n}\n\nconst dimensionOf = (axis: 'rows' | 'columns', name: string) => {\n const d = (config.value[axis] ?? []).find((x) => (typeof x === 'string' ? x : x.field) === name)\n return (typeof d === 'string' ? { field: d } : d) as Exclude<PivotDimension, string> | undefined\n}\n\nconst filterOf = (name: string) => config.value.filters?.[name]\n\n/** Whether a field's values can be filtered from its chip (not a value: it sums the field up). */\nconst canFilter = (item: PivotFieldItem) => item.kind !== 'value' && !!item.field\n\n// ---- the menus ----\n\nconst aggregatesFor = (name: string) => {\n const type = name ? field(name).type : 'string'\n const builtIn =\n type === 'number'\n ? ['sum', 'count', 'avg', 'min', 'max', 'distinct', 'median']\n : type === 'date'\n ? ['count', 'distinct', 'min', 'max']\n : ['count', 'distinct']\n const custom = (props.aggregators ?? [])\n .filter((a) => type === 'number' || a.input === 'value')\n .map((a) => a.name)\n return [...builtIn, ...custom.filter((n) => !builtIn.includes(n))]\n}\nconst aggregateText = (name: string) =>\n messages.value.aggregates[name] ??\n aggregatorMap.value[name]?.label ??\n BUILTIN_AGGREGATORS[name]?.label ??\n name\n\nconst currentAggregate = (index: number) => resolved.value?.values[index]?.aggregate ?? 'count'\n\n/** The groupings a field offers: periods for dates, round range sizes for numbers. */\nfunction groupingsFor(name: string): { key: string; by: PivotGroupBy | null; text: string }[] {\n const f = field(name)\n const m = messages.value.fields\n const none = { key: '', by: null, text: m.noGroup }\n if (f.type === 'date')\n return [none, ...DATE_PERIODS.map((p) => ({ key: p, by: p, text: m.periods[p] }))]\n if (f.type !== 'number') return []\n const measure = source.value.measure(name)\n let min = Infinity\n let max = -Infinity\n for (let i = 0; i < measure.length; i++) {\n const v = measure[i]!\n if (v < min) min = v\n if (v > max) max = v\n }\n const write = new Intl.NumberFormat(undefined, { maximumFractionDigits: 6 })\n return [\n none,\n ...stepsFor(min, max).map((step) => ({\n key: groupKey({ step }),\n by: { step },\n text: format(m.step, { step: write.format(step) }),\n })),\n ]\n}\n\n/** The sort a row field's menu shows: by its labels, or the rows by a value's grand total. */\nfunction sortKey(axis: 'rows' | 'columns', name: string) {\n const sort = config.value.sort\n if (axis === 'rows' && sort && !sort.column?.length) return `v:${sort.valueIndex}:${sort.order}`\n return dimensionOf(axis, name)?.sort ?? 'asc'\n}\nfunction onSort(axis: 'rows' | 'columns', name: string, key: string | number) {\n const by = /^v:(\\d+):(asc|desc)$/.exec(String(key))\n if (by) {\n const order = by[2] as 'asc' | 'desc'\n config.value = { ...config.value, sort: { valueIndex: +by[1]!, column: [], order } }\n return\n }\n const next = withDimension(config.value, axis, name, { sort: key as 'asc' | 'desc' })\n // sorting by labels on the rows takes over from sorting by a value\n if (axis === 'rows') next.sort = null\n config.value = next\n}\nfunction onGroup(axis: 'rows' | 'columns', name: string, key: string | number) {\n const option = groupingsFor(name).find((g) => g.key === key)\n if (!option) return\n config.value = withDimension(config.value, axis, name, { by: option.by })\n}\nfunction onSubtotal(axis: 'rows' | 'columns', name: string, on: boolean) {\n config.value = withDimension(config.value, axis, name, { subtotal: on })\n}\nfunction onAggregate(index: number, name: string | number) {\n config.value = withAggregate(config.value, index, String(name))\n}\n\n// ---- showing values ----\n\nconst SHOWS: PivotShowAs[] = [\n 'percentOfTotal',\n 'percentOfRow',\n 'percentOfColumn',\n 'percentOfParent',\n 'difference',\n 'percentDifference',\n 'runningTotal',\n 'rank',\n]\nconst valueSpec = (index: number) => {\n const v = config.value.values?.[index]\n return typeof v === 'object' ? v : undefined\n}\nconst showKey = (index: number) => {\n const show = valueSpec(index)?.show\n return show === undefined ? '' : typeof show === 'string' ? show : show.as\n}\nfunction onShow(index: number, key: string | number) {\n const spec = valueSpec(index)?.show\n const axis = typeof spec === 'object' ? spec.axis : undefined\n const show = key === '' ? null : axis ? { as: key as PivotShowAs, axis } : (key as PivotShowAs)\n config.value = withValue(config.value, index, { show: show as PivotShowAs | null })\n}\nconst markKey = (index: number) => {\n const mark = valueSpec(index)?.mark\n return mark === undefined ? '' : typeof mark === 'string' ? mark : mark.type\n}\nfunction onMark(index: number, key: string | number) {\n config.value = withValue(config.value, index, {\n mark: key === '' ? null : (key as 'bar' | 'scale'),\n })\n}\n\n// ---- top N ----\n\nconst TOPS = [\n { key: 't5', count: 5, order: 'desc' },\n { key: 't10', count: 10, order: 'desc' },\n { key: 't20', count: 20, order: 'desc' },\n { key: 'b5', count: 5, order: 'asc' },\n { key: 'b10', count: 10, order: 'asc' },\n] as const\nfunction topKey(name: string) {\n const filter = filterOf(name)\n if (filter?.between) return 'range'\n if (!filter?.top) return ''\n return `${filter.top.order === 'asc' ? 'b' : 't'}${filter.top.count}`\n}\nconst topOptions = (name: string) => {\n const m = messages.value.fields\n const options: { key: string; text: string }[] = [{ key: '', text: m.noTop }]\n for (const t of TOPS) {\n options.push({\n key: t.key,\n text: format(t.order === 'asc' ? m.bottomN : m.topN, { count: t.count }),\n })\n }\n // a top N or a range set elsewhere, which the list does not offer\n const key = topKey(name)\n if (key && !options.some((o) => o.key === key)) options.push({ key, text: topText(name) })\n return options\n}\nfunction onTop(item: PivotFieldItem, key: string | number) {\n if (key === 'range') return\n const match = /^([tb])(\\d+)$/.exec(String(key))\n const old = filterOf(item.field)?.top\n setRank(\n item,\n match\n ? {\n top: {\n ...(old?.valueIndex ? { valueIndex: old.valueIndex } : {}),\n count: Number(match[2]),\n ...(match[1] === 'b' ? { order: 'asc' as const } : {}),\n },\n }\n : null,\n )\n}\nfunction setRank(item: PivotFieldItem, rank: PivotRank | null) {\n config.value = withRank(config.value, item.field, rank, item.kind === 'filter')\n}\n\n/** The field whose top N or range is set in the dialog. */\nconst topItem = ref<PivotFieldItem | null>(null)\nconst topOpen = ref(false)\nfunction openTop(item: PivotFieldItem) {\n topItem.value = item\n // after the menu has closed and given focus back\n nextTick(() => (topOpen.value = true))\n}\n\n// ---- calculated values ----\n\nconst formulaOpen = ref(false)\n/** The value being changed in the dialog, or -1 for a new one. */\nconst formulaIndex = ref(-1)\nconst formulaInitial = computed(() => {\n const spec = formulaIndex.value >= 0 ? valueSpec(formulaIndex.value) : undefined\n return spec?.formula === undefined ? null : { formula: spec.formula, label: spec.label }\n})\nfunction openFormula(index: number) {\n formulaIndex.value = index\n // after a menu has closed and given focus back\n nextTick(() => (formulaOpen.value = true))\n}\nfunction applyFormula(value: { formula: string; label?: string }) {\n const index = formulaIndex.value\n if (index < 0) {\n config.value = { ...config.value, values: [...(config.value.values ?? []), value] }\n return\n }\n config.value = withValue(config.value, index, {\n formula: value.formula,\n label: value.label ?? null,\n })\n}\nconst isFormula = (item: PivotFieldItem) =>\n item.kind === 'value' && valueSpec(item.index)?.formula !== undefined\n\n// ---- filters ----\n\nconst filterOpen = ref<string | null>(null)\nfunction setFilterOpen(key: string, open: boolean) {\n if (open) filterOpen.value = key\n else if (filterOpen.value === key) filterOpen.value = null\n}\nfunction openFilter(item: PivotFieldItem) {\n // after the menu has closed and given focus back\n nextTick(() => (filterOpen.value = item.key))\n}\nconst filterBy = (item: PivotFieldItem): PivotGroupBy | undefined => {\n if (item.kind === 'dimension') {\n const axis = areas.value.rows.some((i) => i.key === item.key) ? 'rows' : 'columns'\n return dimensionOf(axis, item.field)?.by\n }\n return filterOf(item.field)?.by\n}\nfunction applyFilter(item: PivotFieldItem, filter: PivotFilter | null) {\n // the list sets which groups are picked; top N and ranges stay\n const old = filterOf(item.field)\n const kept = { top: old?.top, between: old?.between }\n if (kept.top || kept.between) {\n filter = {\n ...filter,\n ...(kept.top ? { top: kept.top } : {}),\n ...(kept.between ? { between: kept.between } : {}),\n }\n if (filter.by === undefined && old?.by !== undefined) filter.by = old.by\n }\n config.value = withFilter(config.value, item.field, filter, item.kind === 'filter')\n filterOpen.value = null\n}\n\n// ---- a narrow screen ----\n\nconst breakpoints = useBreakpoints()\nconst mounted = ref(false)\nonMounted(() => (mounted.value = true))\nconst inDrawer = computed(() =>\n props.drawer === 'auto' ? mounted.value && !breakpoints.sm : props.drawer,\n)\nconst drawerOpen = ref(false)\n\nconst areaTitle = (area: PivotArea) => messages.value.fields.areas[area]\n/** Where else an item can go; a count of records has no field to group or filter by. */\nconst otherAreas = (item: PivotFieldItem) => {\n const here = areaOf(item)\n return AREAS.filter((a) => a !== here && (item.field || a === 'values'))\n}\nconst axisOf = (item: PivotFieldItem) =>\n areas.value.rows.some((i) => i.key === item.key) ? ('rows' as const) : ('columns' as const)\n\nconst filtered = (item: PivotFieldItem) => isFiltering(filterOf(item.field))\n</script>\n\n<template>\n <div\n :class=\"inDrawer ? 'dou-pivot-fields-trigger' : 'dou-pivot-fields'\"\n :role=\"inDrawer ? undefined : 'region'\"\n :aria-label=\"inDrawer ? undefined : messages.fields.panel\"\n >\n <DouButton v-if=\"inDrawer\" size=\"sm\" variant=\"outline\" @click=\"drawerOpen = true\">\n <template #leading><TablePivotIcon aria-hidden=\"true\" /></template>\n {{ messages.fields.open }}\n </DouButton>\n <component\n :is=\"inDrawer ? DouDrawer : 'div'\"\n v-bind=\"\n inDrawer\n ? { open: drawerOpen, title: messages.fields.panel, placement: 'bottom', size: '80%' }\n : { class: 'dou-pivot-fields__inner' }\n \"\n @update:open=\"drawerOpen = $event\"\n >\n <div :class=\"inDrawer ? 'dou-pivot-fields is-in-drawer' : 'dou-pivot-fields__body'\">\n <section class=\"dou-pivot-fields__list-part\" :aria-label=\"messages.fields.list\">\n <DouInput\n v-model=\"query\"\n size=\"sm\"\n type=\"search\"\n clearable\n :placeholder=\"messages.fields.search\"\n :aria-label=\"messages.fields.search\"\n >\n <template #prefix><SearchIcon aria-hidden=\"true\" /></template>\n </DouInput>\n <DouSortable\n v-model=\"listItems\"\n class=\"dou-pivot-fields__list\"\n :group=\"group\"\n :animation=\"false\"\n item-key=\"key\"\n :item-label=\"(item) => itemLabel(item)\"\n >\n <template #default=\"{ item }\">\n <div class=\"dou-pivot-fields__field\">\n <DouCheckbox\n size=\"sm\"\n :model-value=\"used.has(item.field)\"\n :aria-label=\"itemLabel(item)\"\n @update:model-value=\"toggleField(item.field, !!$event)\"\n />\n <component\n :is=\"TYPE_ICON[field(item.field).type]\"\n class=\"dou-pivot-fields__type\"\n aria-hidden=\"true\"\n />\n <span class=\"dou-pivot-fields__name\">{{ itemLabel(item) }}</span>\n </div>\n </template>\n <template #empty>\n <p class=\"dou-pivot-fields__none\">{{ messages.fields.noFields }}</p>\n </template>\n </DouSortable>\n </section>\n\n <div class=\"dou-pivot-fields__areas\">\n <section\n v-for=\"area in AREAS\"\n :key=\"area\"\n class=\"dou-pivot-fields__area\"\n :class=\"`is-${area}`\"\n :aria-label=\"areaTitle(area)\"\n >\n <h3 class=\"dou-pivot-fields__area-title\">\n <component :is=\"AREA_ICON[area]\" aria-hidden=\"true\" />\n {{ areaTitle(area) }}\n </h3>\n <DouSortable\n v-model=\"models[area].value\"\n class=\"dou-pivot-fields__chips\"\n :group=\"group\"\n item-key=\"key\"\n :item-label=\"(item) => chipText(item)\"\n >\n <template #default=\"{ item }\">\n <div class=\"dou-pivot-fields__chip\" :class=\"{ 'is-filtered': filtered(item) }\">\n <span class=\"dou-pivot-fields__chip-text\" :title=\"chipText(item)\">\n <span class=\"dou-pivot-fields__chip-name\">{{ chipName(item) }}</span>\n <span v-if=\"chipGroup(item)\" class=\"dou-pivot-fields__chip-group\">{{\n chipGroup(item)\n }}</span>\n </span>\n <DouPopover\n v-if=\"canFilter(item)\"\n :open=\"filterOpen === item.key\"\n placement=\"bottom-start\"\n :width=\"260\"\n @update:open=\"setFilterOpen(item.key, $event)\"\n >\n <button\n type=\"button\"\n class=\"dou-pivot-fields__chip-button\"\n :class=\"{ 'is-on': filtered(item) }\"\n :aria-label=\"\n filtered(item)\n ? `${messages.fields.filtered},${format(messages.filter.title, { field: itemLabel(item) })}`\n : format(messages.filter.title, { field: itemLabel(item) })\n \"\n >\n <FilterIcon aria-hidden=\"true\" />\n </button>\n <template #content>\n <!-- a fresh list each time it opens, picked as the filter is now -->\n <FilterList\n :key=\"filterOpen === item.key ? 'open' : 'closed'\"\n :source=\"source as never\"\n :field=\"item.field\"\n :by=\"filterBy(item)\"\n :filter=\"filterOf(item.field)\"\n :messages=\"messages\"\n @apply=\"applyFilter(item, $event)\"\n @cancel=\"filterOpen = null\"\n />\n </template>\n </DouPopover>\n <DouDropdown placement=\"bottom-end\">\n <button\n type=\"button\"\n class=\"dou-pivot-fields__chip-button\"\n :aria-label=\"format(messages.fields.menu, { field: chipText(item) })\"\n >\n <ChevronDownIcon aria-hidden=\"true\" />\n </button>\n <template #menu>\n <template v-if=\"item.kind === 'dimension'\">\n <DouDropdownSub :title=\"messages.fields.sort\">\n <DouDropdownRadioGroup\n :model-value=\"sortKey(axisOf(item), item.field)\"\n @update:model-value=\"onSort(axisOf(item), item.field, $event!)\"\n >\n <DouDropdownItem value=\"asc\">{{ messages.fields.asc }}</DouDropdownItem>\n <DouDropdownItem value=\"desc\">{{\n messages.fields.desc\n }}</DouDropdownItem>\n <template v-if=\"axisOf(item) === 'rows'\">\n <template v-for=\"(value, index) in areas.values\" :key=\"value.key\">\n <DouDropdownItem\n :value=\"`v:${index}:desc`\"\n :divided=\"index === 0\"\n >{{\n format(messages.fields.byValueDesc, { value: chipText(value) })\n }}</DouDropdownItem\n >\n <DouDropdownItem :value=\"`v:${index}:asc`\">{{\n format(messages.fields.byValueAsc, { value: chipText(value) })\n }}</DouDropdownItem>\n </template>\n </template>\n </DouDropdownRadioGroup>\n </DouDropdownSub>\n <DouDropdownSub\n v-if=\"groupingsFor(item.field).length\"\n :title=\"messages.fields.groupBy\"\n >\n <DouDropdownRadioGroup\n :model-value=\"groupKey(dimensionOf(axisOf(item), item.field)?.by)\"\n @update:model-value=\"onGroup(axisOf(item), item.field, $event!)\"\n >\n <DouDropdownItem\n v-for=\"option in groupingsFor(item.field)\"\n :key=\"option.key\"\n :value=\"option.key\"\n >{{ option.text }}</DouDropdownItem\n >\n </DouDropdownRadioGroup>\n </DouDropdownSub>\n <DouDropdownSub :title=\"messages.fields.top\">\n <DouDropdownRadioGroup\n :model-value=\"topKey(item.field)\"\n @update:model-value=\"onTop(item, $event!)\"\n >\n <DouDropdownItem\n v-for=\"option in topOptions(item.field)\"\n :key=\"option.key\"\n :value=\"option.key\"\n :divided=\"option.key === 'b5'\"\n >{{ option.text }}</DouDropdownItem\n >\n </DouDropdownRadioGroup>\n <DouDropdownItem divided @click=\"openTop(item)\">{{\n messages.fields.customTop\n }}</DouDropdownItem>\n </DouDropdownSub>\n <DouDropdownItem\n :checked=\"dimensionOf(axisOf(item), item.field)?.subtotal !== false\"\n @update:checked=\"onSubtotal(axisOf(item), item.field, $event)\"\n >{{ messages.fields.subtotal }}</DouDropdownItem\n >\n </template>\n <template v-if=\"item.kind === 'value'\">\n <DouDropdownItem v-if=\"isFormula(item)\" @click=\"openFormula(item.index)\">{{\n messages.fields.editFormula\n }}</DouDropdownItem>\n <DouDropdownSub v-else :title=\"messages.fields.aggregate\">\n <DouDropdownRadioGroup\n :model-value=\"currentAggregate(item.index)\"\n @update:model-value=\"onAggregate(item.index, $event!)\"\n >\n <DouDropdownItem\n v-for=\"name in aggregatesFor(item.field)\"\n :key=\"name\"\n :value=\"name\"\n >{{ aggregateText(name) }}</DouDropdownItem\n >\n </DouDropdownRadioGroup>\n </DouDropdownSub>\n <DouDropdownSub :title=\"messages.fields.showAs\">\n <DouDropdownRadioGroup\n :model-value=\"showKey(item.index)\"\n @update:model-value=\"onShow(item.index, $event!)\"\n >\n <DouDropdownItem value=\"\">{{\n messages.fields.showPlain\n }}</DouDropdownItem>\n <DouDropdownItem\n v-for=\"(show, i) in SHOWS\"\n :key=\"show\"\n :value=\"show\"\n :divided=\"i === 0 || show === 'difference'\"\n >{{ messages.shows[show] }}</DouDropdownItem\n >\n </DouDropdownRadioGroup>\n </DouDropdownSub>\n <DouDropdownSub :title=\"messages.fields.mark\">\n <DouDropdownRadioGroup\n :model-value=\"markKey(item.index)\"\n @update:model-value=\"onMark(item.index, $event!)\"\n >\n <DouDropdownItem value=\"\">{{ messages.fields.noMark }}</DouDropdownItem>\n <DouDropdownItem value=\"bar\">{{ messages.fields.bar }}</DouDropdownItem>\n <DouDropdownItem value=\"scale\">{{\n messages.fields.scale\n }}</DouDropdownItem>\n </DouDropdownRadioGroup>\n </DouDropdownSub>\n <DouDropdownItem\n v-if=\"(config.values?.length ?? 0) > 1\"\n :checked=\"config.valuesOn === 'rows'\"\n @update:checked=\"\n config = { ...config, valuesOn: $event ? 'rows' : 'columns' }\n \"\n >{{ messages.fields.valuesOnRows }}</DouDropdownItem\n >\n </template>\n <DouDropdownItem v-if=\"canFilter(item)\" @click=\"openFilter(item)\">{{\n messages.fields.filter\n }}</DouDropdownItem>\n <DouDropdownItem\n divided\n :disabled=\"placeOf(item).first\"\n @click=\"shift(item, -1)\"\n >{{ messages.fields.moveUp }}</DouDropdownItem\n >\n <DouDropdownItem :disabled=\"placeOf(item).last\" @click=\"shift(item, 1)\">{{\n messages.fields.moveDown\n }}</DouDropdownItem>\n <DouDropdownSub :title=\"messages.fields.moveTo\">\n <DouDropdownItem\n v-for=\"to in otherAreas(item)\"\n :key=\"to\"\n @click=\"moveTo(item, to)\"\n >{{ areaTitle(to) }}</DouDropdownItem\n >\n </DouDropdownSub>\n <DouDropdownItem tone=\"danger\" @click=\"moveTo(item, null)\">{{\n messages.fields.remove\n }}</DouDropdownItem>\n </template>\n </DouDropdown>\n </div>\n </template>\n <template #empty>\n <p class=\"dou-pivot-fields__drop\">{{ messages.fields.dropHere }}</p>\n </template>\n </DouSortable>\n <DouButton\n v-if=\"area === 'values'\"\n class=\"dou-pivot-fields__add\"\n size=\"sm\"\n variant=\"ghost\"\n @click=\"openFormula(-1)\"\n >\n <template #leading><PlusIcon aria-hidden=\"true\" /></template>\n {{ messages.fields.addFormula }}\n </DouButton>\n </section>\n </div>\n </div>\n </component>\n <FormulaDialog\n v-model:open=\"formulaOpen\"\n :source=\"source as PivotSource<unknown>\"\n :messages=\"messages\"\n :initial=\"formulaInitial\"\n @apply=\"applyFormula\"\n />\n <TopDialog\n v-model:open=\"topOpen\"\n :messages=\"messages\"\n :field=\"topItem ? itemLabel(topItem) : ''\"\n :values=\"areas.values.map((v) => chipText(v))\"\n :filter=\"topItem ? filterOf(topItem.field) : undefined\"\n @apply=\"topItem && setRank(topItem, $event)\"\n />\n </div>\n</template>\n"],"mappings":""}
|