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

Issue 546 - Visible columns and data misalignment in export #592

Merged
merged 4 commits into from
Sep 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
[#546](https://github.com/plotly/dash-table/issues/546)
- New prop `export_columns` that takes values `all` or `visible` (default). This prop controls the columns used during export

### Fixed
[#460](https://github.com/plotly/dash-table/issues/460)
- The `datestartswith` relational operator now supports number comparison
- Fixed a bug where the implicit operator for columns was `equal` instead of the expected default for the column type

[#546](https://github.com/plotly/dash-table/issues/546)
- Visible columns are used correctly for both header and data rows

[#591](https://github.com/plotly/dash-table/issues/591)
- Fixed row and column selection when multiple tables are present

Expand Down
3 changes: 2 additions & 1 deletion src/dash-table/components/ControlledTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -798,8 +798,9 @@ export default class ControlledTable extends PureComponent<ControlledTableProps>
tooltip_duration
);

const { export_format, export_headers, virtual, merge_duplicate_headers } = this.props;
const { export_columns, export_format, export_headers, virtual, merge_duplicate_headers } = this.props;
const buttonProps = {
export_columns,
export_format,
virtual_data: virtual,
columns,
Expand Down
24 changes: 14 additions & 10 deletions src/dash-table/components/Export/index.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
import XLSX from 'xlsx';
import React from 'react';
import { IDerivedData, Columns } from 'dash-table/components/Table/props';
import { IDerivedData, Columns, ExportHeaders, ExportFormat, ExportColumns } from 'dash-table/components/Table/props';
import { createWorkbook, createHeadings, createWorksheet } from './utils';
import getHeaderRows from 'dash-table/derived/header/headerRows';

interface IExportButtonProps {
columns: Columns;
export_format: string;
export_columns: ExportColumns;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated string usage to enums

export_format: ExportFormat;
virtual_data: IDerivedData;
visibleColumns: Columns;
export_headers: string;
export_headers: ExportHeaders;
merge_duplicate_headers: boolean;
}

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

const { columns, export_format, virtual_data, export_headers, visibleColumns, merge_duplicate_headers } = props;
const isFormatSupported = export_format === 'csv' || export_format === 'xlsx';
const { columns, export_columns, export_format, virtual_data, export_headers, visibleColumns, merge_duplicate_headers } = props;
const isFormatSupported = export_format === ExportFormat.Csv || export_format === ExportFormat.Xlsx;

const exportedColumns = export_columns === ExportColumns.Visible ? visibleColumns : columns;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use either visible columns or all columns depending on the prop setting


const handleExport = () => {
const columnID = visibleColumns.map(column => column.id);
const columnHeaders = visibleColumns.map(column => column.name);
const columnID = exportedColumns.map(column => column.id);
const columnHeaders = exportedColumns.map(column => column.name);

const maxLength = getHeaderRows(columns);
const heading = (export_headers !== 'none') ? createHeadings(columnHeaders, maxLength) : [];
const heading = (export_headers !== ExportHeaders.None) ? createHeadings(columnHeaders, maxLength) : [];
const ws = createWorksheet(heading, virtual_data.data, columnID, export_headers, merge_duplicate_headers);
const wb = createWorkbook(ws);
if (export_format === 'xlsx') {
if (export_format === ExportFormat.Xlsx) {
XLSX.writeFile(wb, 'Data.xlsx', {bookType: 'xlsx', type: 'buffer'});
} else if (export_format === 'csv') {
} else if (export_format === ExportFormat.Csv) {
XLSX.writeFile(wb, 'Data.csv', {bookType: 'csv', type: 'buffer'});
}
};
Expand Down
32 changes: 19 additions & 13 deletions src/dash-table/components/Export/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as R from 'ramda';
import XLSX from 'xlsx';
import { Data } from 'dash-table/components/Table/props';
import { Data, ExportHeaders } 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[][] {
export function transformMultiDimArray(array: (string | string[])[], maxLength: number): string[][] {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

const newArray: string[][] = array.map(row => {
if (row instanceof Array && row.length < maxLength) {
return row.concat(Array(maxLength - row.length).fill(''));
Expand Down Expand Up @@ -53,24 +53,30 @@ export function createWorkbook(ws: XLSX.WorkSheet) {
return wb;
}

export function createWorksheet(heading: string[][], data: Data, columnID: string[], exportHeader: string, mergeDuplicateHeaders: boolean ) {
const ws = XLSX.utils.aoa_to_sheet(heading);
if (exportHeader === 'display' || exportHeader === 'names' || exportHeader === 'none') {
XLSX.utils.sheet_add_json(ws, data, {
header: columnID,
skipHeader: true,
origin: heading.length
});
if (exportHeader === 'display' && mergeDuplicateHeaders) {
export function createWorksheet(heading: string[][], data: Data, columnID: string[], exportHeader: ExportHeaders, mergeDuplicateHeaders: boolean) {
const ws = XLSX.utils.aoa_to_sheet([]);

data = R.map(R.pick(columnID))(data);

if (exportHeader === ExportHeaders.Display || exportHeader === ExportHeaders.Names || exportHeader === ExportHeaders.None) {
XLSX.utils.sheet_add_json(ws, heading, { skipHeader: true });

const contentOptions = heading.length > 0 ?
{ header: columnID, skipHeader: true, origin: heading.length } :
{ skipHeader: true };

XLSX.utils.sheet_add_json(ws, data, contentOptions);

if (exportHeader === ExportHeaders.Display && mergeDuplicateHeaders) {
ws['!merges'] = getMergeRanges(heading);
}
} else if (exportHeader === 'ids') {
} else if (exportHeader === ExportHeaders.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);
const transformedArray = transformMultiDimArray(columnHeaders, maxLength);
return R.transpose(transformedArray);
}
23 changes: 21 additions & 2 deletions src/dash-table/components/Table/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ export enum ColumnType {
Datetime = 'datetime'
}

export enum ExportColumns {
All = 'all',
Visible = 'visible'
}

export enum ExportFormat {
Csv = 'csv',
Xlsx = 'xlsx',
None = 'none'
}

export enum ExportHeaders {
Ids = 'ids',
Names = 'names',
None = 'none',
Display = 'display'
}

export enum SortMode {
Single = 'single',
Multi = 'multi'
Expand Down Expand Up @@ -327,8 +345,9 @@ interface IDefaultProps {
css: IStylesheetRule[];
data: Data;
editable: boolean;
export_format: 'csv' | 'xlsx' | 'none';
export_headers: 'ids' | 'names' | 'none' | 'display';
export_columns: ExportColumns;
export_format: ExportFormat;
export_headers: ExportHeaders;
fill_width: boolean;
filter_query: string;
filter_action: TableAction;
Expand Down
8 changes: 8 additions & 0 deletions src/dash-table/dash/DataTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const defaultProps = {
columns: [],
data: [],
editable: false,
export_columns: 'visible',
export_format: 'none',
include_headers_on_copy_paste: false,
selected_cells: [],
Expand Down Expand Up @@ -503,6 +504,13 @@ export const propTypes = {
column_id: PropTypes.string
}),

/**
* Denotes the columns that will be used in the export data file.
* If `all`, all columns will be used (visible + hidden). If `visible`,
* only the visible columns will be used. Defaults to `visible`.
*/
export_columns: PropTypes.oneOf(['all', 'visible']),

/**
* Denotes the type of the export data file,
* Defaults to `'none'`
Expand Down
12 changes: 7 additions & 5 deletions src/dash-table/dash/Sanitizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {
Selection,
SanitizedProps,
SortAsNull,
TableAction
TableAction,
ExportFormat,
ExportHeaders
} from 'dash-table/components/Table/props';
import headerRows from 'dash-table/derived/header/headerRows';
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
Expand Down Expand Up @@ -79,10 +81,10 @@ export default class Sanitizer {
const visibleColumns = this.getVisibleColumns(columns, props.hidden_columns);

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';
if (props.export_format === ExportFormat.Xlsx && R.isNil(headerFormat)) {
headerFormat = ExportHeaders.Names;
} else if (props.export_format === ExportFormat.Csv && R.isNil(headerFormat)) {
headerFormat = ExportHeaders.Ids;
}

return R.merge(props, {
Expand Down
57 changes: 31 additions & 26 deletions tests/cypress/tests/unit/exportUtils_tests.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,45 @@
import { transformMultDimArray, getMergeRanges, createHeadings, createWorksheet } from 'dash-table/components/Export/utils';
import { transformMultiDimArray, getMergeRanges, createHeadings, createWorksheet } from 'dash-table/components/Export/utils';
import * as R from 'ramda';
import { ExportHeaders } from 'dash-table/components/Table/props';

describe('export', () => {

describe('transformMultDimArray', () => {
describe('transformMultiDimArray', () => {
it('array with only strings', () => {
const testedArray = [];
const transformedArray = transformMultDimArray(testedArray, 0);
const transformedArray = transformMultiDimArray(testedArray, 0);
const expectedArray = [];
expect(transformedArray).to.deep.equal(expectedArray);
});
it('array with only strings', () => {
const testedArray = ['a', 'b', 'c', 'd'];
const transformedArray = transformMultDimArray(testedArray, 0);
const transformedArray = transformMultiDimArray(testedArray, 0);
const expectedArray = [['a'], ['b'], ['c'], ['d']];
expect(transformedArray).to.deep.equal(expectedArray);
});
it ('array with strings and strings array with same length', () => {
const testedArray = ['a', ['b', 'c'], ['b', 'd']];
const transformedArray = transformMultDimArray(testedArray, 2);
const transformedArray = transformMultiDimArray(testedArray, 2);
const expectedArray = [['a', 'a'], ['b', 'c'], ['b', 'd']];
expect(transformedArray).to.deep.equal(expectedArray);
});
it ('2D strings array', () => {
const testedArray = [['a', 'b', 'c'], ['b', 'c', 'd'], ['b', 'd', 'a']];
const transformedArray = transformMultDimArray(testedArray, 3);
const transformedArray = transformMultiDimArray(testedArray, 3);
const expectedArray = [['a', 'b', 'c'], ['b', 'c', 'd'], ['b', 'd', 'a']];
expect(transformedArray).to.deep.equal(expectedArray);

});
it ('multidimensional array', () => {
const testedArray = [['a', 'b'], ['b', 'c', 'd'], ['a', 'b', 'd', 'a']];
const transformedArray = transformMultDimArray(testedArray, 4);
const transformedArray = transformMultiDimArray(testedArray, 4);
const expectedArray = [['a', 'b', '', ''], ['b', 'c', 'd', ''], ['a', 'b', 'd', 'a']];
expect(transformedArray).to.deep.equal(expectedArray);

});
it ('multidimensional array with strings', () => {
const testedArray = ['rows', ['a', 'b'], ['b', 'c', 'd'], ['a', 'b', 'd', 'a']];
const transformedArray = transformMultDimArray(testedArray, 4);
const transformedArray = transformMultiDimArray(testedArray, 4);
const expectedArray = [['rows', 'rows', 'rows', 'rows'], ['a', 'b', '', ''], ['b', 'c', 'd', ''], ['a', 'b', 'd', 'a']];
expect(transformedArray).to.deep.equal(expectedArray);
});
Expand Down Expand Up @@ -210,20 +211,24 @@ describe('export', () => {
});

describe('createWorksheet ', () => {
const Headings = [['rows', 'rows', 'b'],
['rows', 'c', 'c'],
['rows', 'e', 'f'],
['rows', 'rows', 'rows']];
const Headings = [
['rows', 'rows', 'b'],
['rows', 'c', 'c'],
['rows', 'e', 'f'],
['rows', 'rows', 'rows']
];

const data = [
{col1: 1, col2: 2, col3: 3},
{col1: 2, col2: 3, col3: 4},
{col1: 1, col2: 2, col3: 3}
{ col1: 1, col2: 2, col3: 'x', col4: 3 },
{ col1: 2, col2: 3, col3: 'x', col4: 4 },
{ col1: 1, col2: 2, col3: 'x', col4: 3 }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add one unused (hidden) column to the dataframe

];
const columnID = ['col1', 'col2', 'col3'];

const columnID = ['col1', 'col2', 'col4'];
it('create sheet with column names as headers for name or display header mode', () => {
const wsName = createWorksheet(Headings, data, columnID, 'names', true);
const wsDisplay = createWorksheet(Headings, data, columnID, 'display', true);
const wsDisplayNoMerge = createWorksheet(Headings, data, columnID, 'display', false);
const wsName = createWorksheet(Headings, data, columnID, ExportHeaders.Names, true);
const wsDisplay = createWorksheet(Headings, data, columnID, ExportHeaders.Display, true);
const wsDisplayNoMerge = createWorksheet(Headings, data, columnID, ExportHeaders.Display, false);
const expectedWS = {
A1: {t: 's', v: 'rows'},
A2: {t: 's', v: 'rows'},
Expand Down Expand Up @@ -256,7 +261,7 @@ describe('export', () => {
expect(wsDisplay).to.deep.equal(expectedWSDisplay);
});
it('create sheet with column ids as headers', () => {
const ws = createWorksheet(Headings, data, columnID, 'ids', true);
const ws = createWorksheet(Headings, data, columnID, ExportHeaders.Ids, true);
const expectedWS = {
A1: {t: 's', v: 'col1'},
A2: {t: 'n', v: 1},
Expand All @@ -266,15 +271,15 @@ describe('export', () => {
B2: {t: 'n', v: 2},
B3: {t: 'n', v: 3},
B4: {t: 'n', v: 2},
C1: {t: 's', v: 'col3'},
C1: {t: 's', v: 'col4'},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed col3 for col4 above

C2: {t: 'n', v: 3},
C3: {t: 'n', v: 4},
C4: {t: 'n', v: 3}};
expectedWS['!ref'] = 'A1:C4';
expect(ws).to.deep.equal(expectedWS);
});
it('create sheet with no headers', () => {
const ws = createWorksheet([], data, columnID, 'none', true);
const ws = createWorksheet([], data, columnID, ExportHeaders.None, true);
const expectedWS = {
A1: {t: 'n', v: 1},
A2: {t: 'n', v: 2},
Expand All @@ -290,11 +295,11 @@ describe('export', () => {
});
it('create sheet with undefined column for clearable columns', () => {
const newData = [
{col2: 2, col3: 3},
{col2: 3, col3: 4},
{col2: 2, col3: 3}
{col2: 2, col4: 3},
{col2: 3, col4: 4},
{col2: 2, col4: 3}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

col3 became col4 above

];
const ws = createWorksheet(Headings, newData, columnID, 'display', false);
const ws = createWorksheet(Headings, newData, columnID, ExportHeaders.Display, false);
const expectedWS = {A1: {t: 's', v: 'rows'},
A2: {t: 's', v: 'rows'},
A3: {t: 's', v: 'rows'},
Expand Down
8 changes: 4 additions & 4 deletions tests/visual/percy-storybook/DashTable.percy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ fixtures.forEach(fixture => {
});

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

storiesOf('DashTable/Without Data', module)
.add('with 1 column', () => (<DataTable
Expand Down Expand Up @@ -363,13 +363,13 @@ storiesOf('DashTable/Export', module)
setProps={setProps}
data={dataA2J.slice(0, 10)}
columns={columnsA2J.slice(0, 10)}
export_format= {'xlsx'}
export_format= {ExportFormat.Xlsx}
/>))
.add('Export Button for csv file', () => (<DataTable
setProps={setProps}
data={dataA2J.slice(0, 10)}
columns={columnsA2J.slice(0, 10)}
export_format= {'xlsx'}
export_format={ExportFormat.Xlsx}
/>))
.add('No export Button for file formatted not supported', () => (<DataTable
setProps={setProps}
Expand All @@ -381,5 +381,5 @@ storiesOf('DashTable/Export', module)
setProps={setProps}
data={dataA2J.slice(0, 10)}
columns={columnsA2J.slice(0, 10)}
export_format= {'none'}
export_format= {ExportFormat.None}
/>));