pivotgrid-js 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 +29 -0
- package/LICENSE.commercial +60 -0
- package/README.dev.md +247 -0
- package/README.md +253 -0
- package/config/config-editor.css +298 -0
- package/config/config-editor.html +202 -0
- package/config/config-editor.js +687 -0
- package/demo_data/demo-config.js +38 -0
- package/demo_data/demo-data.js +1 -0
- package/dist/pivotgrid.cjs.js +2867 -0
- package/dist/pivotgrid.css +1091 -0
- package/dist/pivotgrid.esm.js +2867 -0
- package/dist/pivotgrid.js +2865 -0
- package/dist/pivotgrid.min.js +18 -0
- package/engine/aggregator.js +193 -0
- package/engine/column-store.js +99 -0
- package/engine/dictionary-encoder.js +30 -0
- package/package.json +50 -0
- package/providers/array-provider.js +255 -0
- package/providers/rest-provider.js +296 -0
- package/server/.env +5 -0
- package/server/README.md +88 -0
- package/server/configs/main_config.json +112 -0
- package/server/connectors/__init__.py +0 -0
- package/server/connectors/__pycache__/postgresql.cpython-312.pyc +0 -0
- package/server/connectors/postgresql.py +34 -0
- package/server/server.py +328 -0
- package/src/field-zones.css +167 -0
- package/src/field-zones.js +344 -0
- package/src/filter-manager.js +290 -0
- package/src/pivot.css +252 -0
- package/src/pivot.js +919 -0
- package/widget/cache-manager.js +253 -0
- package/widget/i18n.js +179 -0
- package/widget/pivot-widget.js +572 -0
- package/widget/widget.css +672 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
var PivotGridLib=(()=>{class r{static ROW_HEIGHT=24;static HEADER_HEIGHT=32;static COL_HEADER_W=200;static COL_W=150;static INDENT=16;static BUFFER=5;constructor({container:e,result:t,rows:s,columns:l,measure:o,fieldDefs:i={},labels:n={}}){this.container=e,this.rows=s,this.columns=l,this.measure=o,this.fieldDefs=i,this._labels=n,this._measureKey=o+"_sum",this._colHeaderW=r.COL_HEADER_W,this._hideSubtotals=!1,this.collapsed=new Set,this.collapsedCols=new Set,this.rowPool=[],this.rendered=new Map,this._applyResult(t),this._mount(),this._renderVisible(),this._bindScroll()}_applyResult(e){this.cells=e.cells,this.colTree=e.colTree,this.colKeys=e.colKeys,this.tree=e.tree,this.grandTotal=e.grandTotal,e.measureKey&&(this._measureKey=e.measureKey),this._buildFlatCols(),this._buildFlatRows()}_buildFlatCols(){if(!this.colTree||!this.colTree.length){this.flatCols=[];return}const e=[],t=this.columns&&this.columns.length>1,s=l=>{for(const o of l)o.children?this.collapsedCols.has(o.code)?e.push({code:o.code,label:o.value,isSubtotal:!0,collapsed:!0}):(s(o.children),t&&!this._hideSubtotals&&e.push({code:o.code,label:"\u2211",isSubtotal:!0,collapsed:!1})):e.push({code:o.code,label:o.value,isSubtotal:!1})};s(this.colTree),this.flatCols=e}_getGroupSpan(e){if(!e.children||this.collapsedCols.has(e.code))return 1;let s=this.columns&&this.columns.length>1&&!this._hideSubtotals?1:0;for(const l of e.children)s+=this._getGroupSpan(l);return s}_colTreeDepth(){if(!this.colTree||!this.colTree.length)return 1;const e=t=>{let s=0;for(const l of t)l.children&&!this.collapsedCols.has(l.code)&&(s=Math.max(s,1+e(l.children)));return s};return 1+e(this.colTree)}_buildFlatRows(){this.flatRows=[];const e=t=>{for(const s of t)this.flatRows.push(s),s.children&&!this.collapsed.has(s.code)&&e(s.children)};this.tree&&e(this.tree),this.flatRows.push({isGrandTotal:!0})}get _headerHeight(){return r.HEADER_HEIGHT*this._colTreeDepth()}_mount(){this.container.innerHTML="",this.container.classList.add("pg-root");const e=this.flatCols.length?this.flatCols:this.colKeys;this.totalWidth=this._colHeaderW+(e.length+1)*r.COL_W,this._mountColHeader(),this._mountScrollArea()}_mountColHeader(){const e=r.HEADER_HEIGHT,t=this._colHeaderW,s=r.COL_W,l=this._colTreeDepth(),o=e*l;this.headerEl=document.createElement("div"),this.headerEl.className="pg-col-header",this.headerEl.style.cssText=`
|
|
2
|
+
position: absolute; top: 0; left: 0;
|
|
3
|
+
width: ${this.totalWidth}px; height: ${o}px;
|
|
4
|
+
background: #fafafa; border-bottom: 1px solid #d0d0d0; z-index: 10;
|
|
5
|
+
`;const i=this._absCell({x:0,y:0,w:t,h:o,text:"",cls:"row-label"});this.rows.forEach((a,d)=>{const h=document.createElement("span"),u=(this.fieldDefs||{})[a]||{};if(h.textContent=u.title||u.label||a,h.style.cssText="cursor:pointer; padding: 0 2px;",h.title=`Expand to "${a}"`,d<this.rows.length-1?h.addEventListener("click",()=>this.expandToDepth(d+1)):h.style.cursor="default",d>0){const f=document.createElement("span");f.textContent=" \u203A ",f.style.color="#ccc",i.appendChild(f)}i.appendChild(h)});const n=document.createElement("div");if(n.className="pg-col-resize-handle",n.style.cssText=`
|
|
6
|
+
position: absolute; top: 0; left: ${t-4}px;
|
|
7
|
+
width: 8px; height: ${o}px;
|
|
8
|
+
cursor: col-resize; z-index: 20;
|
|
9
|
+
`,this.headerEl.appendChild(n),this._bindResizeHandle(n),this.colTree&&this.colTree.length){let a=0;for(const d of this.colTree)a=this._renderColNode(d,0,a,l)}const c=this.flatCols.length?this.flatCols:this.colKeys;this._absCell({x:t+c.length*s,y:0,w:s,h:o,text:this._labels.total||"Total",cls:"total-col"}),this.container.appendChild(this.headerEl)}_renderColNode(e,t,s,l){const o=r.HEADER_HEIGHT,i=this._colHeaderW,n=r.COL_W,c=this.collapsedCols.has(e.code),a=!e.children,d=this._getGroupSpan(e),h=a||c?(l-t)*o:o,u=c?"subtotal-col":a?"":"pg-col-header-group",f=this._absCell({x:i+s*n,y:t*o,w:d*n,h,text:e.value,cls:u});if(e.children){const p=document.createElement("span");p.className="pg-toggle"+(c?" collapsed":""),p.textContent="\u25BE",p.addEventListener("click",_=>{_.stopPropagation(),this._toggleColCollapse(e.code)}),f.insertBefore(p,f.firstChild)}if(!a&&!c){let p=s;for(const _ of e.children)p=this._renderColNode(_,t+1,p,l);if(this.columns&&this.columns.length>1&&!this._hideSubtotals){const _=(l-t-1)*o;_>0&&this._absCell({x:i+(s+d-1)*n,y:(t+1)*o,w:n,h:_,text:"\u2211",cls:"subtotal-col"})}}return s+d}_absCell({x:e,y:t,w:s,h:l,text:o,cls:i}){const n=document.createElement("div");return n.className="pg-col-header-cell"+(i?" "+i:""),n.style.cssText=`
|
|
10
|
+
position: absolute;
|
|
11
|
+
left: ${e}px; top: ${t}px;
|
|
12
|
+
width: ${s}px; height: ${l}px;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
`,n.textContent=o,this.headerEl.appendChild(n),n}_mountScrollArea(){const e=this._headerHeight;this.scrollArea=document.createElement("div"),this.scrollArea.className="pg-scroll",this.scrollArea.style.top=e+"px",this.container.appendChild(this.scrollArea),this.virtualSpace=document.createElement("div"),this.virtualSpace.style.cssText=`
|
|
15
|
+
position: relative;
|
|
16
|
+
width: ${this.totalWidth}px;
|
|
17
|
+
height: ${this.flatRows.length*r.ROW_HEIGHT}px;
|
|
18
|
+
`,this.scrollArea.appendChild(this.virtualSpace)}_renderVisible(){const e=this.scrollArea.clientHeight,t=this.scrollArea.scrollTop,s=r.ROW_HEIGHT,l=r.BUFFER,o=Math.max(0,Math.floor(t/s)-l),i=Math.min(this.flatRows.length-1,Math.ceil((t+e)/s)+l);for(const[n,c]of this.rendered)(n<o||n>i)&&(this.virtualSpace.removeChild(c),this._recycleRow(c),this.rendered.delete(n));for(let n=o;n<=i;n++){if(this.rendered.has(n))continue;const c=this._acquireRow();this._fillRow(c,this.flatRows[n],n),this.virtualSpace.appendChild(c),this.rendered.set(n,c)}}_acquireRow(){if(this.rowPool.length){const t=this.rowPool.pop();return t.className="pg-row",t.removeAttribute("style"),t.innerHTML="",t}const e=document.createElement("div");return e.className="pg-row",e}_recycleRow(e){this.rowPool.push(e)}_fillRow(e,t,s){const l=r.ROW_HEIGHT;if(e.style.top=s*l+"px",e.style.width=this.totalWidth+"px",e.style.height=l+"px",t.isGrandTotal){e.classList.add("grand-total"),this._fillGrandTotalRow(e);return}e.style.background=s%2===0?"#ffffff":"#fcfcfc",this._fillHeaderCell(e,t),this._fillValueCells(e,t)}_fillHeaderCell(e,t){const s=r.ROW_HEIGHT,l=this._colHeaderW,o=r.INDENT,i=document.createElement("div");if(i.className="pg-cell-header",i.style.cssText=`width:${l}px;height:${s}px;padding-left:${8+t.depth*o}px`,t.children){const c=document.createElement("span");c.className="pg-toggle"+(this.collapsed.has(t.code)?" collapsed":""),c.textContent="\u25BE",c.addEventListener("click",a=>{a.stopPropagation(),this._toggleCollapse(t.code)}),i.appendChild(c)}else{const c=document.createElement("span");c.className="pg-toggle-spacer",i.appendChild(c)}const n=document.createElement("span");n.className=`pg-label depth-${Math.min(t.depth,2)}`,n.textContent=t.value,i.appendChild(n),e.appendChild(i)}_fillValueCells(e,t){const s=r.ROW_HEIGHT,l=r.COL_W,o=this.flatCols.length?this.flatCols:this.colKeys;for(const a of o){const d=t.code+"||"+a.code,h=this.cells.get(d),u=document.createElement("div");u.className="pg-cell"+(h==null?" empty":"")+(a.isSubtotal?" subtotal":""),u.style.cssText=`width:${l}px;height:${s}px`,u.textContent=h!=null?this._fmt(h):"\u2014",h!=null&&u.addEventListener("click",()=>this._emitDrillthrough(t,a.code,h)),e.appendChild(u)}const i=t.code+"||__total__",n=this.cells.get(i)||0,c=document.createElement("div");c.className="pg-cell total",c.style.cssText=`width:${l}px;height:${s}px`,c.textContent=this._fmt(n),c.addEventListener("click",()=>this._emitDrillthrough(t,"__total__",n)),e.appendChild(c)}_fillGrandTotalRow(e){const t=r.ROW_HEIGHT,s=this._colHeaderW,l=r.COL_W,o=this.flatCols.length?this.flatCols:this.colKeys,i=document.createElement("div");i.className="pg-cell-header",i.style.cssText=`width:${s}px;height:${t}px;padding-left:8px`;const n=document.createElement("span");n.className="pg-toggle-spacer",i.appendChild(n);const c=document.createElement("span");c.className="pg-label depth-0",c.textContent=this._labels.total||"Total",i.appendChild(c),e.appendChild(i);for(const d of o){const h="__grand__||"+d.code,u=this.cells.get(h)||0,f=document.createElement("div");f.className="pg-cell total"+(d.isSubtotal?" subtotal":""),f.style.cssText=`width:${l}px;height:${t}px`,f.textContent=this._fmt(u),f.addEventListener("click",()=>this._emitDrillthrough({isGrandTotal:!0},d.code,u)),e.appendChild(f)}const a=document.createElement("div");a.className="pg-cell total grand-total-val",a.style.cssText=`width:${l}px;height:${t}px`,a.textContent=this._fmt(this.grandTotal||0),a.addEventListener("click",()=>this._emitDrillthrough({isGrandTotal:!0},"__total__",this.grandTotal)),e.appendChild(a)}_toggleColCollapse(e){if(this.collapsedCols.has(e)){this.collapsedCols.delete(e);const t=this._findColNode(e);if(t?.children)for(const s of t.children)s.children&&this.collapsedCols.add(s.code)}else this.collapsedCols.add(e);this._rebuildCols()}_findColNode(e,t=this.colTree){if(!t)return null;for(const s of t){if(s.code===e)return s;const l=this._findColNode(e,s.children);if(l)return l}return null}toggleSubtotals(e){this._hideSubtotals=!e,this._rebuildCols()}_rebuildCols(){this._buildFlatCols();const e=this.flatCols.length?this.flatCols:this.colKeys;this.totalWidth=this._colHeaderW+(e.length+1)*r.COL_W,this.virtualSpace.style.width=this.totalWidth+"px",this.scrollArea.style.top=this._headerHeight+"px",this.headerEl.remove(),this._mountColHeader(),this.headerEl.style.transform=`translateX(-${this.scrollArea.scrollLeft}px)`,this._redraw()}_redraw(){this.virtualSpace.style.height=this.flatRows.length*r.ROW_HEIGHT+"px";for(const[,e]of this.rendered)this.virtualSpace.removeChild(e),this._recycleRow(e);this.rendered.clear(),this._renderVisible()}_bindScroll(){let e=!1;this.scrollArea.addEventListener("scroll",()=>{this.headerEl.style.transform=`translateX(-${this.scrollArea.scrollLeft}px)`,e||(requestAnimationFrame(()=>{this._renderVisible(),e=!1}),e=!0)})}_emitDrillthrough(e,t,s){const l={};if(!e.isGrandTotal){const o=this._getNodeChain(e);for(let i=0;i<o.length;i++)l[this.rows[i]]=o[i].value}if(t!=="__total__"){const o=t.split("\u2192");for(let i=0;i<o.length;i++)this.columns[i]&&(l[this.columns[i]]=o[i])}this.container.dispatchEvent(new CustomEvent("drillthrough",{bubbles:!0,detail:{context:l,value:s}}))}_getNodeChain(e){const t=[e];if(e.depth===0)return t;const s=this.flatRows.indexOf(e);for(let l=s-1;l>=0;l--){const o=this.flatRows[l];if(!o.isGrandTotal&&o.depth===e.depth-1){if(t.unshift(o),o.depth===0)break;e=o}}return t}_fmt(e){return new Intl.NumberFormat("ru-RU",{maximumFractionDigits:0}).format(e)}_bindResizeHandle(e){e.addEventListener("mousedown",t=>{t.preventDefault();const s=t.clientX,l=this._colHeaderW,o=n=>{const c=Math.max(r.COL_HEADER_W,l+n.clientX-s);this._colHeaderW=c,this._rebuild()},i=()=>{document.removeEventListener("mousemove",o),document.removeEventListener("mouseup",i)};document.addEventListener("mousemove",o),document.addEventListener("mouseup",i)})}_rebuild(){this.headerEl?.remove(),this.headerEl=null,this._buildFlatCols(),this._mountColHeader();for(const[,e]of this.rendered)this._recycleRow(e);this.rendered.clear(),this._renderVisible()}setResult(e,{rows:t,columns:s,measure:l,fieldDefs:o}={}){if(t&&(this.rows=t),s&&(this.columns=s),l&&(this.measure=l),o&&(this.fieldDefs=o),this.collapsedCols.clear(),this._applyResult(e),this.colTree){for(const n of this.colTree)n.children&&this.collapsedCols.add(n.code);this._buildFlatCols()}const i=this.flatCols.length?this.flatCols:this.colKeys;this.totalWidth=this._colHeaderW+(i.length+1)*r.COL_W,this.virtualSpace.style.width=this.totalWidth+"px",this.scrollArea.style.top=this._headerHeight+"px",this.headerEl.remove(),this._mountColHeader(),this._redraw()}collapseAll(){const e=t=>{if(t)for(const s of t)s.children&&(this.collapsed.add(s.code),e(s.children))};e(this.tree),this._buildFlatRows(),this._redraw()}static _detectMaxHeight(){const e=document.createElement("div");e.style.cssText="position:fixed;visibility:hidden;",document.body.appendChild(e);let t=1e6;for(;t<1e8&&(e.style.height=t+"px",!(e.offsetHeight<t));)t*=2;return e.remove(),t/2}static MAX_FLAT_ROWS=Math.floor(r._detectMaxHeight()/r.ROW_HEIGHT);_confirmLargeExpand(e,t,s){const l=(e/1e6).toFixed(1),o=(this._labels.confirmLargeExpand||"Too many rows (~{millions}M). Click OK to expand anyway.").replace("{millions}",l);window.confirm(o)?t():s?.()}_toggleCollapse(e){const t=this.collapsed.has(e);if(t?this.collapsed.delete(e):this.collapsed.add(e),this._buildFlatRows(),t&&this.flatRows.length>r.MAX_FLAT_ROWS){this._confirmLargeExpand(this.flatRows.length,()=>this._redraw(),()=>{this.collapsed.add(e),this._buildFlatRows()});return}this._redraw()}expandToDepth(e){const t=[],s=o=>{for(const i of o)i.children&&(i.depth<e-1?(this.collapsed.delete(i.code),s(i.children)):i.depth===e-1&&t.push(i))};s(this.tree);const l=t.some(o=>!this.collapsed.has(o.code));for(const o of t)l?this.collapsed.add(o.code):this.collapsed.delete(o.code);this._buildFlatRows(),this._redraw()}expandAll(){if(this.collapsed.clear(),this._buildFlatRows(),this.flatRows.length>r.MAX_FLAT_ROWS){this._confirmLargeExpand(this.flatRows.length,()=>this._redraw());return}this._redraw()}expandAllCols(){this.collapsedCols.clear(),this._rebuildCols()}collapseAllCols(){const e=t=>{if(t)for(const s of t)s.children&&(this.collapsedCols.add(s.code),e(s.children))};e(this.colTree),this._rebuildCols()}}})();
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregator
|
|
3
|
+
*
|
|
4
|
+
* Builds the pivot structure from aggRows.
|
|
5
|
+
* Supports fieldDefs for correct sorting of dates and lookup fields.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const agg = new Aggregator();
|
|
9
|
+
*
|
|
10
|
+
* const result = agg.build({
|
|
11
|
+
* rows: ['region', 'month'],
|
|
12
|
+
* columns: ['channel'],
|
|
13
|
+
* measure: 'revenue',
|
|
14
|
+
* func: 'sum',
|
|
15
|
+
* aggRows,
|
|
16
|
+
* fieldDefs: {
|
|
17
|
+
* region: { label: 'region' },
|
|
18
|
+
* month: { label: 'month_name', sortKey: 'month_num' },
|
|
19
|
+
* },
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
class Aggregator {
|
|
24
|
+
|
|
25
|
+
build({ rows, columns, measure, func, aggRows, fieldDefs = {} }) {
|
|
26
|
+
const measureKey = `${measure}_${func}`;
|
|
27
|
+
const cells = new Map();
|
|
28
|
+
let grandTotal = 0;
|
|
29
|
+
const hasColumns = columns && columns.length > 0;
|
|
30
|
+
|
|
31
|
+
// Pre-compute label and sortKey once, outside the loop
|
|
32
|
+
const rowCols = rows.map(f => (fieldDefs[f] || {}).label || f);
|
|
33
|
+
const rowSorts = rows.map(f => (fieldDefs[f] || {}).sortKey || null);
|
|
34
|
+
const colCols = hasColumns ? columns.map(f => (fieldDefs[f] || {}).label || f) : [];
|
|
35
|
+
const colSorts = hasColumns ? columns.map(f => (fieldDefs[f] || {}).sortKey || null) : [];
|
|
36
|
+
|
|
37
|
+
const rowDepth = rows.length;
|
|
38
|
+
const colDepth = colCols.length;
|
|
39
|
+
const rowKeysBuf = new Array(rowDepth);
|
|
40
|
+
const colKeysBuf = new Array(colDepth);
|
|
41
|
+
|
|
42
|
+
// Build tree roots inline — no extra passes over aggRows needed
|
|
43
|
+
const rowRoot = new Map();
|
|
44
|
+
const colRoot = new Map();
|
|
45
|
+
|
|
46
|
+
for (const row of aggRows) {
|
|
47
|
+
const val = Number(row[measureKey]) || 0;
|
|
48
|
+
grandTotal += val;
|
|
49
|
+
|
|
50
|
+
// Row keys + row tree in a single pass
|
|
51
|
+
let rNode = rowRoot;
|
|
52
|
+
for (let d = 0; d < rowDepth; d++) {
|
|
53
|
+
const v = String(row[rowCols[d]] ?? '');
|
|
54
|
+
const sortVal = rowSorts[d] ? row[rowSorts[d]] : v;
|
|
55
|
+
rowKeysBuf[d] = d === 0 ? v : rowKeysBuf[d - 1] + '→' + v;
|
|
56
|
+
if (!rNode.has(v)) rNode.set(v, { sortKey: sortVal, children: new Map() });
|
|
57
|
+
rNode = rNode.get(v).children;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Column keys + column tree in a single pass
|
|
61
|
+
if (hasColumns) {
|
|
62
|
+
let cNode = colRoot;
|
|
63
|
+
for (let d = 0; d < colDepth; d++) {
|
|
64
|
+
const v = String(row[colCols[d]] ?? '');
|
|
65
|
+
const sortVal = colSorts[d] ? row[colSorts[d]] : v;
|
|
66
|
+
colKeysBuf[d] = d === 0 ? v : colKeysBuf[d - 1] + '→' + v;
|
|
67
|
+
if (!cNode.has(v)) cNode.set(v, { sortKey: sortVal, children: new Map() });
|
|
68
|
+
cNode = cNode.get(v).children;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Accumulate cell values
|
|
73
|
+
for (let d = 0; d < rowDepth; d++) {
|
|
74
|
+
const rk = rowKeysBuf[d];
|
|
75
|
+
if (hasColumns) {
|
|
76
|
+
for (let cd = 0; cd < colDepth; cd++) {
|
|
77
|
+
const key = rk + '||' + colKeysBuf[cd];
|
|
78
|
+
cells.set(key, (cells.get(key) || 0) + val);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const totalKey = rk + '||__total__';
|
|
82
|
+
cells.set(totalKey, (cells.get(totalKey) || 0) + val);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (hasColumns) {
|
|
86
|
+
for (let cd = 0; cd < colDepth; cd++) {
|
|
87
|
+
const gtKey = '__grand__||' + colKeysBuf[cd];
|
|
88
|
+
cells.set(gtKey, (cells.get(gtKey) || 0) + val);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Map trees → node arrays
|
|
94
|
+
const toNodes = (map, depth, parentKey, maxDepth) =>
|
|
95
|
+
[...map.entries()]
|
|
96
|
+
.sort(([, a], [, b]) => {
|
|
97
|
+
const av = a.sortKey, bv = b.sortKey;
|
|
98
|
+
if (av !== bv && !isNaN(Number(av)) && !isNaN(Number(bv))) return Number(av) - Number(bv);
|
|
99
|
+
return String(av).localeCompare(String(bv), 'ru');
|
|
100
|
+
})
|
|
101
|
+
.map(([val, data]) => {
|
|
102
|
+
const code = parentKey ? parentKey + '→' + val : val;
|
|
103
|
+
const children = depth + 1 < maxDepth
|
|
104
|
+
? toNodes(data.children, depth + 1, code, maxDepth)
|
|
105
|
+
: null;
|
|
106
|
+
return { value: val, code, depth, children };
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const tree = toNodes(rowRoot, 0, '', rowDepth);
|
|
110
|
+
const colTree = hasColumns ? toNodes(colRoot, 0, '', colDepth) : null;
|
|
111
|
+
const colKeys = hasColumns ? this._flattenColTree(colTree) : [];
|
|
112
|
+
|
|
113
|
+
return { cells, colKeys, colTree, tree, grandTotal };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Get the label value for a field from a row ───────────────────────────
|
|
117
|
+
|
|
118
|
+
_labelVal(row, field, fieldDefs) {
|
|
119
|
+
const def = fieldDefs[field] || {};
|
|
120
|
+
return String(row[def.label || field] ?? '');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Tree ─────────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Builds the tree in a single pass over aggRows.
|
|
127
|
+
* Groups by label, sorts by sortKey (if present).
|
|
128
|
+
*/
|
|
129
|
+
_buildTree(aggRows, levels, fieldDefs = {}) {
|
|
130
|
+
if (!levels.length) return null;
|
|
131
|
+
|
|
132
|
+
// Map<labelValue, { sortKey: value, children: Map }>
|
|
133
|
+
const root = new Map();
|
|
134
|
+
|
|
135
|
+
for (const row of aggRows) {
|
|
136
|
+
let node = root;
|
|
137
|
+
for (let d = 0; d < levels.length; d++) {
|
|
138
|
+
const field = levels[d];
|
|
139
|
+
const def = fieldDefs[field] || {};
|
|
140
|
+
const labelCol = def.label || field;
|
|
141
|
+
const sortCol = def.sortKey || null;
|
|
142
|
+
const labelVal = String(row[labelCol] ?? '');
|
|
143
|
+
const sortVal = sortCol ? row[sortCol] : labelVal;
|
|
144
|
+
|
|
145
|
+
if (!node.has(labelVal)) {
|
|
146
|
+
node.set(labelVal, { sortKey: sortVal, children: new Map() });
|
|
147
|
+
}
|
|
148
|
+
node = node.get(labelVal).children;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Recursively convert Map → node tree
|
|
153
|
+
const toNodes = (map, depth, parentKey) => {
|
|
154
|
+
return [...map.entries()]
|
|
155
|
+
.sort(([, aData], [, bData]) => {
|
|
156
|
+
const a = aData.sortKey;
|
|
157
|
+
const b = bData.sortKey;
|
|
158
|
+
// Numeric sort if both values are numbers
|
|
159
|
+
if (a !== b && !isNaN(Number(a)) && !isNaN(Number(b))) {
|
|
160
|
+
return Number(a) - Number(b);
|
|
161
|
+
}
|
|
162
|
+
return String(a).localeCompare(String(b), 'ru');
|
|
163
|
+
})
|
|
164
|
+
.map(([val, data]) => {
|
|
165
|
+
const code = parentKey ? parentKey + '→' + val : val;
|
|
166
|
+
const children = depth + 1 < levels.length
|
|
167
|
+
? toNodes(data.children, depth + 1, code)
|
|
168
|
+
: null;
|
|
169
|
+
return { value: val, code, depth, children };
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return toNodes(root, 0, '');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Flat list of all column tree nodes
|
|
177
|
+
_flattenColTree(nodes) {
|
|
178
|
+
const result = [];
|
|
179
|
+
const walk = (nodes) => {
|
|
180
|
+
for (const node of nodes) {
|
|
181
|
+
result.push({
|
|
182
|
+
code: node.code,
|
|
183
|
+
label: node.value,
|
|
184
|
+
depth: node.depth,
|
|
185
|
+
hasChildren: !!node.children,
|
|
186
|
+
});
|
|
187
|
+
if (node.children) walk(node.children);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
walk(nodes);
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Columnar row storage backed by TypedArrays.
|
|
3
|
+
* Dimensions → Uint16Array (via DictionaryEncoder),
|
|
4
|
+
* measures → Float64Array.
|
|
5
|
+
* ~10× memory savings compared to an array of objects.
|
|
6
|
+
*/
|
|
7
|
+
class ColumnStore {
|
|
8
|
+
constructor({ dimensions, measures, funcs, capacity }) {
|
|
9
|
+
this.dimensions = dimensions;
|
|
10
|
+
this.capacity = capacity;
|
|
11
|
+
this.length = 0;
|
|
12
|
+
|
|
13
|
+
// Expand revenue → revenue_sum, revenue_avg...
|
|
14
|
+
const expandedMeasures = measures.flatMap(m =>
|
|
15
|
+
funcs.map(fn => `${m}_${fn}`)
|
|
16
|
+
);
|
|
17
|
+
this.measures = expandedMeasures; // overwrite
|
|
18
|
+
|
|
19
|
+
// One encoder per dimension
|
|
20
|
+
this.encoders = {};
|
|
21
|
+
for (const dim of dimensions) {
|
|
22
|
+
this.encoders[dim] = new DictionaryEncoder();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Columns: Uint16 for dimensions, Float64 for measures
|
|
26
|
+
this.dimCols = {};
|
|
27
|
+
this.measCols = {};
|
|
28
|
+
for (const dim of dimensions) this.dimCols[dim] = new Uint16Array(capacity);
|
|
29
|
+
for (const m of expandedMeasures) this.measCols[m] = new Float64Array(capacity);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Returns true if no more rows can be added */
|
|
33
|
+
isFull() {
|
|
34
|
+
return this.length >= this.capacity;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Appends rows to the store.
|
|
39
|
+
* Rows beyond capacity are silently discarded.
|
|
40
|
+
* @param {Object[]} rows
|
|
41
|
+
* @returns {number} number of rows actually added
|
|
42
|
+
*/
|
|
43
|
+
append(rows) {
|
|
44
|
+
const room = this.capacity - this.length;
|
|
45
|
+
const batch = rows.length > room ? rows.slice(0, room) : rows;
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < batch.length; i++) {
|
|
48
|
+
const row = batch[i];
|
|
49
|
+
const idx = this.length + i;
|
|
50
|
+
for (const dim of this.dimensions) {
|
|
51
|
+
this.dimCols[dim][idx] = this.encoders[dim].encode(row[dim]);
|
|
52
|
+
}
|
|
53
|
+
for (const m of this.measures) {
|
|
54
|
+
this.measCols[m][idx] = Number(row[m]) || 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.length += batch.length;
|
|
59
|
+
return batch.length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Returns an iterable view of rows without materialising
|
|
64
|
+
* the full object array — Aggregator traverses data on-the-fly.
|
|
65
|
+
* Supports for…of, .forEach and .length.
|
|
66
|
+
*/
|
|
67
|
+
rows() {
|
|
68
|
+
const { dimensions, measures, dimCols, measCols, encoders, length } = this;
|
|
69
|
+
|
|
70
|
+
const iterable = {
|
|
71
|
+
length,
|
|
72
|
+
|
|
73
|
+
forEach(fn) {
|
|
74
|
+
for (let i = 0; i < length; i++) {
|
|
75
|
+
const row = {};
|
|
76
|
+
for (const dim of dimensions) row[dim] = encoders[dim].decode(dimCols[dim][i]);
|
|
77
|
+
for (const m of measures) row[m] = measCols[m][i];
|
|
78
|
+
fn(row, i);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
[Symbol.iterator]() {
|
|
83
|
+
let i = 0;
|
|
84
|
+
return {
|
|
85
|
+
next() {
|
|
86
|
+
if (i >= length) return { done: true, value: undefined };
|
|
87
|
+
const row = {};
|
|
88
|
+
for (const dim of dimensions) row[dim] = encoders[dim].decode(dimCols[dim][i]);
|
|
89
|
+
for (const m of measures) row[m] = measCols[m][i];
|
|
90
|
+
i++;
|
|
91
|
+
return { done: false, value: row };
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return iterable;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encodes string values of a single column into uint16 indices.
|
|
3
|
+
* Used inside ColumnStore — one instance per dimension.
|
|
4
|
+
*/
|
|
5
|
+
class DictionaryEncoder {
|
|
6
|
+
constructor() {
|
|
7
|
+
this._map = new Map(); // string → index
|
|
8
|
+
this._reverse = []; // index → string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Returns the numeric index for a value, creating it if needed */
|
|
12
|
+
encode(value) {
|
|
13
|
+
const str = String(value);
|
|
14
|
+
if (!this._map.has(str)) {
|
|
15
|
+
const idx = this._reverse.length;
|
|
16
|
+
this._map.set(str, idx);
|
|
17
|
+
this._reverse.push(str);
|
|
18
|
+
}
|
|
19
|
+
return this._map.get(str);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Restores the string value by index */
|
|
23
|
+
decode(index) {
|
|
24
|
+
return this._reverse[index];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get size() {
|
|
28
|
+
return this._reverse.length;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pivotgrid-js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vanilla JS pivot table — no dependencies, no frameworks",
|
|
5
|
+
"author": "Aleksandr Korolev <korolevalexa@gmail.com>",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"homepage": "https://github.com/AlexKorole/pivotgrid",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/AlexKorole/pivotgrid.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"pivot",
|
|
14
|
+
"grid",
|
|
15
|
+
"table",
|
|
16
|
+
"pivot-table",
|
|
17
|
+
"data",
|
|
18
|
+
"analytics",
|
|
19
|
+
"vanilla-js"
|
|
20
|
+
],
|
|
21
|
+
"main": "dist/pivotgrid.cjs.js",
|
|
22
|
+
"module": "dist/pivotgrid.esm.js",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": "./dist/pivotgrid.esm.js",
|
|
26
|
+
"require": "./dist/pivotgrid.cjs.js"
|
|
27
|
+
},
|
|
28
|
+
"./css": "./dist/pivotgrid.css"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"config",
|
|
33
|
+
"src",
|
|
34
|
+
"engine",
|
|
35
|
+
"providers",
|
|
36
|
+
"widget",
|
|
37
|
+
"server",
|
|
38
|
+
"demo_data",
|
|
39
|
+
"LICENSE",
|
|
40
|
+
"LICENSE.commercial",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "node build.js",
|
|
45
|
+
"prepublishOnly": "npm run build"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"esbuild": "^0.20.2"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArrayProvider
|
|
3
|
+
*
|
|
4
|
+
* Provider for local array data.
|
|
5
|
+
* Implements the same interface as RestProvider —
|
|
6
|
+
* used for demo mode without a server.
|
|
7
|
+
*/
|
|
8
|
+
class ArrayProvider {
|
|
9
|
+
|
|
10
|
+
constructor({ data, dimensions, measures, funcs, fields = {},
|
|
11
|
+
cachedDimensions = [], maxCachedRows = 500_000,
|
|
12
|
+
drillthroughQuery = null }) {
|
|
13
|
+
this.data = data;
|
|
14
|
+
this.dimensions = dimensions;
|
|
15
|
+
this.measures = measures;
|
|
16
|
+
this.funcs = funcs;
|
|
17
|
+
this.fields = fields;
|
|
18
|
+
this.maxCachedRows = maxCachedRows;
|
|
19
|
+
this.drillthroughQuery = drillthroughQuery;
|
|
20
|
+
|
|
21
|
+
this._cachedDims = [...cachedDimensions];
|
|
22
|
+
this._store = null;
|
|
23
|
+
this._cacheRows = 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Cache API ──────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
async prefetch() {
|
|
29
|
+
this._store = null;
|
|
30
|
+
this._cacheRows = 0;
|
|
31
|
+
if (!this._cachedDims.length) return;
|
|
32
|
+
|
|
33
|
+
const aggRows = this._groupBy(this._cachedDims);
|
|
34
|
+
this._store = this._makeStore(this._cachedDims, aggRows);
|
|
35
|
+
this._cacheRows = aggRows.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async countRows(logicalFields) {
|
|
39
|
+
if (!logicalFields.length) return 0;
|
|
40
|
+
const keys = new Set();
|
|
41
|
+
for (const row of this.data) {
|
|
42
|
+
keys.add(logicalFields.map(f => this._val(row, f)).join('|'));
|
|
43
|
+
}
|
|
44
|
+
return keys.size;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async refreshCache(newDims) {
|
|
48
|
+
this._cachedDims = [...newDims];
|
|
49
|
+
await this.prefetch();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get cachedDimensions() { return [...this._cachedDims]; }
|
|
53
|
+
get cacheRows() { return this._cacheRows; }
|
|
54
|
+
|
|
55
|
+
// ── Grid data ─────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
getBestRows(requiredDims = [], activeFilters = {}) {
|
|
58
|
+
if (!this._store) return null;
|
|
59
|
+
|
|
60
|
+
const hasAllRequired = requiredDims.every(dim => {
|
|
61
|
+
const col = (this.fields[dim] || {}).label || dim;
|
|
62
|
+
return this._store.dimensions.includes(col);
|
|
63
|
+
});
|
|
64
|
+
if (!hasAllRequired) return null;
|
|
65
|
+
|
|
66
|
+
const filterDims = Object.keys(activeFilters);
|
|
67
|
+
const hasAllFilterDims = filterDims.every(dim => {
|
|
68
|
+
const col = (this.fields[dim] || {}).label || dim;
|
|
69
|
+
return this._store.dimensions.includes(col);
|
|
70
|
+
});
|
|
71
|
+
if (!hasAllFilterDims) return null;
|
|
72
|
+
|
|
73
|
+
const rows = this._store.rows();
|
|
74
|
+
return filterDims.length > 0
|
|
75
|
+
? this._filterRows(rows, activeFilters)
|
|
76
|
+
: rows;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async getRowsForDims(requiredDims, activeFilters = {}) {
|
|
80
|
+
const cached = this.getBestRows(requiredDims, activeFilters);
|
|
81
|
+
if (cached) return { rows: cached, fromCache: true };
|
|
82
|
+
|
|
83
|
+
const rows = this._groupBy(requiredDims, activeFilters);
|
|
84
|
+
return { rows, fromCache: false };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Drillthrough ───────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
async countDistinct(logicalField) {
|
|
90
|
+
const col = (this.fields[logicalField] || {}).label || logicalField;
|
|
91
|
+
const vals = new Set(this.data.map(r => String(r[col] ?? '')));
|
|
92
|
+
return vals.size;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async getDistinctValues(logicalField) {
|
|
96
|
+
const def = this.fields[logicalField] || {};
|
|
97
|
+
const col = def.label || logicalField;
|
|
98
|
+
const sortCol = def.sortKey || col;
|
|
99
|
+
const vals = [...new Set(this.data.map(r => String(r[col] ?? '')))];
|
|
100
|
+
return vals.sort((a, b) => {
|
|
101
|
+
const av = this.data.find(r => String(r[col]) === a)?.[sortCol];
|
|
102
|
+
const bv = this.data.find(r => String(r[col]) === b)?.[sortCol];
|
|
103
|
+
if (av !== undefined && bv !== undefined && !isNaN(Number(av)) && !isNaN(Number(bv))) {
|
|
104
|
+
return Number(av) - Number(bv);
|
|
105
|
+
}
|
|
106
|
+
return String(av ?? a).localeCompare(String(bv ?? b), 'ru');
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async drillthrough({ filters = {}, limit = 200 }) {
|
|
111
|
+
let rows = this.data;
|
|
112
|
+
|
|
113
|
+
// Apply filters
|
|
114
|
+
for (const [dim, val] of Object.entries(filters)) {
|
|
115
|
+
const col = (this.fields[dim] || {}).label || dim;
|
|
116
|
+
rows = rows.filter(r => String(r[col] ?? '') === String(val));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return rows.slice(0, limit);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Aggregation ───────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
_groupBy(logicalFields, activeFilters = {}) {
|
|
125
|
+
// Filter source data
|
|
126
|
+
let data = this.data;
|
|
127
|
+
if (Object.keys(activeFilters).length) {
|
|
128
|
+
data = this._filterRawRows(data, activeFilters);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const cols = logicalFields.map(f => (this.fields[f] || {}).label || f);
|
|
132
|
+
const groups = new Map();
|
|
133
|
+
|
|
134
|
+
for (const row of data) {
|
|
135
|
+
const key = cols.map(c => String(row[c] ?? '')).join('|§|');
|
|
136
|
+
if (!groups.has(key)) {
|
|
137
|
+
const entry = {};
|
|
138
|
+
for (const col of cols) entry[col] = row[col];
|
|
139
|
+
// Add sortKey columns if present
|
|
140
|
+
for (const f of logicalFields) {
|
|
141
|
+
const def = this.fields[f] || {};
|
|
142
|
+
if (def.sortKey) entry[def.sortKey] = row[def.sortKey];
|
|
143
|
+
}
|
|
144
|
+
// Initialize aggregates
|
|
145
|
+
for (const m of this.measures) {
|
|
146
|
+
for (const fn of this.funcs) {
|
|
147
|
+
entry[`${m}_${fn}`] = fn === 'min' ? Infinity : fn === 'max' ? -Infinity : 0;
|
|
148
|
+
}
|
|
149
|
+
entry[`__count_${m}`] = 0;
|
|
150
|
+
entry[`__sum2_${m}`] = 0;
|
|
151
|
+
}
|
|
152
|
+
groups.set(key, entry);
|
|
153
|
+
}
|
|
154
|
+
const entry = groups.get(key);
|
|
155
|
+
for (const m of this.measures) {
|
|
156
|
+
const v = Number(row[m]) || 0;
|
|
157
|
+
entry[`__count_${m}`]++;
|
|
158
|
+
entry[`${m}_sum`] = (entry[`${m}_sum`] || 0) + v;
|
|
159
|
+
entry[`${m}_count`]= entry[`__count_${m}`];
|
|
160
|
+
entry[`${m}_min`] = Math.min(entry[`${m}_min`] ?? Infinity, v);
|
|
161
|
+
entry[`${m}_max`] = Math.max(entry[`${m}_max`] ?? -Infinity, v);
|
|
162
|
+
entry[`__sum2_${m}`] += v * v;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Compute avg, stddev, variance
|
|
167
|
+
for (const entry of groups.values()) {
|
|
168
|
+
for (const m of this.measures) {
|
|
169
|
+
const n = entry[`__count_${m}`] || 1;
|
|
170
|
+
const sum = entry[`${m}_sum`] || 0;
|
|
171
|
+
const sum2= entry[`__sum2_${m}`] || 0;
|
|
172
|
+
entry[`${m}_avg`] = sum / n;
|
|
173
|
+
const variance = sum2 / n - (sum / n) ** 2;
|
|
174
|
+
entry[`${m}_variance`] = variance;
|
|
175
|
+
entry[`${m}_stddev`] = Math.sqrt(Math.max(0, variance));
|
|
176
|
+
delete entry[`__count_${m}`];
|
|
177
|
+
delete entry[`__sum2_${m}`];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return [...groups.values()];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
_filterRawRows(data, activeFilters) {
|
|
185
|
+
const predicates = [];
|
|
186
|
+
for (const [dim, filter] of Object.entries(activeFilters)) {
|
|
187
|
+
const col = (this.fields[dim] || {}).label || dim;
|
|
188
|
+
if (filter.values && filter.values.length > 0) {
|
|
189
|
+
const valSet = new Set(filter.values);
|
|
190
|
+
predicates.push(row => valSet.has(String(row[col] ?? '')));
|
|
191
|
+
}
|
|
192
|
+
if (filter.searchText) {
|
|
193
|
+
const text = filter.searchText.toLowerCase();
|
|
194
|
+
predicates.push(filter.searchType === 'starts_with'
|
|
195
|
+
? row => String(row[col] ?? '').toLowerCase().startsWith(text)
|
|
196
|
+
: row => String(row[col] ?? '').toLowerCase().includes(text)
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!predicates.length) return data;
|
|
201
|
+
return data.filter(row => predicates.every(p => p(row)));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
_filterRows(rows, activeFilters) {
|
|
205
|
+
const predicates = [];
|
|
206
|
+
for (const [dim, filter] of Object.entries(activeFilters)) {
|
|
207
|
+
const col = (this.fields[dim] || {}).label || dim;
|
|
208
|
+
if (filter.values && filter.values.length > 0) {
|
|
209
|
+
const valSet = new Set(filter.values);
|
|
210
|
+
predicates.push(row => valSet.has(String(row[col] ?? '')));
|
|
211
|
+
}
|
|
212
|
+
if (filter.searchText) {
|
|
213
|
+
const text = filter.searchText.toLowerCase();
|
|
214
|
+
predicates.push(filter.searchType === 'starts_with'
|
|
215
|
+
? row => String(row[col] ?? '').toLowerCase().startsWith(text)
|
|
216
|
+
: row => String(row[col] ?? '').toLowerCase().includes(text)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (!predicates.length) return rows;
|
|
221
|
+
const filtered = [];
|
|
222
|
+
for (const row of rows) {
|
|
223
|
+
if (predicates.every(p => p(row))) filtered.push(row);
|
|
224
|
+
}
|
|
225
|
+
return filtered;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
_val(row, logicalField) {
|
|
231
|
+
const col = (this.fields[logicalField] || {}).label || logicalField;
|
|
232
|
+
return String(row[col] ?? '');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
_makeStore(logicalFields, rows) {
|
|
236
|
+
const dims = logicalFields.map(f => (this.fields[f] || {}).label || f);
|
|
237
|
+
// Add sortKey columns
|
|
238
|
+
for (const f of logicalFields) {
|
|
239
|
+
const def = this.fields[f] || {};
|
|
240
|
+
if (def.sortKey && !dims.includes(def.sortKey)) dims.push(def.sortKey);
|
|
241
|
+
}
|
|
242
|
+
const store = new ColumnStore({
|
|
243
|
+
dimensions: dims,
|
|
244
|
+
measures: this.measures,
|
|
245
|
+
funcs: this.funcs,
|
|
246
|
+
capacity: this.maxCachedRows,
|
|
247
|
+
});
|
|
248
|
+
store.append(rows);
|
|
249
|
+
return store;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async load() {
|
|
253
|
+
throw new Error('Use prefetch() / getRowsForDims() / drillthrough()');
|
|
254
|
+
}
|
|
255
|
+
}
|