Skip to content

Commit ac8c540

Browse files
first cut at drag'n'drop
1 parent b7e669f commit ac8c540

9 files changed

+198
-23
lines changed

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@
2525
},
2626
"homepage": "https://github.com/plotly/react-pivottable#readme",
2727
"dependencies": {
28+
"immutability-helper": "^2.3.1",
29+
"prop-types": "^15.5.10",
2830
"react": "^15.6.1",
29-
"react-dom": "^15.6.1"
31+
"react-dom": "^15.6.1",
32+
"react-sortablejs": "^1.3.4",
33+
"sortablejs": "^1.6.1"
3034
},
3135
"devDependencies": {
3236
"@storybook/addon-actions": "^3.2.6",

src/DnDCell.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import Sortable from 'react-sortablejs';
4+
import DraggableAttribute from './DraggableAttribute';
5+
6+
class DnDCell extends React.Component {
7+
render() {
8+
return <Sortable
9+
options={{
10+
group: 'shared',
11+
ghostClass: 'pvtPlaceholder'
12+
}}
13+
tag="td"
14+
className={this.props.classes}
15+
onChange={this.props.onChange}
16+
>
17+
{this.props.items.map(x => <DraggableAttribute name={x} key={x} />)}
18+
</Sortable>;
19+
}
20+
}
21+
22+
DnDCell.propTypes = {
23+
items: PropTypes.arrayOf(PropTypes.string).isRequired,
24+
classes: PropTypes.string.isRequired,
25+
onChange: PropTypes.func.isRequired
26+
};
27+
28+
export default DnDCell;

src/DraggableAttribute.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
class DraggableAttribute extends React.Component {
5+
render() {
6+
return <li data-id={this.props.name}>
7+
<span className="pvtAttr">{this.props.name}</span>
8+
</li>;
9+
}
10+
}
11+
12+
DraggableAttribute.propTypes = {
13+
name: PropTypes.string.isRequired
14+
};
15+
16+
export default DraggableAttribute;

src/PivotTable.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import React, {Component} from 'react';
2-
import {PivotData} from '../src/Utilities';
3-
import TableRenderer from '../src/TableRenderer';
1+
import React from 'react';
2+
import {PivotData} from './Utilities';
3+
import TableRenderer from './TableRenderer';
44

55

6-
export default class PivotTable extends Component {
6+
export default class PivotTable extends React.Component {
77
render() {
88
return <TableRenderer pivotData={new PivotData(this.props)} />;
99
}

src/PivotTableUI.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import update from 'immutability-helper';
4+
import {PivotData} from './Utilities';
5+
import DnDCell from './DnDCell';
6+
import TableRenderer from './TableRenderer';
7+
import './pivottable.css';
8+
9+
10+
class PivotTableUI extends React.Component {
11+
componentWillMount() {
12+
this.materializeInput(this.props.data);
13+
}
14+
15+
componentWillUpdate(nextProps) {
16+
this.materializeInput(nextProps.data);
17+
}
18+
19+
materializeInput(nextData) {
20+
if (this.data === nextData) { return; }
21+
this.data = nextData;
22+
const attrValues = {};
23+
const materializedInput = [];
24+
let recordsProcessed = 0;
25+
PivotData.forEachRecord(this.data, {}, function(record) {
26+
materializedInput.push(record);
27+
for (const attr of Object.keys(record)) {
28+
if (!(attr in attrValues)) {
29+
attrValues[attr] = {};
30+
if (recordsProcessed > 0) {
31+
attrValues[attr].null = recordsProcessed;
32+
}
33+
}
34+
}
35+
for (const attr in attrValues) {
36+
const value = attr in record ? record[attr] : 'null';
37+
if (!(value in attrValues[attr])) { attrValues[attr][value] = 0; }
38+
attrValues[attr][value]++;
39+
}
40+
recordsProcessed++;
41+
});
42+
43+
this.materializedInput = materializedInput;
44+
this.attrValues = attrValues;
45+
}
46+
47+
onChange(command) { this.props.onChange(update(this.props, command)); }
48+
49+
render() {
50+
return (
51+
<table className="pvtUi"><tbody>
52+
<tr>
53+
<td>(renderers)</td>
54+
<DnDCell
55+
items={Object.keys(this.attrValues)
56+
.filter(e => !this.props.rows.includes(e) && !this.props.cols.includes(e))}
57+
classes="pvtAxisContainer pvtUnused pvtHorizList"
58+
onChange={function() {}}
59+
/>
60+
</tr>
61+
<tr>
62+
<td>(aggregators)</td>
63+
<DnDCell
64+
items={this.props.cols} classes="pvtAxisContainer pvtHorizList pvtCols"
65+
onChange={newCols => this.onChange({cols: {$set: newCols}})}
66+
/>
67+
</tr>
68+
<tr>
69+
<DnDCell
70+
items={this.props.rows} classes="pvtAxisContainer pvtVertList pvtRows"
71+
onChange={newRows => this.onChange({rows: {$set: newRows}})}
72+
/>
73+
<td>
74+
<TableRenderer pivotData={new PivotData(
75+
update(this.props, {data: {$set: this.materializedInput}}))}
76+
/>
77+
</td>
78+
</tr>
79+
80+
</tbody></table>
81+
);
82+
}
83+
}
84+
85+
PivotTableUI.defaultProps = {
86+
rows: [], cols: []
87+
};
88+
89+
PivotTableUI.propTypes = {
90+
data: PropTypes.oneOfType([PropTypes.array, PropTypes.object, PropTypes.func]).isRequired,
91+
onChange: PropTypes.func.isRequired,
92+
cols: PropTypes.arrayOf(PropTypes.string),
93+
rows: PropTypes.arrayOf(PropTypes.string)
94+
};
95+
96+
97+
export default PivotTableUI;

src/PivotTableUISmartWrapper.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
import PivotTableUI from './PivotTableUI';
3+
4+
5+
export default class PivotTableUISmartWrapper extends React.Component {
6+
constructor(props) {
7+
super(props);
8+
this.state = props;
9+
}
10+
11+
render() {
12+
return (<PivotTableUI {...this.state} onChange={s => this.setState(s)} />);
13+
}
14+
}

src/TableRenderer.jsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import React, {Component} from 'react';
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
23
import './pivottable.css';
34

5+
46
// helper function for setting row/col-span in pivotTableRenderer
57
const spanSize = function(arr, i, j) {
68
let x;
@@ -29,7 +31,7 @@ const spanSize = function(arr, i, j) {
2931
return len;
3032
};
3133

32-
export default class TableRenderer extends Component {
34+
class TableRenderer extends React.Component {
3335
render() {
3436
const pivotData = this.props.pivotData;
3537
const colAttrs = pivotData.colAttrs;
@@ -41,15 +43,15 @@ export default class TableRenderer extends Component {
4143
<table className="pvtTable">
4244
<thead>
4345
{colAttrs.map(function(c, j) { return (
44-
<tr>
46+
<tr key={`colAttr${j}`}>
4547
{(j === 0 && rowAttrs.length !== 0) &&
4648
<th colSpan={rowAttrs.length} rowSpan={colAttrs.length} />
4749
}
4850
<th className="pvtAxisLabel">{c}</th>
4951
{colKeys.map(function(colKey, i) {
5052
const x = spanSize(colKeys, i, j);
5153
if (x === -1) {return null;}
52-
return <th className="pvtColLabel"
54+
return <th className="pvtColLabel" key={`colKey${i}`}
5355
colSpan={x} rowSpan={j === colAttrs.length - 1 && rowAttrs.length !== 0 ? 2 : 1}
5456
>
5557
{colKey[j]}
@@ -66,8 +68,8 @@ export default class TableRenderer extends Component {
6668

6769
{(rowAttrs.length !== 0) &&
6870
<tr>
69-
{rowAttrs.map(function(r) {
70-
return <th className="pvtAxisLabel">{r}</th>;
71+
{rowAttrs.map(function(r, i) {
72+
return <th className="pvtAxisLabel" key={`rowAttr${i}`}>{r}</th>;
7173
})}
7274
<th className="pvtTotalLabel">{colAttrs.length === 0 ? 'Totals' : null}</th>
7375
</tr>
@@ -78,17 +80,18 @@ export default class TableRenderer extends Component {
7880
{rowKeys.map(function(rowKey, i) {
7981
const totalAggregator = pivotData.getAggregator(rowKey, []);
8082
return (
81-
<tr>
83+
<tr key={`rowKeyRow${i}`}>
8284
{rowKey.map(function(txt, j) {
8385
const x = spanSize(rowKeys, i, j);
8486
if (x === -1) {return null;}
85-
return <th className="pvtRowLabel"
87+
return <th key={`rowKeyLabel${i}-${j}`} className="pvtRowLabel"
8688
rowSpan={x} colSpan={j === rowAttrs.length - 1 && colAttrs.length !== 0 ? 2 : 1}
8789
>{txt}</th>;
8890
})}
89-
{colKeys.map(function(colKey) {
91+
{colKeys.map(function(colKey, j) {
9092
const aggregator = pivotData.getAggregator(rowKey, colKey);
91-
return <td className="pvtVal">{aggregator.format(aggregator.value())}</td>;
93+
return <td className="pvtVal" key={`pvtVal${i}-${j}`}>
94+
{aggregator.format(aggregator.value())}</td>;
9295
})}
9396
<td className="pvtTotal">{totalAggregator.format(totalAggregator.value())}</td>
9497
</tr>
@@ -100,9 +103,11 @@ export default class TableRenderer extends Component {
100103
colSpan={rowAttrs.length + (colAttrs.length === 0 ? 0 : 1)}
101104
>Totals</th>
102105

103-
{colKeys.map(function(colKey) {
106+
{colKeys.map(function(colKey, i) {
104107
const totalAggregator = pivotData.getAggregator([], colKey);
105-
return <td className="pvtTotal">{totalAggregator.format(totalAggregator.value())}</td>;
108+
return <td className="pvtTotal" key={`total${i}`}>
109+
{totalAggregator.format(totalAggregator.value())}
110+
</td>;
106111
})}
107112

108113
<td className="pvtGrandTotal">{grandTotalAggregator.format(grandTotalAggregator.value())}</td>
@@ -112,3 +117,9 @@ export default class TableRenderer extends Component {
112117
);
113118
}
114119
}
120+
121+
TableRenderer.propTypes = {
122+
pivotData: PropTypes.object.isRequired
123+
};
124+
125+
export default TableRenderer;

src/pivottable.css

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.pvtUi { color: #333; }
1+
.pvtUi { color: #333; font-family: Verdana; }
22

33

44
table.pvtTable {
@@ -55,6 +55,9 @@ table.pvtTable tbody tr td {
5555
border-radius: 5px;
5656
border: 1px dashed #aaa;
5757
}
58+
.pvtAxisContainer li.pvtPlaceholder span.pvtAttr {
59+
display: none;
60+
}
5861

5962
.pvtAxisContainer li span.pvtAttr {
6063
-webkit-text-size-adjust: 100%;
@@ -72,7 +75,7 @@ table.pvtTable tbody tr td {
7275
color: grey;
7376
}
7477

75-
.pvtHorizList li { display: inline; }
78+
.pvtHorizList li { display: inline-block; }
7679
.pvtVertList { vertical-align: top; }
7780

7881
.pvtFilteredAttribute { font-style: italic }

stories/index.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import React from 'react';
33
import { setAddon, storiesOf } from '@storybook/react';
44

55
import PivotTable from '../src/PivotTable'
6+
import PivotTableUISmartWrapper from '../src/PivotTableUISmartWrapper'
67
import { mps } from './SampleData'
7-
const dataSetHider = (dataset) => (injectRecord) => dataset.map(injectRecord);
88

9-
storiesOf('PivotTable', module)
10-
.add('Canadian MPs 2012 dataset', () => <PivotTable
11-
cols={['Party']} rows={['Province']} data={dataSetHider(mps)} />
9+
storiesOf('Canadian Parliament 2012', module)
10+
.add('PivotTable', () => <PivotTable
11+
cols={['Party']} rows={['Province']} data={(injectRecord) => mps.map(injectRecord)} />
12+
)
13+
.add('PivotTableUISmartWrapper', () => <PivotTableUISmartWrapper data={(injectRecord) => mps.map(injectRecord)}/>
1214
);
1315

1416

0 commit comments

Comments
 (0)