Skip to content
This repository was archived by the owner on Jun 4, 2024. It is now read-only.

Ability to export data as csv / xlsx file #499

Merged
merged 70 commits into from
Jul 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
693f588
props
alinastarkov Jun 20, 2019
1e4ca4a
props
alinastarkov Jun 20, 2019
f74d067
export button
alinastarkov Jun 20, 2019
ca489f9
implementation of buttons
alinastarkov Jun 20, 2019
c4bec18
merge cells
alinastarkov Jun 25, 2019
d289a5c
merge headers
alinastarkov Jun 27, 2019
fdabb83
accounts for merge headers
alinastarkov Jun 27, 2019
c75cfff
refactor code
alinastarkov Jun 28, 2019
7adaab4
test cases
alinastarkov Jun 28, 2019
9d346c2
more test cases
alinastarkov Jul 2, 2019
1dd0b8d
unit test cases
alinastarkov Jul 5, 2019
3fe463a
refactor code
alinastarkov Jul 5, 2019
51c98b2
visual test for export button
alinastarkov Jul 5, 2019
b003204
update package.json
alinastarkov Jul 5, 2019
9eafde2
merge master in branch
alinastarkov Jul 8, 2019
5ab5e13
integration test
alinastarkov Jul 9, 2019
416cce6
types for indexforEach
alinastarkov Jul 9, 2019
77ffe91
integration test
alinastarkov Jul 9, 2019
8f51eda
delete files in folder
alinastarkov Jul 9, 2019
8d3c61d
Jul 9, 2019
3c84347
catch assertion error
alinastarkov Jul 9, 2019
89560d0
remove nested props
alinastarkov Jul 9, 2019
d8f9d0d
Jul 9, 2019
22df76f
Merge branch 'ExportTable' into test-export-table
Jul 9, 2019
4c39f59
Jul 10, 2019
894d651
integration test for download
alinastarkov Jul 10, 2019
47a7536
assertion
alinastarkov Jul 10, 2019
3dcc019
add export_header props
alinastarkov Jul 12, 2019
031ed95
Merge branch 'master' of https://github.com/plotly/dash-table into Ex…
alinastarkov Jul 12, 2019
58990d8
change headers API
alinastarkov Jul 12, 2019
616bb99
typo
alinastarkov Jul 12, 2019
4c70ead
new tests for create workbook
alinastarkov Jul 12, 2019
6eadf6d
linting
alinastarkov Jul 12, 2019
950ec0a
revert to original app mode
alinastarkov Jul 12, 2019
af29d99
merge master in branch
alinastarkov Jul 17, 2019
802b8ae
spacing
alinastarkov Jul 17, 2019
94113ef
refactor and reuse already written modules
alinastarkov Jul 17, 2019
ec40276
change maxLength condition
alinastarkov Jul 17, 2019
0314ded
default props
alinastarkov Jul 17, 2019
54d1b42
refactor code
alinastarkov Jul 17, 2019
135bff3
Merge branch 'master' into ExportTable
alinastarkov Jul 17, 2019
2f15e91
linting
alinastarkov Jul 17, 2019
e84a815
linting
alinastarkov Jul 18, 2019
22d5b17
linting
alinastarkov Jul 18, 2019
ac659fa
wrong file path
alinastarkov Jul 18, 2019
49e1d28
take 10 rows instead of 100
alinastarkov Jul 18, 2019
c049426
syntax changes
alinastarkov Jul 18, 2019
3cb51b1
duplicate codes
alinastarkov Jul 18, 2019
c462301
Revert "syntax changes"
alinastarkov Jul 18, 2019
c11f49a
wrong file path
alinastarkov Jul 18, 2019
749503d
get attribute replace name
alinastarkov Jul 18, 2019
6559189
change file path
alinastarkov Jul 18, 2019
3699b14
export_headers instead of export_header
alinastarkov Jul 19, 2019
927c816
switch back to default app mode
alinastarkov Jul 19, 2019
e338436
handle export_headers based on export_format
alinastarkov Jul 19, 2019
c98d31b
add __init__.py files
alinastarkov Jul 19, 2019
9bbcea5
decapitalize variable name
alinastarkov Jul 19, 2019
84e94cb
linting
alinastarkov Jul 19, 2019
8a7215e
rename folder
alinastarkov Jul 19, 2019
3d3ec29
linting
alinastarkov Jul 19, 2019
3e548a3
update to React 16
alinastarkov Jul 22, 2019
c8b448e
change to functional component and use React.memo
alinastarkov Jul 22, 2019
a84b575
add xlrd to requirements.txt
alinastarkov Jul 22, 2019
e709eb8
remove commented codes in index.html
alinastarkov Jul 22, 2019
29caed8
adding 'none' to table props
alinastarkov Jul 22, 2019
aaa66f5
use Data type instead of any[]
alinastarkov Jul 22, 2019
b895a3b
detail description for export_headers
alinastarkov Jul 22, 2019
8c1f669
merge in worksheet
alinastarkov Jul 22, 2019
5de8d15
Merge branch 'master' into ExportTable
alinastarkov Jul 22, 2019
303a3e4
edit CHANGELOG.md
alinastarkov Jul 22, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ jobs:
command: |
. venv/bin/activate
git clone --depth 1 [email protected]:plotly/dash.git dash-main
pip install -e ./dash-main --quiet
pip install -e ./dash-main[testing] --quiet
cd dash-main/dash-renderer && npm install --ignore-scripts && npm run build && pip install -e . && cd ../..

- run:
Expand All @@ -226,7 +226,7 @@ jobs:
name: Run integration tests
command: |
. venv/bin/activate
python -m unittest tests.dash.test_integration
pytest tests/integration


workflows:
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
[#313](https://github.com/plotly/dash-table/issues/313)
- Ability to export table as csv or xlsx file.

[#497](https://github.com/plotly/dash-table/pull/497)
- New `column.clearable` flag that displays a `Ø` action in the column
Accepts a boolean or array of booleans for multi-line headers.
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions dash-main
Submodule dash-main added at 9819e5
7 changes: 3 additions & 4 deletions dash_table/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
</head>
<body>
<div id='root'></div>
<script src='https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react-dom.js'></script>

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

<!-- <script src='https://cdnjs.cloudflare.com/ajax/libs/react/16.4.2/umd/react.development.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.2/umd/react-dom.development.js'></script> -->
<script src="./demo.js"></script>
</body>
</html>
9 changes: 4 additions & 5 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
</head>
<body>
<div id='root'></div>
<script src='https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/react/15.4.2/react-dom.js'></script>

<!-- <script src='https://cdnjs.cloudflare.com/ajax/libs/react/16.4.2/umd/react.development.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.2/umd/react-dom.development.js'></script> -->

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>

<script src="./demo.js"></script>
</body>
</html>
4 changes: 2 additions & 2 deletions index.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
apps = {
filename.replace(".py", "").replace("app_", ""): getattr(
getattr(
__import__(".".join(["tests", "dash", filename.replace(".py", "")])), "dash"
__import__(".".join(["tests", "integration", filename.replace(".py", "")])), "integration"
),
filename.replace(".py", ""),
)
for filename in os.listdir(os.path.join("tests", "dash"))
for filename in os.listdir(os.path.join("tests", "integration"))
if filename.startswith("app_") and filename.endswith(".py")
}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@
"webpack": "^4.36.1",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.7.2",
"webpack-preprocessor": "^0.1.12"
"webpack-preprocessor": "^0.1.12",
"xlsx": "^0.14.3"
},
"files": [
"/dash_table/bundle*{.js,.map}"
Expand Down
4 changes: 1 addition & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ nbformat==4.4.0
numpy==1.15.1
pandas==0.24.1
parso==0.3.1
percy==2.0.0
pexpect==4.6.0
pickleshare==0.7.4
plotly==3.2.1
Expand All @@ -42,9 +41,7 @@ Pygments==2.2.0
pylint==2.1.1
python-dateutil==2.7.3
pytz==2018.5
requests==2.20.0
retrying==1.3.3
selenium==3.14.0
simplegeneric==0.8.1
six==1.11.0
toml==0.9.6
Expand All @@ -54,3 +51,4 @@ urllib3==1.23
wcwidth==0.1.7
Werkzeug==0.14.1
wrapt==1.10.11
xlrd>= 1.0.0
5 changes: 5 additions & 0 deletions src/dash-table/components/ControlledTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isCtrlDown,
isNavKey
} from 'dash-table/utils/unicode';
import ExportButton from 'dash-table/components/Export';
import { selectionBounds, selectionCycle } from 'dash-table/utils/navigation';
import { makeCell, makeSelection } from 'dash-table/derived/cell/cellProps';

Expand Down Expand Up @@ -760,6 +761,9 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
tooltip_duration
);

const { export_format, export_headers, virtual } = this.props;
const buttonProps = { export_format, virtual_data: virtual, columns, export_headers };

return (<div
id={id}
onCopy={this.onCopy}
Expand Down Expand Up @@ -802,6 +806,7 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
<button className='next-page' onClick={this.loadNext}>Next</button>
</div>
)}
<ExportButton {...buttonProps} />
</div>);
}

Expand Down
40 changes: 40 additions & 0 deletions src/dash-table/components/Export/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import XLSX from 'xlsx';
import React from 'react';
import { IDerivedData, IVisibleColumn } from 'dash-table/components/Table/props';
import { createWorkbook, createHeadings, createWorksheet } from './utils';
import getHeaderRows from 'dash-table/derived/header/headerRows';

interface IExportButtonProps {
export_format: string;
virtual_data: IDerivedData;
columns: IVisibleColumn[];
export_headers: string;
}

export default React.memo((props: IExportButtonProps) => {

const { columns, export_format, virtual_data, export_headers } = props;
const isFormatSupported = export_format === 'csv' || export_format === 'xlsx';

const handleExport = () => {
const columnID = columns.map(column => column.id);
const columnHeaders = columns.map(column => column.name);
const maxLength = getHeaderRows(columns);
const heading = (export_headers !== 'none') ? createHeadings(columnHeaders, maxLength) : [];
const ws = createWorksheet(heading, virtual_data.data, columnID, export_headers);
const wb = createWorkbook(ws);
if (export_format === 'xlsx') {
XLSX.writeFile(wb, 'Data.xlsx', {bookType: 'xlsx', type: 'buffer'});
} else if (export_format === 'csv') {
XLSX.writeFile(wb, 'Data.csv', {bookType: 'csv', type: 'buffer'});
}
};

return (
<div>
{ !isFormatSupported ? null : (
<button className='export' onClick={handleExport}>Export</button>
)}
</div>
);
});
75 changes: 75 additions & 0 deletions src/dash-table/components/Export/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as R from 'ramda';
import XLSX from 'xlsx';
import { Data } from 'dash-table/components/Table/props';

interface IMergeObject {
s: {r: number, c: number};
e: {r: number, c: number};
}

export function transformMultDimArray(array: (string | string[])[], maxLength: number): string[][] {
const newArray: string[][] = array.map(row => {
if (row instanceof Array && row.length < maxLength) {
return row.concat(Array(maxLength - row.length).fill(''));
}
if (maxLength === 0 || maxLength === 1) {
return [row];
}
if (row instanceof String || typeof(row) === 'string') {
return Array(maxLength).fill(row);
}
return row;
});
return newArray;
}

export function getMergeRanges(array: string[][]) {
let apiMergeArray: IMergeObject[] = [];
const iForEachOuter = R.addIndex<(string[]), void>(R.forEach);
const iForEachInner = R.addIndex<(string), void>(R.forEach);
iForEachOuter((row: string[], rIndex: number) => {
let dict: any = {};
iForEachInner((cell: string, cIndex: number) => {
if (!dict[cell]) {
dict[cell] = {s: {r: rIndex, c: cIndex}, e: {r: rIndex, c: cIndex }};
} else {
if (cIndex === (dict[cell].e.c + 1)) {
dict[cell].e = {r: rIndex, c: cIndex};
} else {
apiMergeArray.push(dict[cell]);
dict[cell] = {s: {r: rIndex, c: cIndex}, e: {r: rIndex, c: cIndex }};
}
}
}, row);
const objectsToMerge: IMergeObject[] = Object.values(dict);
apiMergeArray = R.concat(apiMergeArray, objectsToMerge );
}, array);
return R.filter((item: IMergeObject) => item.s.c !== item.e.c || item.s.r !== item.e.r, apiMergeArray);
}

export function createWorkbook(ws: XLSX.WorkSheet) {
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'SheetJS');
return wb;
}

export function createWorksheet(heading: string[][], data: Data, columnID: string[], exportHeader: string ) {
const ws = XLSX.utils.aoa_to_sheet(heading);
if (exportHeader === 'display' || exportHeader === 'names' || exportHeader === 'none') {
XLSX.utils.sheet_add_json(ws, data, {
skipHeader: true,
origin: heading.length
});
if (exportHeader === 'display') {
ws['!merges'] = getMergeRanges(heading);
}
} else if (exportHeader === 'ids') {
XLSX.utils.sheet_add_json(ws, data, { header: columnID });
}
return ws;
}

export function createHeadings(columnHeaders: (string | string[])[], maxLength: number) {
const transformedArray = transformMultDimArray(columnHeaders, maxLength);
return R.transpose(transformedArray);
}
2 changes: 2 additions & 0 deletions src/dash-table/components/Table/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,8 @@ interface IDefaultProps {
css: IStylesheetRule[];
data: Data;
editable: boolean;
export_format: 'csv' | 'xlsx' | 'none';
export_headers: 'ids' | 'names' | 'none' | 'display';
fill_width: boolean;
filter_query: string;
filter_action: TableAction;
Expand Down
18 changes: 18 additions & 0 deletions src/dash-table/dash/DataTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const defaultProps = {
data: [],
columns: [],
editable: false,
export_format: 'none',
selected_cells: [],
selected_rows: [],
selected_row_ids: [],
Expand Down Expand Up @@ -435,6 +436,23 @@ export const propTypes = {
column_id: PropTypes.string
}),

/**
* Denotes the type of the export data file,
* Defaults to `'none'`
*/
export_format: PropTypes.oneOf(['csv', 'xlsx', 'none']),

/**
* Denotes the format of the headers in the export data file.
* If `'none'`, there will be no header. If `'display'`, then the header
* of the data file will be be how it is currently displayed. Note that
* `'display'` is only supported for `'xlsx'` export_format and will behave
* like `'names'` for `'csv'` export format. If `'ids'` or `'names'`,
* then the headers of data file will be the column id or the column
* names, respectively
*/
export_headers: PropTypes.oneOf(['none', 'ids', 'names', 'display']),

/**
* `fill_width` toggles between a set of CSS for two common behaviors:
* - True: The table container's width will grow to fill the available space
Expand Down
10 changes: 8 additions & 2 deletions src/dash-table/dash/Sanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,18 @@ const applyDefaultToLocale = (locale: INumberLocale) => getLocale(locale);
export default class Sanitizer {
sanitize(props: PropsWithDefaults): SanitizedProps {
const locale_format = this.applyDefaultToLocale(props.locale_format);

let headerFormat = props.export_headers;
if (props.export_format === 'xlsx' && R.isNil(headerFormat)) {
headerFormat = 'names';
} else if (props.export_format === 'csv' && R.isNil(headerFormat)) {
headerFormat = 'ids';
}
return R.merge(props, {
columns: this.applyDefaultsToColumns(locale_format, props.sort_as_null, props.columns, props.editable),
fixed_columns: getFixedColumns(props.fixed_columns, props.row_deletable, props.row_selectable),
fixed_rows: getFixedRows(props.fixed_rows, props.columns, props.filter_action),
locale_format
locale_format,
export_headers: headerFormat
});
}

Expand Down
1 change: 0 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
from . import dash
1 change: 0 additions & 1 deletion tests/cypress/tests/standalone/edit_headers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import DashTable from 'cypress/DashTable';

import { AppMode } from 'demo/AppMode';

describe(`edit, mode=${AppMode.Typed}`, () => {
Expand Down
Loading