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

Commit e941cd1

Browse files
Marc-Andre-RivetShammamah Hossain
authored and
Shammamah Hossain
committed
Issue 546 - Visible columns and data misalignment in export (#592)
1 parent 187f652 commit e941cd1

File tree

9 files changed

+113
-61
lines changed

9 files changed

+113
-61
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@ All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## [Unreleased]
6+
### Added
7+
[#546](https://github.com/plotly/dash-table/issues/546)
8+
- New prop `export_columns` that takes values `all` or `visible` (default). This prop controls the columns used during export
9+
610
### Fixed
711
[#460](https://github.com/plotly/dash-table/issues/460)
812
- The `datestartswith` relational operator now supports number comparison
913
- Fixed a bug where the implicit operator for columns was `equal` instead of the expected default for the column type
1014

15+
[#546](https://github.com/plotly/dash-table/issues/546)
16+
- Visible columns are used correctly for both header and data rows
17+
1118
[#591](https://github.com/plotly/dash-table/issues/591)
1219
- Fixed row and column selection when multiple tables are present
1320

src/dash-table/components/ControlledTable/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -818,8 +818,9 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
818818
tooltip_duration
819819
);
820820

821-
const { export_format, export_headers, virtual, merge_duplicate_headers } = this.props;
821+
const { export_columns, export_format, export_headers, virtual, merge_duplicate_headers } = this.props;
822822
const buttonProps = {
823+
export_columns,
823824
export_format,
824825
virtual_data: virtual,
825826
columns,

src/dash-table/components/Export/index.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,37 @@
11
import XLSX from 'xlsx';
22
import React from 'react';
3-
import { IDerivedData, Columns } from 'dash-table/components/Table/props';
3+
import { IDerivedData, Columns, ExportHeaders, ExportFormat, ExportColumns } from 'dash-table/components/Table/props';
44
import { createWorkbook, createHeadings, createWorksheet } from './utils';
55
import getHeaderRows from 'dash-table/derived/header/headerRows';
66

77
interface IExportButtonProps {
88
columns: Columns;
9-
export_format: string;
9+
export_columns: ExportColumns;
10+
export_format: ExportFormat;
1011
virtual_data: IDerivedData;
1112
visibleColumns: Columns;
12-
export_headers: string;
13+
export_headers: ExportHeaders;
1314
merge_duplicate_headers: boolean;
1415
}
1516

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

18-
const { columns, export_format, virtual_data, export_headers, visibleColumns, merge_duplicate_headers } = props;
19-
const isFormatSupported = export_format === 'csv' || export_format === 'xlsx';
19+
const { columns, export_columns, export_format, virtual_data, export_headers, visibleColumns, merge_duplicate_headers } = props;
20+
const isFormatSupported = export_format === ExportFormat.Csv || export_format === ExportFormat.Xlsx;
21+
22+
const exportedColumns = export_columns === ExportColumns.Visible ? visibleColumns : columns;
2023

2124
const handleExport = () => {
22-
const columnID = visibleColumns.map(column => column.id);
23-
const columnHeaders = visibleColumns.map(column => column.name);
25+
const columnID = exportedColumns.map(column => column.id);
26+
const columnHeaders = exportedColumns.map(column => column.name);
27+
2428
const maxLength = getHeaderRows(columns);
25-
const heading = (export_headers !== 'none') ? createHeadings(columnHeaders, maxLength) : [];
29+
const heading = (export_headers !== ExportHeaders.None) ? createHeadings(columnHeaders, maxLength) : [];
2630
const ws = createWorksheet(heading, virtual_data.data, columnID, export_headers, merge_duplicate_headers);
2731
const wb = createWorkbook(ws);
28-
if (export_format === 'xlsx') {
32+
if (export_format === ExportFormat.Xlsx) {
2933
XLSX.writeFile(wb, 'Data.xlsx', {bookType: 'xlsx', type: 'buffer'});
30-
} else if (export_format === 'csv') {
34+
} else if (export_format === ExportFormat.Csv) {
3135
XLSX.writeFile(wb, 'Data.csv', {bookType: 'csv', type: 'buffer'});
3236
}
3337
};

src/dash-table/components/Export/utils.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as R from 'ramda';
22
import XLSX from 'xlsx';
3-
import { Data } from 'dash-table/components/Table/props';
3+
import { Data, ExportHeaders } from 'dash-table/components/Table/props';
44

55
interface IMergeObject {
66
s: {r: number, c: number};
77
e: {r: number, c: number};
88
}
99

10-
export function transformMultDimArray(array: (string | string[])[], maxLength: number): string[][] {
10+
export function transformMultiDimArray(array: (string | string[])[], maxLength: number): string[][] {
1111
const newArray: string[][] = array.map(row => {
1212
if (row instanceof Array && row.length < maxLength) {
1313
return row.concat(Array(maxLength - row.length).fill(''));
@@ -53,24 +53,30 @@ export function createWorkbook(ws: XLSX.WorkSheet) {
5353
return wb;
5454
}
5555

56-
export function createWorksheet(heading: string[][], data: Data, columnID: string[], exportHeader: string, mergeDuplicateHeaders: boolean ) {
57-
const ws = XLSX.utils.aoa_to_sheet(heading);
58-
if (exportHeader === 'display' || exportHeader === 'names' || exportHeader === 'none') {
59-
XLSX.utils.sheet_add_json(ws, data, {
60-
header: columnID,
61-
skipHeader: true,
62-
origin: heading.length
63-
});
64-
if (exportHeader === 'display' && mergeDuplicateHeaders) {
56+
export function createWorksheet(heading: string[][], data: Data, columnID: string[], exportHeader: ExportHeaders, mergeDuplicateHeaders: boolean) {
57+
const ws = XLSX.utils.aoa_to_sheet([]);
58+
59+
data = R.map(R.pick(columnID))(data);
60+
61+
if (exportHeader === ExportHeaders.Display || exportHeader === ExportHeaders.Names || exportHeader === ExportHeaders.None) {
62+
XLSX.utils.sheet_add_json(ws, heading, { skipHeader: true });
63+
64+
const contentOptions = heading.length > 0 ?
65+
{ header: columnID, skipHeader: true, origin: heading.length } :
66+
{ skipHeader: true };
67+
68+
XLSX.utils.sheet_add_json(ws, data, contentOptions);
69+
70+
if (exportHeader === ExportHeaders.Display && mergeDuplicateHeaders) {
6571
ws['!merges'] = getMergeRanges(heading);
6672
}
67-
} else if (exportHeader === 'ids') {
73+
} else if (exportHeader === ExportHeaders.Ids) {
6874
XLSX.utils.sheet_add_json(ws, data, { header: columnID });
6975
}
7076
return ws;
7177
}
7278

7379
export function createHeadings(columnHeaders: (string | string[])[], maxLength: number) {
74-
const transformedArray = transformMultDimArray(columnHeaders, maxLength);
80+
const transformedArray = transformMultiDimArray(columnHeaders, maxLength);
7581
return R.transpose(transformedArray);
7682
}

src/dash-table/components/Table/props.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,24 @@ export enum ColumnType {
2323
Datetime = 'datetime'
2424
}
2525

26+
export enum ExportColumns {
27+
All = 'all',
28+
Visible = 'visible'
29+
}
30+
31+
export enum ExportFormat {
32+
Csv = 'csv',
33+
Xlsx = 'xlsx',
34+
None = 'none'
35+
}
36+
37+
export enum ExportHeaders {
38+
Ids = 'ids',
39+
Names = 'names',
40+
None = 'none',
41+
Display = 'display'
42+
}
43+
2644
export enum SortMode {
2745
Single = 'single',
2846
Multi = 'multi'
@@ -327,8 +345,9 @@ interface IDefaultProps {
327345
css: IStylesheetRule[];
328346
data: Data;
329347
editable: boolean;
330-
export_format: 'csv' | 'xlsx' | 'none';
331-
export_headers: 'ids' | 'names' | 'none' | 'display';
348+
export_columns: ExportColumns;
349+
export_format: ExportFormat;
350+
export_headers: ExportHeaders;
332351
fill_width: boolean;
333352
filter_query: string;
334353
filter_action: TableAction;

src/dash-table/dash/DataTable.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export const defaultProps = {
8888
columns: [],
8989
data: [],
9090
editable: false,
91+
export_columns: 'visible',
9192
export_format: 'none',
9293
include_headers_on_copy_paste: false,
9394
selected_cells: [],
@@ -503,6 +504,13 @@ export const propTypes = {
503504
column_id: PropTypes.string
504505
}),
505506

507+
/**
508+
* Denotes the columns that will be used in the export data file.
509+
* If `all`, all columns will be used (visible + hidden). If `visible`,
510+
* only the visible columns will be used. Defaults to `visible`.
511+
*/
512+
export_columns: PropTypes.oneOf(['all', 'visible']),
513+
506514
/**
507515
* Denotes the type of the export data file,
508516
* Defaults to `'none'`

src/dash-table/dash/Sanitizer.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import {
1212
Selection,
1313
SanitizedProps,
1414
SortAsNull,
15-
TableAction
15+
TableAction,
16+
ExportFormat,
17+
ExportHeaders
1618
} from 'dash-table/components/Table/props';
1719
import headerRows from 'dash-table/derived/header/headerRows';
1820
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
@@ -79,10 +81,10 @@ export default class Sanitizer {
7981
const visibleColumns = this.getVisibleColumns(columns, props.hidden_columns);
8082

8183
let headerFormat = props.export_headers;
82-
if (props.export_format === 'xlsx' && R.isNil(headerFormat)) {
83-
headerFormat = 'names';
84-
} else if (props.export_format === 'csv' && R.isNil(headerFormat)) {
85-
headerFormat = 'ids';
84+
if (props.export_format === ExportFormat.Xlsx && R.isNil(headerFormat)) {
85+
headerFormat = ExportHeaders.Names;
86+
} else if (props.export_format === ExportFormat.Csv && R.isNil(headerFormat)) {
87+
headerFormat = ExportHeaders.Ids;
8688
}
8789

8890
return R.merge(props, {

tests/cypress/tests/unit/exportUtils_tests.ts

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,45 @@
1-
import { transformMultDimArray, getMergeRanges, createHeadings, createWorksheet } from 'dash-table/components/Export/utils';
1+
import { transformMultiDimArray, getMergeRanges, createHeadings, createWorksheet } from 'dash-table/components/Export/utils';
22
import * as R from 'ramda';
3+
import { ExportHeaders } from 'dash-table/components/Table/props';
34

45
describe('export', () => {
56

6-
describe('transformMultDimArray', () => {
7+
describe('transformMultiDimArray', () => {
78
it('array with only strings', () => {
89
const testedArray = [];
9-
const transformedArray = transformMultDimArray(testedArray, 0);
10+
const transformedArray = transformMultiDimArray(testedArray, 0);
1011
const expectedArray = [];
1112
expect(transformedArray).to.deep.equal(expectedArray);
1213
});
1314
it('array with only strings', () => {
1415
const testedArray = ['a', 'b', 'c', 'd'];
15-
const transformedArray = transformMultDimArray(testedArray, 0);
16+
const transformedArray = transformMultiDimArray(testedArray, 0);
1617
const expectedArray = [['a'], ['b'], ['c'], ['d']];
1718
expect(transformedArray).to.deep.equal(expectedArray);
1819
});
1920
it ('array with strings and strings array with same length', () => {
2021
const testedArray = ['a', ['b', 'c'], ['b', 'd']];
21-
const transformedArray = transformMultDimArray(testedArray, 2);
22+
const transformedArray = transformMultiDimArray(testedArray, 2);
2223
const expectedArray = [['a', 'a'], ['b', 'c'], ['b', 'd']];
2324
expect(transformedArray).to.deep.equal(expectedArray);
2425
});
2526
it ('2D strings array', () => {
2627
const testedArray = [['a', 'b', 'c'], ['b', 'c', 'd'], ['b', 'd', 'a']];
27-
const transformedArray = transformMultDimArray(testedArray, 3);
28+
const transformedArray = transformMultiDimArray(testedArray, 3);
2829
const expectedArray = [['a', 'b', 'c'], ['b', 'c', 'd'], ['b', 'd', 'a']];
2930
expect(transformedArray).to.deep.equal(expectedArray);
3031

3132
});
3233
it ('multidimensional array', () => {
3334
const testedArray = [['a', 'b'], ['b', 'c', 'd'], ['a', 'b', 'd', 'a']];
34-
const transformedArray = transformMultDimArray(testedArray, 4);
35+
const transformedArray = transformMultiDimArray(testedArray, 4);
3536
const expectedArray = [['a', 'b', '', ''], ['b', 'c', 'd', ''], ['a', 'b', 'd', 'a']];
3637
expect(transformedArray).to.deep.equal(expectedArray);
3738

3839
});
3940
it ('multidimensional array with strings', () => {
4041
const testedArray = ['rows', ['a', 'b'], ['b', 'c', 'd'], ['a', 'b', 'd', 'a']];
41-
const transformedArray = transformMultDimArray(testedArray, 4);
42+
const transformedArray = transformMultiDimArray(testedArray, 4);
4243
const expectedArray = [['rows', 'rows', 'rows', 'rows'], ['a', 'b', '', ''], ['b', 'c', 'd', ''], ['a', 'b', 'd', 'a']];
4344
expect(transformedArray).to.deep.equal(expectedArray);
4445
});
@@ -210,20 +211,24 @@ describe('export', () => {
210211
});
211212

212213
describe('createWorksheet ', () => {
213-
const Headings = [['rows', 'rows', 'b'],
214-
['rows', 'c', 'c'],
215-
['rows', 'e', 'f'],
216-
['rows', 'rows', 'rows']];
214+
const Headings = [
215+
['rows', 'rows', 'b'],
216+
['rows', 'c', 'c'],
217+
['rows', 'e', 'f'],
218+
['rows', 'rows', 'rows']
219+
];
220+
217221
const data = [
218-
{col1: 1, col2: 2, col3: 3},
219-
{col1: 2, col2: 3, col3: 4},
220-
{col1: 1, col2: 2, col3: 3}
222+
{ col1: 1, col2: 2, col3: 'x', col4: 3 },
223+
{ col1: 2, col2: 3, col3: 'x', col4: 4 },
224+
{ col1: 1, col2: 2, col3: 'x', col4: 3 }
221225
];
222-
const columnID = ['col1', 'col2', 'col3'];
226+
227+
const columnID = ['col1', 'col2', 'col4'];
223228
it('create sheet with column names as headers for name or display header mode', () => {
224-
const wsName = createWorksheet(Headings, data, columnID, 'names', true);
225-
const wsDisplay = createWorksheet(Headings, data, columnID, 'display', true);
226-
const wsDisplayNoMerge = createWorksheet(Headings, data, columnID, 'display', false);
229+
const wsName = createWorksheet(Headings, data, columnID, ExportHeaders.Names, true);
230+
const wsDisplay = createWorksheet(Headings, data, columnID, ExportHeaders.Display, true);
231+
const wsDisplayNoMerge = createWorksheet(Headings, data, columnID, ExportHeaders.Display, false);
227232
const expectedWS = {
228233
A1: {t: 's', v: 'rows'},
229234
A2: {t: 's', v: 'rows'},
@@ -256,7 +261,7 @@ describe('export', () => {
256261
expect(wsDisplay).to.deep.equal(expectedWSDisplay);
257262
});
258263
it('create sheet with column ids as headers', () => {
259-
const ws = createWorksheet(Headings, data, columnID, 'ids', true);
264+
const ws = createWorksheet(Headings, data, columnID, ExportHeaders.Ids, true);
260265
const expectedWS = {
261266
A1: {t: 's', v: 'col1'},
262267
A2: {t: 'n', v: 1},
@@ -266,15 +271,15 @@ describe('export', () => {
266271
B2: {t: 'n', v: 2},
267272
B3: {t: 'n', v: 3},
268273
B4: {t: 'n', v: 2},
269-
C1: {t: 's', v: 'col3'},
274+
C1: {t: 's', v: 'col4'},
270275
C2: {t: 'n', v: 3},
271276
C3: {t: 'n', v: 4},
272277
C4: {t: 'n', v: 3}};
273278
expectedWS['!ref'] = 'A1:C4';
274279
expect(ws).to.deep.equal(expectedWS);
275280
});
276281
it('create sheet with no headers', () => {
277-
const ws = createWorksheet([], data, columnID, 'none', true);
282+
const ws = createWorksheet([], data, columnID, ExportHeaders.None, true);
278283
const expectedWS = {
279284
A1: {t: 'n', v: 1},
280285
A2: {t: 'n', v: 2},
@@ -290,11 +295,11 @@ describe('export', () => {
290295
});
291296
it('create sheet with undefined column for clearable columns', () => {
292297
const newData = [
293-
{col2: 2, col3: 3},
294-
{col2: 3, col3: 4},
295-
{col2: 2, col3: 3}
298+
{col2: 2, col4: 3},
299+
{col2: 3, col4: 4},
300+
{col2: 2, col4: 3}
296301
];
297-
const ws = createWorksheet(Headings, newData, columnID, 'display', false);
302+
const ws = createWorksheet(Headings, newData, columnID, ExportHeaders.Display, false);
298303
const expectedWS = {A1: {t: 's', v: 'rows'},
299304
A2: {t: 's', v: 'rows'},
300305
A3: {t: 's', v: 'rows'},

tests/visual/percy-storybook/DashTable.percy.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ fixtures.forEach(fixture => {
4646
});
4747

4848
import dataset from './../../assets/gapminder.csv';
49-
import { TableAction } from 'dash-table/components/Table/props';
49+
import { TableAction, ExportFormat } from 'dash-table/components/Table/props';
5050

5151
storiesOf('DashTable/Without Data', module)
5252
.add('with 1 column', () => (<DataTable
@@ -363,13 +363,13 @@ storiesOf('DashTable/Export', module)
363363
setProps={setProps}
364364
data={dataA2J.slice(0, 10)}
365365
columns={columnsA2J.slice(0, 10)}
366-
export_format= {'xlsx'}
366+
export_format= {ExportFormat.Xlsx}
367367
/>))
368368
.add('Export Button for csv file', () => (<DataTable
369369
setProps={setProps}
370370
data={dataA2J.slice(0, 10)}
371371
columns={columnsA2J.slice(0, 10)}
372-
export_format= {'xlsx'}
372+
export_format={ExportFormat.Xlsx}
373373
/>))
374374
.add('No export Button for file formatted not supported', () => (<DataTable
375375
setProps={setProps}
@@ -381,5 +381,5 @@ storiesOf('DashTable/Export', module)
381381
setProps={setProps}
382382
data={dataA2J.slice(0, 10)}
383383
columns={columnsA2J.slice(0, 10)}
384-
export_format= {'none'}
384+
export_format= {ExportFormat.None}
385385
/>));

0 commit comments

Comments
 (0)