diff --git a/LICENSE b/LICENSE index 1516b64..11dc2ea 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2017 Plotly, Inc +Copyright (c) 2021 Jakub Jagielka Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0081512..7983136 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ +# react-pivottable-grouping + +This is a fork of [react-pivottable](https://react-pivottable.js.org/) with added capacity of grouping and displaying subtotals. +It adds an option `grouping: true` to the possible options. The rest of the API remains unchaged. + +## Preview +See the working [demo](https://jjagielka.github.io/react-pivottable-demo/) here. + +Left image is the default [react-pivottable](https://github.com/plotly/react-pivottable) rendering, while right images shows [react-pivottable-grouping](https://jjagielka.github.com/react-pivottable-grouping) with the default _grouping:true_ enabled. + + +--- + +Original [react-pivottable](https://react-pivottable.js.org/) README.md + # react-pivottable `react-pivottable` is a React-based pivot table library with drag'n'drop @@ -35,25 +50,29 @@ import React from 'react'; import ReactDOM from 'react-dom'; import PivotTableUI from 'react-pivottable/PivotTableUI'; import 'react-pivottable/pivottable.css'; +import 'react-pivottable/grouping.css'; // see documentation for supported input formats -const data = [['attribute', 'attribute2'], ['value1', 'value2']]; +const data = [ + ['attribute', 'attribute2'], + ['value1', 'value2'], +]; class App extends React.Component { - constructor(props) { - super(props); - this.state = props; - } - - render() { - return ( - this.setState(s)} - {...this.state} - /> - ); - } + constructor(props) { + super(props); + this.state = props; + } + + render() { + return ( + this.setState(s)} + {...this.state} + /> + ); + } } ReactDOM.render(, document.body); @@ -79,6 +98,7 @@ To add the Plotly renderers to your app, you can use the following pattern: import React from 'react'; import PivotTableUI from 'react-pivottable/PivotTableUI'; import 'react-pivottable/pivottable.css'; +import 'react-pivottable/grouping.css'; import TableRenderers from 'react-pivottable/TableRenderers'; import Plot from 'react-plotly.js'; import createPlotlyRenderers from 'react-pivottable/PlotlyRenderers'; @@ -87,24 +107,27 @@ import createPlotlyRenderers from 'react-pivottable/PlotlyRenderers'; const PlotlyRenderers = createPlotlyRenderers(Plot); // see documentation for supported input formats -const data = [['attribute', 'attribute2'], ['value1', 'value2']]; +const data = [ + ['attribute', 'attribute2'], + ['value1', 'value2'], +]; class App extends React.Component { - constructor(props) { - super(props); - this.state = props; - } - - render() { - return ( - this.setState(s)} - renderers={Object.assign({}, TableRenderers, PlotlyRenderers)} - {...this.state} - /> - ); - } + constructor(props) { + super(props); + this.state = props; + } + + render() { + return ( + this.setState(s)} + renderers={Object.assign({}, TableRenderers, PlotlyRenderers)} + {...this.state} + /> + ); + } } ReactDOM.render(, document.body); @@ -120,6 +143,7 @@ peer-dependcy warning and handle the dependency injection like this: import React from 'react'; import PivotTableUI from 'react-pivottable/PivotTableUI'; import 'react-pivottable/pivottable.css'; +import 'react-pivottable/grouping.css'; import TableRenderers from 'react-pivottable/TableRenderers'; import createPlotlyComponent from 'react-plotly.js/factory'; import createPlotlyRenderers from 'react-pivottable/PlotlyRenderers'; @@ -131,24 +155,27 @@ const Plot = createPlotlyComponent(window.Plotly); const PlotlyRenderers = createPlotlyRenderers(Plot); // see documentation for supported input formats -const data = [['attribute', 'attribute2'], ['value1', 'value2']]; +const data = [ + ['attribute', 'attribute2'], + ['value1', 'value2'], +]; class App extends React.Component { - constructor(props) { - super(props); - this.state = props; - } - - render() { - return ( - this.setState(s)} - renderers={Object.assign({}, TableRenderers, PlotlyRenderers)} - {...this.state} - /> - ); - } + constructor(props) { + super(props); + this.state = props; + } + + render() { + return ( + this.setState(s)} + renderers={Object.assign({}, TableRenderers, PlotlyRenderers)} + {...this.state} + /> + ); + } } ReactDOM.render(, document.body); @@ -156,10 +183,10 @@ ReactDOM.render(, document.body); ## Properties and layered architecture -* `` - * `` - * `` - * `PivotData(props)` +- `` + - `` + - `` + - `PivotData(props)` The interactive component provided by `react-pivottable` is `PivotTableUI`, but output rendering is delegated to the non-interactive `PivotTable` component, @@ -182,7 +209,7 @@ indication of which layer consumes each, from the bottom up: | `PivotData` | `vals`
array of strings | `[]` | attribute names used as arguments to aggregator (gets passed to aggregator generating function) | | `PivotData` | `aggregators`
object of functions | `aggregators` from `Utilites` | dictionary of generators for aggregation functions in dropdown (see [original PivotTable.js documentation](https://github.com/nicolaskruchten/pivottable/wiki/Aggregators)) | | `PivotData` | `aggregatorName`
string | first key in `aggregators` | key to `aggregators` object specifying the aggregator to use for computations | -| `PivotData` | `valueFilter`
object of arrays of strings | `{}` | object whose keys are attribute names and values are objects of attribute value-boolean pairs which denote records to include or exclude from computation and rendering; used to prepopulate the filter menus that appear on double-click | +| `PivotData` | `valueFilter`
object of arrays of strings | `{}` | object whose keys are attribute names and values are objects of attribute value-boolean pairs which denote records to include or exclude from computation and rendering; used to prepopulate the filter menus that appear on double-click | | `PivotData` | `sorters`
object or function | `{}` | accessed or called with an attribute name and can return a [function which can be used as an argument to `array.sort`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) for output purposes. If no function is returned, the default sorting mechanism is a built-in "natural sort" implementation. Useful for sorting attributes like month names, see [original PivotTable.js example 1](http://nicolas.kruchten.com/pivottable/examples/mps_agg.html) and [original PivotTable.js example 2](http://nicolas.kruchten.com/pivottable/examples/montreal_2014.html). | | `PivotData` | `rowOrder`
string | `"key_a_to_z"` | the order in which row data is provided to the renderer, must be one of `"key_a_to_z"`, `"value_a_to_z"`, `"value_z_to_a"`, ordering by value orders by row total | | `PivotData` | `colOrder`
string | `"key_a_to_z"` | the order in which column data is provided to the renderer, must be one of `"key_a_to_z"`, `"value_a_to_z"`, `"value_z_to_a"`, ordering by value orders by column total | @@ -208,17 +235,17 @@ if the value was the string `"null"`. ```js const data = [ - { - attr1: 'value1_attr1', - attr2: 'value1_attr2', - //... - }, - { - attr1: 'value2_attr1', - attr2: 'value2_attr2', - //... - }, + { + attr1: 'value1_attr1', + attr2: 'value1_attr2', //... + }, + { + attr1: 'value2_attr1', + attr2: 'value2_attr2', + //... + }, + //... ]; ``` @@ -232,10 +259,10 @@ compatible with the output of CSV parsing libraries like PapaParse. ```js const data = [ - ['attr1', 'attr2'], - ['value1_attr1', 'value1_attr2'], - ['value2_attr1', 'value2_attr2'], - //... + ['attr1', 'attr2'], + ['value1_attr1', 'value1_attr2'], + ['value2_attr1', 'value2_attr2'], + //... ]; ``` diff --git a/examples/App.jsx b/examples/App.jsx index ded8dc8..ee63713 100644 --- a/examples/App.jsx +++ b/examples/App.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import tips from './tips'; import {sortAs} from '../src/Utilities'; import TableRenderers from '../src/TableRenderers'; @@ -6,11 +6,49 @@ import createPlotlyComponent from 'react-plotly.js/factory'; import createPlotlyRenderers from '../src/PlotlyRenderers'; import PivotTableUI from '../src/PivotTableUI'; import '../src/pivottable.css'; +import '../src/grouping.css'; import Dropzone from 'react-dropzone'; import Papa from 'papaparse'; const Plot = createPlotlyComponent(window.Plotly); +function Checkbox(props) { + return +} + +function Grouping(props) { + const [disabled, setDisabled] = useState(true); + + const visible = !!props.rendererName && props.rendererName.startsWith('Table'); + + if(!visible) + return
; + + const onChange = e => { + setDisabled(!e.target.checked); + props.onChange(e); + }; + + return
+
+ +
+
+ + + +
+
+
+
+ } + class PivotTableUISmartWrapper extends React.PureComponent { constructor(props) { super(props); @@ -30,7 +68,7 @@ class PivotTableUISmartWrapper extends React.PureComponent { createPlotlyRenderers(Plot) )} {...this.state.pivotState} - onChange={s => this.setState({pivotState: s})} + // onChange={s => this.setState({pivotState: s}))} unusedOrientationCutoff={Infinity} /> ); @@ -44,11 +82,12 @@ export default class App extends React.Component { filename: 'Sample Dataset: Tips', pivotState: { data: tips, - rows: ['Payer Gender'], - cols: ['Party Size'], - aggregatorName: 'Sum over Sum', + rows: ['Payer Gender', "Meal"], + cols: ["Payer Smoker", 'Party Size',], + // aggregatorName: 'Sum over Sum', vals: ['Tip', 'Total Bill'], - rendererName: 'Grouped Column Chart', + // rendererName: 'Grouped Column Chart', + rendererName: 'Table', sorters: { Meal: sortAs(['Lunch', 'Dinner']), 'Day of Week': sortAs([ @@ -111,6 +150,12 @@ export default class App extends React.Component { }); } + onGrouping({target: {name, checked}}) { + var pivotState = Object.assign({}, this.state.pivotState); + pivotState[name] = checked; + this.setState({pivotState}); + } + render() { return (
@@ -149,7 +194,13 @@ export default class App extends React.Component {

{this.state.filename}


- +
+ +
+ this.setState({pivotState: s})}/>
); diff --git a/package.json b/package.json index f4adaf6..2f874a8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "react-pivottable", - "version": "0.11.0", - "description": "A React-based pivot table", + "name": "react-pivottable-grouping", + "version": "0.1.0", + "description": "A React-based pivot table with grouping", "main": "PivotTableUI.js", "files": [ "PivotTable.js", @@ -14,7 +14,8 @@ "PlotlyRenderers.js.map", "TableRenderers.js.map", "Utilities.js.map", - "pivottable.css" + "pivottable.css", + "grouping.css" ], "scripts": { "start": "webpack-dev-server", @@ -24,26 +25,26 @@ "test:prettier:fix": "prettier --write \"src/*.js*\"", "test:jest": "jest", "test": "npm run test:eslint && npm run test:prettier && npm run test:jest", - "clean": "rm -rf __tests__ PivotTable.js* PivotTableUI.js* PlotlyRenderers.js* TableRenderers.js* Utilities.js* pivottable.css", - "build": "npm run clean && cp src/pivottable.css . && babel src --out-dir=. --source-maps --presets=env,react --plugins babel-plugin-add-module-exports", + "clean": "rm -rf __tests__ PivotTable.js* PivotTableUI.js* PlotlyRenderers.js* TableRenderers.js* Utilities.js* pivottable.css grouping.css", + "build": "npm run clean && cp src/pivottable.css . && cp src/grouping.css . && babel src --out-dir=. --source-maps --presets=env,react --plugins babel-plugin-add-module-exports", "doPublish": "npm run build && npm publish", "postpublish": "npm run clean", - "deploy": "webpack -p && mv bundle.js examples && cd examples && git init && git add . && git commit -m build && git push --force git@github.com:plotly/react-pivottable.git master:gh-pages && rm -rf .git bundle.js" + "deploy": "webpack -p && mv bundle.js examples && cd examples && git init && git add . && git commit -m build && git push --force git@github.com:jjagielka/react-pivottable-grouping.git master:gh-pages && rm -rf .git bundle.js" }, "repository": { "type": "git", - "url": "git+https://github.com/plotly/react-pivottable.git" + "url": "git+https://github.com/jjagielka/react-pivottable-grouping.git" }, "keywords": [ "react", "pivottable" ], - "author": "Nicolas Kruchten ", + "author": "Jakub Jagielka ", "license": "MIT", "bugs": { - "url": "https://github.com/plotly/react-pivottable/issues" + "url": "https://github.com/jjagielka/react-pivottable-grouping/issues" }, - "homepage": "https://github.com/plotly/react-pivottable#readme", + "homepage": "https://github.com/jjagielka/react-pivottable-grouping#readme", "dependencies": { "immutability-helper": "^2.3.1", "prop-types": "^15.5.10", diff --git a/src/TableRenderers.jsx b/src/TableRenderers.jsx index 44a2249..04fa594 100644 --- a/src/TableRenderers.jsx +++ b/src/TableRenderers.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import {PivotData} from './Utilities'; // helper function for setting row/col-span in pivotTableRenderer -const spanSize = function(arr, i, j) { +const spanSize = function(arr, i, j, no_loop = false) { let x; if (i !== 0) { let asc, end; @@ -26,7 +26,7 @@ const spanSize = function(arr, i, j) { let asc1, end1; let stop = false; for ( - x = 0, end1 = j, asc1 = end1 >= 0; + x = no_loop ? j : 0, end1 = j, asc1 = end1 >= 0; asc1 ? x <= end1 : x >= end1; asc1 ? x++ : x-- ) { @@ -52,16 +52,37 @@ function redColorScaleGenerator(values) { }; } +const flatKey = arr => arr.join(String.fromCharCode(0)); +const has = (set, arr) => arr.every(set.has, set); +const add = (set, arr) => (arr.forEach(set.add, set) || set); +const remove = (set, arr) => (arr.forEach(set.delete, set) || set); +const toggle = (set, arr) => (has(set, arr) ? remove : add)(set, arr); + function makeRenderer(opts = {}) { class TableRenderer extends React.PureComponent { render() { const pivotData = new PivotData(this.props); const colAttrs = pivotData.props.cols; const rowAttrs = pivotData.props.rows; - const rowKeys = pivotData.getRowKeys(); - const colKeys = pivotData.getColKeys(); + let rowKeys = pivotData.getRowKeys(true); + let colKeys = pivotData.getColKeys(true); const grandTotalAggregator = pivotData.getAggregator([], []); + const grouping = pivotData.props.grouping; + const compactRows = grouping && this.props.compactRows; + // speacial case for spanSize counting (no_loop) + const specialCase = grouping && !this.props.rowGroupBefore; + const folded = (this.state || {}).folded || new Set(); + const isFolded = keys => has(folded, keys.map(flatKey)); + const fold = keys => this.setState({folded: toggle(new Set(folded), keys.map(flatKey))}); + + if(grouping){ + for (const key of folded) { + colKeys = colKeys.filter(colKey => !flatKey(colKey).startsWith(key + String.fromCharCode(0))); + rowKeys = rowKeys.filter(rowKey => !flatKey(rowKey).startsWith(key + String.fromCharCode(0))); + } + } + let valueCellColors = () => {}; let rowTotalColors = () => {}; let colTotalColors = () => {}; @@ -132,16 +153,23 @@ function makeRenderer(opts = {}) { } : null; + const rbClass = grouping? this.props.rowGroupBefore ? "rowGroupBefore" : "rowGroupAfter" : ""; + const cbClass = grouping? this.props.colGroupBefore ? "colGroupBefore" : "colGroupAfter" : ""; + const clickClass = (pred, closed) => pred? " pvtClickable" + (closed? " closed": "") : ""; return ( - +
{colAttrs.map(function(c, j) { + const clickable = grouping && colAttrs.length > j + 1; + const levelKeys = colKeys.filter(x => x.length === j+1); return ( {j === 0 && rowAttrs.length !== 0 && ( + {colKeys.map(function(colKey, i) { const x = spanSize(colKeys, i, j); if (x === -1) { @@ -149,7 +177,7 @@ function makeRenderer(opts = {}) { } return ( @@ -180,8 +209,12 @@ function makeRenderer(opts = {}) { {rowAttrs.length !== 0 && ( {rowAttrs.map(function(r, i) { + const clickable = grouping && rowAttrs.length > i + 1; + const levelKeys = rowKeys.filter(x => x.length === i+1); return ( - ); @@ -196,33 +229,48 @@ function makeRenderer(opts = {}) { {rowKeys.map(function(rowKey, i) { const totalAggregator = pivotData.getAggregator(rowKey, []); + const rowGap = rowAttrs.length - rowKey.length; return ( - + {rowKey.map(function(txt, j) { - const x = spanSize(rowKeys, i, j); + if (compactRows && j < rowKey.length - 1) { + return null; + } + const clickable = grouping && rowAttrs.length > j + 1; + const x = compactRows ? 1 : spanSize(rowKeys, i, j, specialCase); if (x === -1) { return null; } return ( ); })} + {!compactRows && rowGap + ? + : null + } {colKeys.map(function(colKey, j) { const aggregator = pivotData.getAggregator(rowKey, colKey); + const colGap = colAttrs.length - colKey.length; return (
)} - {c} fold(levelKeys): null} + >{c} fold([colKey.slice(0, j + 1)]) : null} > {colKey[j]}
+ fold(levelKeys): null} + key={`rowAttr${i}`}> {r}
fold([rowKey.slice(0, j + 1)]) : null} > {txt} {"Total (" + rowKey[rowKey.length - 1] + ")"} { - // nulls first +const naturalSort = (as = null, bs = null, nulls_first = true) => { + // nulls first or last if (bs !== null && as === null) { - return -1; + return nulls_first ? -1 : 1; } if (as !== null && bs === null) { - return 1; + return nulls_first ? 1 : -1; } // then raw NaNs @@ -133,7 +133,15 @@ const sortAs = function(order) { l_mapping[x.toLowerCase()] = i; } } - return function(a, b) { + return function(a = null, b = null, nulls_first = true) { + if (b !== null && a === null) { + return nulls_first ? -1 : 1; + } + if (a !== null && b === null) { + return nulls_first ? 1 : -1; + } + + if (a in mapping && b in mapping) { return mapping[a] - mapping[b]; } else if (a in mapping) { @@ -147,7 +155,7 @@ const sortAs = function(order) { } else if (b in l_mapping) { return 1; } - return naturalSort(a, b); + return naturalSort(a, b, nulls_first); }; }; @@ -525,6 +533,9 @@ const derivers = { }, }; +// [1,2,3] -> [[1], [1,2], [1,2,3]] +const subarrays = (array) => array.map((d, i) => array.slice(0, i + 1)); + /* Data Model class */ @@ -590,7 +601,7 @@ class PivotData { ); } - arrSort(attrs) { + arrSort(attrs, nulls_first) { let a; const sortersArr = (() => { const result = []; @@ -599,10 +610,12 @@ class PivotData { } return result; })(); + // Why not .map above? + // const sortersArr = Array.from(attrs).map(a => getSort(this.props.sorters, a)); return function(a, b) { for (const i of Object.keys(sortersArr || {})) { const sorter = sortersArr[i]; - const comparison = sorter(a[i], b[i]); + const comparison = sorter(a[i], b[i], nulls_first); if (comparison !== 0) { return comparison; } @@ -623,7 +636,7 @@ class PivotData { this.rowKeys.sort((a, b) => -naturalSort(v(a, []), v(b, []))); break; default: - this.rowKeys.sort(this.arrSort(this.props.rows)); + this.rowKeys.sort(this.arrSort(this.props.rows, this.props.rowGroupBefore)); } switch (this.props.colOrder) { case 'value_a_to_z': @@ -633,64 +646,77 @@ class PivotData { this.colKeys.sort((a, b) => -naturalSort(v([], a), v([], b))); break; default: - this.colKeys.sort(this.arrSort(this.props.cols)); + this.colKeys.sort(this.arrSort(this.props.cols, this.props.colGroupBefore)); } } } - getColKeys() { + getColKeys(all_keys = false) { this.sortKeys(); - return this.colKeys; + return all_keys ? this.colKeys : this.colKeys.filter(x => x.length === this.props.cols.length); } - getRowKeys() { + getRowKeys(all_keys = false) { this.sortKeys(); - return this.rowKeys; + return all_keys ? this.rowKeys : this.rowKeys.filter(x => x.length === this.props.rows.length); } processRecord(record) { // this code is called in a tight loop - const colKey = []; - const rowKey = []; + let colKeys = []; + let rowKeys = []; for (const x of Array.from(this.props.cols)) { - colKey.push(x in record ? record[x] : 'null'); + colKeys.push(x in record ? record[x] : 'null'); } for (const x of Array.from(this.props.rows)) { - rowKey.push(x in record ? record[x] : 'null'); + rowKeys.push(x in record ? record[x] : 'null'); } - const flatRowKey = rowKey.join(String.fromCharCode(0)); - const flatColKey = colKey.join(String.fromCharCode(0)); + + colKeys = this.props.grouping ? subarrays(colKeys) : [colKeys]; + rowKeys = this.props.grouping ? subarrays(rowKeys) : [rowKeys]; this.allTotal.push(record); - if (rowKey.length !== 0) { - if (!this.rowTotals[flatRowKey]) { - this.rowKeys.push(rowKey); - this.rowTotals[flatRowKey] = this.aggregator(this, rowKey, []); - } - this.rowTotals[flatRowKey].push(record); - } + for (const rowKey of rowKeys) { + const flatRowKey = rowKey.join(String.fromCharCode(0)); - if (colKey.length !== 0) { - if (!this.colTotals[flatColKey]) { - this.colKeys.push(colKey); - this.colTotals[flatColKey] = this.aggregator(this, [], colKey); - } - this.colTotals[flatColKey].push(record); - } + for (const colKey of colKeys) { + const flatColKey = colKey.join(String.fromCharCode(0)); - if (colKey.length !== 0 && rowKey.length !== 0) { - if (!this.tree[flatRowKey]) { - this.tree[flatRowKey] = {}; - } - if (!this.tree[flatRowKey][flatColKey]) { - this.tree[flatRowKey][flatColKey] = this.aggregator( - this, - rowKey, - colKey - ); + if (rowKey.length !== 0) { + if (!this.rowTotals[flatRowKey]) { + this.rowKeys.push(rowKey); + this.rowTotals[flatRowKey] = this.aggregator(this, rowKey, []); + } + if (!(this.props.grouping && colKey.length !== 1)) { + this.rowTotals[flatRowKey].push(record); + } + } + + if (colKey.length !== 0) { + if (!this.colTotals[flatColKey]) { + this.colKeys.push(colKey); + this.colTotals[flatColKey] = this.aggregator(this, [], colKey); + } + if (!(this.props.grouping && rowKey.length !== 1)) { + this.colTotals[flatColKey].push(record); + } + } + + if (colKey.length !== 0 && rowKey.length !== 0) { + if (!this.tree[flatRowKey]) { + this.tree[flatRowKey] = {}; + } + if (!this.tree[flatRowKey][flatColKey]) { + this.tree[flatRowKey][flatColKey] = this.aggregator( + this, + rowKey, + colKey + ); + } + this.tree[flatRowKey][flatColKey].push(record); + } } - this.tree[flatRowKey][flatColKey].push(record); } } @@ -783,6 +809,9 @@ PivotData.defaultProps = { rowOrder: 'key_a_to_z', colOrder: 'key_a_to_z', derivedAttributes: {}, + grouping: false, + rowGroupBefore: true, + colGroupBefore: false }; PivotData.propTypes = { @@ -800,6 +829,9 @@ PivotData.propTypes = { derivedAttributes: PropTypes.objectOf(PropTypes.func), rowOrder: PropTypes.oneOf(['key_a_to_z', 'value_a_to_z', 'value_z_to_a']), colOrder: PropTypes.oneOf(['key_a_to_z', 'value_a_to_z', 'value_z_to_a']), + grouping: PropTypes.bool, + rowGroupBefore: PropTypes.bool, + colGroupBefore: PropTypes.bool }; export { diff --git a/src/grouping.css b/src/grouping.css new file mode 100644 index 0000000..6e6dbec --- /dev/null +++ b/src/grouping.css @@ -0,0 +1,111 @@ +:root { + --pvt-row-padding: 5px; + --pvt-row-indent: 20px; +} + +table.pvtTable.rowGroupBefore tbody tr.pvtLevel1 td { + border-top: 1px double #aaa; +} +table.pvtTable.rowGroupBefore tbody tr.pvtLevel2 td { + border-top: 1px double black; +} +table.pvtTable.rowGroupBefore tbody tr.pvtLevel3 td { + border-top: 1px double black; +} +table.pvtTable.rowGroupBefore tbody tr.pvtLevel4 td { + border-top: 2px double black; +} + + +table.pvtTable.rowGroupAfter tbody tr.pvtLevel1 td { + border-bottom: 1px double #aaa; +} +table.pvtTable.rowGroupAfter tbody tr.pvtLevel2 td { + border-bottom: 1px double black; +} +table.pvtTable.rowGroupAfter tbody tr.pvtLevel3 td { + border-bottom: 1px double black; +} +table.pvtTable.rowGroupAfter tbody tr.pvtLevel4 td { + border-bottom: 2px double black; +} + + +table tbody tr.pvtLevel1 td, +table tbody tr td.pvtLevel1 { + font-weight: 700; + background-color: #eee !important; +} +table tbody tr.pvtLevel2 td, +table tbody tr td.pvtLevel2 { + font-weight: 900; + background-color: gainsboro !important; +} +table tbody tr.pvtLevel3 td, +table tbody tr td.pvtLevel3 { + font-weight: 900; + background-color: #ccc !important; +} +table tbody tr.pvtLevel4 td, +table tbody tr td.pvtLevel4 { + font-weight: 900; + background-color: darkgray !important; +} + + +/* tr.pvtData th:not(.pvtSubtotal) { */ +tr.pvtData th:last-of-type { + font-weight: normal; +} + + +table.colGroupAfter td.pvtLevel1 { + border-right: 1px double #aaa; +} +table.colGroupAfter td.pvtLevel2 { + border-right: 1px double black; +} +table.colGroupAfter td.pvtLevel3 { + border-right: 1px double black; +} +table.colGroupAfter td.pvtLevel4 { + border-right: 2px double black; +} + + +table.colGroupBefore td.pvtLevel1 { + border-left: 1px double #aaa; +} +table.colGroupBefore td.pvtLevel2 { + border-left: 1px double black; +} +table.colGroupBefore td.pvtLevel3 { + border-left: 1px double black; +} +table.colGroupBefore td.pvtLevel4 { + border-left: 2px double black; +} + +.pvtTotal, +.pvtGrandTotal { + background-color: #ccc !important; +} + +.pvtClickable { + cursor: pointer; +} + +/* Folding */ +.pvtClickable:not(.closed):before +{ + content: '\25E2'; + padding-right: 5px; + font-size: smaller; +} + +.pvtClickable.closed:before +{ + content: '\25B7'; + padding-right: 5px; + font-size: smaller; +}