From 85343250c8a823cfbeaf59c2d75b9e6c1da3220a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 15 Feb 2019 16:10:46 -0500 Subject: [PATCH 01/55] - add d3-format 3rd party library - add formatter resolution code - use formatter in cell label - add formatting prop --- package.json | 2 + src/dash-table/DataTable.js | 3 +- src/dash-table/components/Table/props.ts | 1 + src/dash-table/derived/cell/contents.tsx | 105 +++++++++--------- src/dash-table/handlers/cellEvents.ts | 2 +- src/dash-table/{reconcile => type}/any.ts | 0 src/dash-table/{reconcile => type}/date.ts | 2 +- src/dash-table/type/formatter.ts | 18 +++ src/dash-table/{reconcile => type}/null.ts | 2 +- src/dash-table/{reconcile => type}/number.ts | 11 +- .../{reconcile/index.ts => type/reconcile.ts} | 2 +- src/dash-table/{reconcile => type}/text.ts | 2 +- src/dash-table/utils/applyClipboardToData.ts | 2 +- 13 files changed, 94 insertions(+), 58 deletions(-) rename src/dash-table/{reconcile => type}/any.ts (100%) rename src/dash-table/{reconcile => type}/date.ts (98%) create mode 100644 src/dash-table/type/formatter.ts rename src/dash-table/{reconcile => type}/null.ts (92%) rename src/dash-table/{reconcile => type}/number.ts (69%) rename src/dash-table/{reconcile/index.ts => type/reconcile.ts} (99%) rename src/dash-table/{reconcile => type}/text.ts (92%) diff --git a/package.json b/package.json index 88c6ce15a..e20979b3b 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@percy-io/percy-storybook": "^2.1.0", "@storybook/cli": "^4.1.11", "@storybook/react": "^4.1.11", + "@types/d3-format": "^1.3.1", "@types/ramda": "^0.25.50", "@types/react": "^16.8.3", "@types/react-dom": "^16.8.1", @@ -59,6 +60,7 @@ "core-js": "^2.6.5", "css-loader": "^2.1.0", "cypress": "^3.1.5", + "d3-format": "^1.3.2", "fast-isnumeric": "^1.1.2", "file-loader": "^3.0.1", "http-server": "^0.11.1", diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index 35674d45c..00090bf6e 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -274,8 +274,9 @@ export const propTypes = { * behavior. * Stay tuned by following [https://github.com/plotly/dash-table/issues/166](https://github.com/plotly/dash-table/issues/166) */ - type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']) + type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), + formatting: PropTypes.string })), /** diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 35b735166..80a18aa09 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -111,6 +111,7 @@ export interface ITypeColumn { export interface INumberColumn extends ITypeColumn { presentation?: Presentation.Input | Presentation.Dropdown; + formatting?: string; type: ColumnType.Numeric; } diff --git a/src/dash-table/derived/cell/contents.tsx b/src/dash-table/derived/cell/contents.tsx index 6421feef4..21bd73117 100644 --- a/src/dash-table/derived/cell/contents.tsx +++ b/src/dash-table/derived/cell/contents.tsx @@ -19,6 +19,7 @@ import isCellEditable from './isEditable'; import CellLabel from 'dash-table/components/CellLabel'; import CellDropdown from 'dash-table/components/CellDropdown'; import { memoizeOne } from 'core/memoizer'; +import getFormatter from 'dash-table/type/formatter'; const mapData = R.addIndex(R.map); const mapRow = R.addIndex(R.map); @@ -63,58 +64,62 @@ class Contents { editable: boolean, isFocused: boolean, dropdowns: (DropdownValues | undefined)[][] - ): JSX.Element[][] => mapData( - (datum, rowIndex) => mapRow( - (column, columnIndex) => { - const active = isActiveCell(activeCell, rowIndex + offset.rows, columnIndex + offset.columns); + ): JSX.Element[][] => { + const formatters = R.map(getFormatter, columns); - const dropdown = dropdowns[rowIndex][columnIndex]; + return mapData( + (datum, rowIndex) => mapRow( + (column, columnIndex) => { + const active = isActiveCell(activeCell, rowIndex + offset.rows, columnIndex + offset.columns); - const isEditable = isCellEditable(editable, column.editable); + const dropdown = dropdowns[rowIndex][columnIndex]; - const className = [ - ...(active ? ['input-active'] : []), - isFocused ? 'focused' : 'unfocused', - 'dash-cell-value' - ].join(' '); + const isEditable = isCellEditable(editable, column.editable); - switch (getCellType(active, isEditable, dropdown, column.presentation)) { - case CellType.Dropdown: - return (); - case CellType.Input: - return (); - case CellType.Label: - default: - return (); - } - }, - columns - ), - data - )); + const className = [ + ...(active ? ['input-active'] : []), + isFocused ? 'focused' : 'unfocused', + 'dash-cell-value' + ].join(' '); + + switch (getCellType(active, isEditable, dropdown, column.presentation)) { + case CellType.Dropdown: + return (); + case CellType.Input: + return (); + case CellType.Label: + default: + return (); + } + }, + columns + ), + data + ); + }); } \ No newline at end of file diff --git a/src/dash-table/handlers/cellEvents.ts b/src/dash-table/handlers/cellEvents.ts index 0a4e019be..8560fef7c 100644 --- a/src/dash-table/handlers/cellEvents.ts +++ b/src/dash-table/handlers/cellEvents.ts @@ -1,7 +1,7 @@ import * as R from 'ramda'; import { SelectedCells, ICellFactoryProps } from 'dash-table/components/Table/props'; import isActive from 'dash-table/derived/cell/isActive'; -import reconcile from 'dash-table/reconcile'; +import reconcile from 'dash-table/type/reconcile'; function isCellSelected(selectedCells: SelectedCells, idx: number, i: number) { return selectedCells && R.contains([idx, i], selectedCells); diff --git a/src/dash-table/reconcile/any.ts b/src/dash-table/type/any.ts similarity index 100% rename from src/dash-table/reconcile/any.ts rename to src/dash-table/type/any.ts diff --git a/src/dash-table/reconcile/date.ts b/src/dash-table/type/date.ts similarity index 98% rename from src/dash-table/reconcile/date.ts rename to src/dash-table/type/date.ts index e6dd87c77..45a820efe 100644 --- a/src/dash-table/reconcile/date.ts +++ b/src/dash-table/type/date.ts @@ -1,6 +1,6 @@ import { IDatetimeColumn } from 'dash-table/components/Table/props'; import { reconcileNull } from './null'; -import { IReconciliation } from '.'; +import { IReconciliation } from './reconcile'; // pattern and convertToMs pulled from plotly.js // (simplified - no international calendars for now) diff --git a/src/dash-table/type/formatter.ts b/src/dash-table/type/formatter.ts new file mode 100644 index 000000000..dfb6a68a7 --- /dev/null +++ b/src/dash-table/type/formatter.ts @@ -0,0 +1,18 @@ +import { + ColumnType, + IColumnType +} from 'dash-table/components/Table/props'; + +import { getFormatter as getNumberFormatter } from './number'; + +const DEFAULT_FORMATTER = (value: any) => value; +export default (c: IColumnType) => { + let formatter; + switch (c.type) { + case ColumnType.Numeric: + formatter = getNumberFormatter(c); + break; + } + + return formatter || DEFAULT_FORMATTER; +}; \ No newline at end of file diff --git a/src/dash-table/reconcile/null.ts b/src/dash-table/type/null.ts similarity index 92% rename from src/dash-table/reconcile/null.ts rename to src/dash-table/type/null.ts index 582293867..046a48b9f 100644 --- a/src/dash-table/reconcile/null.ts +++ b/src/dash-table/type/null.ts @@ -1,5 +1,5 @@ import { ITypeColumn } from 'dash-table/components/Table/props'; -import { IReconciliation } from '.'; +import { IReconciliation } from './reconcile'; export const reconcileNull = ( value: any, diff --git a/src/dash-table/reconcile/number.ts b/src/dash-table/type/number.ts similarity index 69% rename from src/dash-table/reconcile/number.ts rename to src/dash-table/type/number.ts index e9c1ca2a4..ae0586016 100644 --- a/src/dash-table/reconcile/number.ts +++ b/src/dash-table/type/number.ts @@ -1,8 +1,9 @@ +import { format } from 'd3-format'; import isNumeric from 'fast-isnumeric'; import { INumberColumn } from 'dash-table/components/Table/props'; import { reconcileNull, isNully } from './null'; -import { IReconciliation } from '.'; +import { IReconciliation } from './reconcile'; export function coerce(value: any, options: INumberColumn | undefined): IReconciliation { return isNumeric(value) ? @@ -10,6 +11,14 @@ export function coerce(value: any, options: INumberColumn | undefined): IReconci reconcileNull(value, options); } +export function getFormatter(options: INumberColumn | undefined) { + if (!options || !options.formatting) { + return; + } + + return format(options.formatting); +} + export function validate(value: any, options: INumberColumn | undefined): IReconciliation { return typeof value === 'number' && !isNully(value) ? { success: true, value } : diff --git a/src/dash-table/reconcile/index.ts b/src/dash-table/type/reconcile.ts similarity index 99% rename from src/dash-table/reconcile/index.ts rename to src/dash-table/type/reconcile.ts index bbf633f07..e5339991d 100644 --- a/src/dash-table/reconcile/index.ts +++ b/src/dash-table/type/reconcile.ts @@ -84,4 +84,4 @@ export default (value: any, c: IColumnType) => { } return doFailureRecovery(res, c); -}; +}; \ No newline at end of file diff --git a/src/dash-table/reconcile/text.ts b/src/dash-table/type/text.ts similarity index 92% rename from src/dash-table/reconcile/text.ts rename to src/dash-table/type/text.ts index aa1d3b5a3..c8f9e45db 100644 --- a/src/dash-table/reconcile/text.ts +++ b/src/dash-table/type/text.ts @@ -1,6 +1,6 @@ import { ITextColumn } from 'dash-table/components/Table/props'; import { isNully, reconcileNull } from './null'; -import { IReconciliation } from '.'; +import { IReconciliation } from './reconcile'; export function coerce(value: any, options: ITextColumn | undefined): IReconciliation { return isNully(value) ? diff --git a/src/dash-table/utils/applyClipboardToData.ts b/src/dash-table/utils/applyClipboardToData.ts index 90b5a0074..9b197e764 100644 --- a/src/dash-table/utils/applyClipboardToData.ts +++ b/src/dash-table/utils/applyClipboardToData.ts @@ -3,7 +3,7 @@ import * as R from 'ramda'; import Logger from 'core/Logger'; import { ActiveCell, Columns, Data, ColumnType } from 'dash-table/components/Table/props'; -import reconcile from 'dash-table/reconcile'; +import reconcile from 'dash-table/type/reconcile'; import isEditable from 'dash-table/derived/cell/isEditable'; export default ( From a56c7d8fae00cc352263f98aace3b30716317871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 15 Feb 2019 17:55:16 -0500 Subject: [PATCH 02/55] - update tests --- tests/cypress/tests/unit/dateCoercion_test.ts | 4 ++-- tests/cypress/tests/unit/dateValidation_test.ts | 4 ++-- tests/cypress/tests/unit/numberCoercion_test.ts | 4 ++-- tests/cypress/tests/unit/numberValidation_test.ts | 4 ++-- tests/cypress/tests/unit/textCoercion_test.ts | 2 +- tests/cypress/tests/unit/textValidation_test.ts | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/cypress/tests/unit/dateCoercion_test.ts b/tests/cypress/tests/unit/dateCoercion_test.ts index 794a9968a..606cc0185 100644 --- a/tests/cypress/tests/unit/dateCoercion_test.ts +++ b/tests/cypress/tests/unit/dateCoercion_test.ts @@ -1,6 +1,6 @@ import { ColumnType, IDatetimeColumn } from 'dash-table/components/Table/props'; -import { isNully } from 'dash-table/reconcile/null'; -import { coerce } from 'dash-table/reconcile/date'; +import { isNully } from 'dash-table/type/null'; +import { coerce } from 'dash-table/type/date'; const DEFAULT_COERCE_SUCCESS = [ { input: ' 2000 ', output: '2000', name: 'year only' }, diff --git a/tests/cypress/tests/unit/dateValidation_test.ts b/tests/cypress/tests/unit/dateValidation_test.ts index b2f021fc2..78b35d764 100644 --- a/tests/cypress/tests/unit/dateValidation_test.ts +++ b/tests/cypress/tests/unit/dateValidation_test.ts @@ -1,6 +1,6 @@ import { ColumnType, IDatetimeColumn } from 'dash-table/components/Table/props'; -import { isNully } from 'dash-table/reconcile/null'; -import { validate } from 'dash-table/reconcile/date'; +import { isNully } from 'dash-table/type/null'; +import { validate } from 'dash-table/type/date'; const DEFAULT_VALIDATE_SUCCESS = [ { input: ' 2000 ', output: '2000', name: 'year only' }, diff --git a/tests/cypress/tests/unit/numberCoercion_test.ts b/tests/cypress/tests/unit/numberCoercion_test.ts index 6a02320c2..c8ea74c69 100644 --- a/tests/cypress/tests/unit/numberCoercion_test.ts +++ b/tests/cypress/tests/unit/numberCoercion_test.ts @@ -1,6 +1,6 @@ import { ColumnType, INumberColumn } from 'dash-table/components/Table/props'; -import { isNully } from 'dash-table/reconcile/null'; -import { coerce } from 'dash-table/reconcile/number'; +import { isNully } from 'dash-table/type/null'; +import { coerce } from 'dash-table/type/number'; const DEFAULT_COERCE_SUCCESS = [ { input: 42, output: 42, name: 'from number' }, diff --git a/tests/cypress/tests/unit/numberValidation_test.ts b/tests/cypress/tests/unit/numberValidation_test.ts index e4304dc49..4e6c577ab 100644 --- a/tests/cypress/tests/unit/numberValidation_test.ts +++ b/tests/cypress/tests/unit/numberValidation_test.ts @@ -1,6 +1,6 @@ import { ColumnType, INumberColumn } from 'dash-table/components/Table/props'; -import { isNully } from 'dash-table/reconcile/null'; -import { validate } from 'dash-table/reconcile/number'; +import { isNully } from 'dash-table/type/null'; +import { validate } from 'dash-table/type/number'; const DEFAULT_VALIDATE_SUCCESS = [ { input: 42, output: 42, name: 'from number' } diff --git a/tests/cypress/tests/unit/textCoercion_test.ts b/tests/cypress/tests/unit/textCoercion_test.ts index 79838bfbd..3474f3e09 100644 --- a/tests/cypress/tests/unit/textCoercion_test.ts +++ b/tests/cypress/tests/unit/textCoercion_test.ts @@ -1,5 +1,5 @@ import { ColumnType, ITextColumn } from 'dash-table/components/Table/props'; -import { coerce } from 'dash-table/reconcile/text'; +import { coerce } from 'dash-table/type/text'; const DEFAULT_COERCE_SUCCESS = [ { input: 42, output: '42', name: 'from number' }, diff --git a/tests/cypress/tests/unit/textValidation_test.ts b/tests/cypress/tests/unit/textValidation_test.ts index 8345f8134..c3be052c2 100644 --- a/tests/cypress/tests/unit/textValidation_test.ts +++ b/tests/cypress/tests/unit/textValidation_test.ts @@ -1,6 +1,6 @@ import { ColumnType, ITextColumn } from 'dash-table/components/Table/props'; -import { isNully } from 'dash-table/reconcile/null'; -import { validate } from 'dash-table/reconcile/text'; +import { isNully } from 'dash-table/type/null'; +import { validate } from 'dash-table/type/text'; const DEFAULT_VALIDATE_SUCCESS = [ { input: '42', output: '42', name: 'from string' } From c7546153cfefac72212841dc8faa2d22d2ee0567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 18 Feb 2019 12:38:10 -0500 Subject: [PATCH 03/55] - add locale logic and nested props --- dash_table/DataTable.py | 23 ----------------------- src/dash-table/DataTable.js | 9 ++++++++- src/dash-table/components/Table/props.ts | 9 +++++++++ src/dash-table/type/number.ts | 14 ++++++++++++-- 4 files changed, 29 insertions(+), 26 deletions(-) diff --git a/dash_table/DataTable.py b/dash_table/DataTable.py index 9f9d601ec..45bf48ba0 100644 --- a/dash_table/DataTable.py +++ b/dash_table/DataTable.py @@ -405,26 +405,3 @@ def __init__(self, active_cell=Component.UNDEFINED, columns=Component.UNDEFINED, raise TypeError( 'Required argument `' + k + '` was not specified.') super(DataTable, self).__init__(**args) - - def __repr__(self): - if(any(getattr(self, c, None) is not None - for c in self._prop_names - if c is not self._prop_names[0]) - or any(getattr(self, c, None) is not None - for c in self.__dict__.keys() - if any(c.startswith(wc_attr) - for wc_attr in self._valid_wildcard_attributes))): - props_string = ', '.join([c+'='+repr(getattr(self, c, None)) - for c in self._prop_names - if getattr(self, c, None) is not None]) - wilds_string = ', '.join([c+'='+repr(getattr(self, c, None)) - for c in self.__dict__.keys() - if any([c.startswith(wc_attr) - for wc_attr in - self._valid_wildcard_attributes])]) - return ('DataTable(' + props_string + - (', ' + wilds_string if wilds_string != '' else '') + ')') - else: - return ( - 'DataTable(' + - repr(getattr(self, self._prop_names[0], None)) + ')') diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index 00090bf6e..d48ac5027 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -276,7 +276,14 @@ export const propTypes = { */ type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), - formatting: PropTypes.string + formatting: PropTypes.string, + + locale: PropTypes.shape({ + decimal: PropTypes.string, + thousands: PropTypes.string, + grouping: PropTypes.arrayOf(PropTypes.number), + currency: PropTypes.arrayOf(PropTypes.string) + }) })), /** diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 80a18aa09..05f033ed5 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -109,9 +109,18 @@ export interface ITypeColumn { validation?: ITypeValidation; } +export interface INumberLocale { + decimal: string; + thousands: string; + grouping: number[]; + currency: [string, string]; + +} + export interface INumberColumn extends ITypeColumn { presentation?: Presentation.Input | Presentation.Dropdown; formatting?: string; + locale?: Partial; type: ColumnType.Numeric; } diff --git a/src/dash-table/type/number.ts b/src/dash-table/type/number.ts index ae0586016..08077ab81 100644 --- a/src/dash-table/type/number.ts +++ b/src/dash-table/type/number.ts @@ -1,10 +1,18 @@ -import { format } from 'd3-format'; +import * as R from 'ramda'; +import { formatLocale } from 'd3-format'; import isNumeric from 'fast-isnumeric'; import { INumberColumn } from 'dash-table/components/Table/props'; import { reconcileNull, isNully } from './null'; import { IReconciliation } from './reconcile'; +const DEFAULT_LOCALE = { + decimal: '.', + thousands: '', + grouping: [3], + currency: ['$', ''] +}; + export function coerce(value: any, options: INumberColumn | undefined): IReconciliation { return isNumeric(value) ? { success: true, value: +value } : @@ -16,7 +24,9 @@ export function getFormatter(options: INumberColumn | undefined) { return; } - return format(options.formatting); + return formatLocale( + R.merge(DEFAULT_LOCALE, options.locale || {}) + ).format(options.formatting); } export function validate(value: any, options: INumberColumn | undefined): IReconciliation { From c210430d76961a172d4d28865279893522f4f8b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 18 Feb 2019 13:02:54 -0500 Subject: [PATCH 04/55] - locale support --- src/dash-table/DataTable.js | 17 ++++++++++------- src/dash-table/components/Table/props.ts | 12 +++++++----- src/dash-table/type/number.ts | 9 ++++++--- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index d48ac5027..def718758 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -276,13 +276,16 @@ export const propTypes = { */ type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), - formatting: PropTypes.string, - - locale: PropTypes.shape({ - decimal: PropTypes.string, - thousands: PropTypes.string, - grouping: PropTypes.arrayOf(PropTypes.number), - currency: PropTypes.arrayOf(PropTypes.string) + format: PropTypes.shape({ + locale: PropTypes.shape({ + currency: PropTypes.arrayOf(PropTypes.string), + decimal: PropTypes.string, + grouping: PropTypes.arrayOf(PropTypes.number), + numerals: PropTypes.arrayOf(PropTypes.string), + percent: PropTypes.string, + thousands: PropTypes.string + }), + specifier: PropTypes.string }) })), diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 05f033ed5..cc1c1c95c 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -110,17 +110,19 @@ export interface ITypeColumn { } export interface INumberLocale { + currency: [string, string]; decimal: string; - thousands: string; grouping: number[]; - currency: [string, string]; - + numerals: string[]; + percent: string; + thousands: string; } export interface INumberColumn extends ITypeColumn { + format?: { + specifier: string; + } & Partial; presentation?: Presentation.Input | Presentation.Dropdown; - formatting?: string; - locale?: Partial; type: ColumnType.Numeric; } diff --git a/src/dash-table/type/number.ts b/src/dash-table/type/number.ts index 08077ab81..9a7625f12 100644 --- a/src/dash-table/type/number.ts +++ b/src/dash-table/type/number.ts @@ -20,13 +20,16 @@ export function coerce(value: any, options: INumberColumn | undefined): IReconci } export function getFormatter(options: INumberColumn | undefined) { - if (!options || !options.formatting) { + if (!options || !options.format) { return; } return formatLocale( - R.merge(DEFAULT_LOCALE, options.locale || {}) - ).format(options.formatting); + R.merge( + DEFAULT_LOCALE, + R.omit(['specifier'], options.format || {}) + ) + ).format(options.format.specifier); } export function validate(value: any, options: INumberColumn | undefined): IReconciliation { From aad2b8028b0405f59ffa15d95b8ee598b6c82936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 18 Feb 2019 14:00:56 -0500 Subject: [PATCH 05/55] - format validation that we can't do with our usage of proptypes --- src/dash-table/DataTable.js | 40 +++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index def718758..93b0007c1 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -1,3 +1,4 @@ +import * as R from 'ramda'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; @@ -24,21 +25,48 @@ export default class DataTable extends Component { } render() { + if (!this.isValid()) { + Logger.error(`Invalid combination of filtering / sorting / pagination`); + return (
Invalid props combination
); + } + + if (!this.validColumns()) { + Logger.error(`Invalid column format`); + return (
Invalid props combination
); + } + + return this.props.id ? () : (); + } + + private isValid() { const { filtering, sorting, pagination_mode } = this.props; - const isValid = isFrontEnd(pagination_mode) || + return isFrontEnd(pagination_mode) || (isBackEnd(filtering) && isBackEnd(sorting)); + } - if (!isValid) { - Logger.error(`Invalid combination of filtering / sorting / pagination`, filtering, sorting, pagination_mode); - return (
Invalid props combination
); - } + private allValidColumns() { + const { + columns + } = this.props; - return this.props.id ? () : (); + return !R.has(column => + column.format && ( + ( + column.format.currency && + column.format.currency.length !== 2 + ) || ( + column.format.grouping && + column.format.grouping.length === 0 + ) || ( + column.format.numerals && + column.format.numerals.length !== 10 + ) + ), columns); } } From 4f2263fc80f026d0518a3aecddae72d51c16a05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 18 Feb 2019 14:26:48 -0500 Subject: [PATCH 06/55] - this is js, not ts! --- src/dash-table/DataTable.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index 93b0007c1..60cd5926a 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -38,7 +38,7 @@ export default class DataTable extends Component { return this.props.id ? () : (); } - private isValid() { + isValid() { const { filtering, sorting, @@ -49,7 +49,7 @@ export default class DataTable extends Component { (isBackEnd(filtering) && isBackEnd(sorting)); } - private allValidColumns() { + allValidColumns() { const { columns } = this.props; From b537f2499f0b7a8ebbbc1d257a7406be6286edbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 18 Feb 2019 14:35:05 -0500 Subject: [PATCH 07/55] incorrect usage --- src/dash-table/DataTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index 60cd5926a..d227d09d9 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -30,7 +30,7 @@ export default class DataTable extends Component { return (
Invalid props combination
); } - if (!this.validColumns()) { + if (!this.allValidColumns()) { Logger.error(`Invalid column format`); return (
Invalid props combination
); } From de4bd161e33a4f1b9c778a4936e608f7f1f0b3d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 18 Feb 2019 16:18:48 -0500 Subject: [PATCH 08/55] - first draft of py formatting helpers --- dash_table/Format.py | 19 +++++++++++++++++++ dash_table/FormatTemplate.py | 17 +++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 dash_table/Format.py create mode 100644 dash_table/FormatTemplate.py diff --git a/dash_table/Format.py b/dash_table/Format.py new file mode 100644 index 000000000..12646251b --- /dev/null +++ b/dash_table/Format.py @@ -0,0 +1,19 @@ +from enum import Enum + +class Precision(Enum): + UNDEFINED = '' + Fixed = 'f' + Significant = 's' + + +class Sign(Enum): + UNDEFINED = '' + NEGATIVE = '-' + POSTIVE = '+' + PARANTHESES = '(' + SPACE = ' ' + + +class SymbolPosition(Enum): + PREFIX = 0 + SUFFIX = 1 \ No newline at end of file diff --git a/dash_table/FormatTemplate.py b/dash_table/FormatTemplate.py new file mode 100644 index 000000000..baa5e6bae --- /dev/null +++ b/dash_table/FormatTemplate.py @@ -0,0 +1,17 @@ +from enum import Enum +from .Format import Precision, Sign + +def money(decimals: int = 2, sign: Sign = Sign.UNDEFINED): + decimals = decimals if isinstance(decimals, int) else 2 + + return { 'specifier': '{0}$.{1}f'.format(sign.value, decimals) } + + +def percentage(decimals: int = 0): + decimals = decimals if isinstance(decimals, int) else 0 + + return { 'specifier': '.{0}%'.format(decimals) } + + +def precision(decimals: int, precision: Precision = Precision.UNDEFINED): + return { 'specifier': '.{0}{1}'.format(decimals, precision.value) } \ No newline at end of file From 37c5ac9e8fa94e0f5361a0dc1a935d2f89bdab80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 19 Feb 2019 11:11:16 -0500 Subject: [PATCH 09/55] - merge format and specifier - add si prefix support - update js props and code to support si prefix --- dash_table/Format.py | 195 ++++++++++++++++++++++- dash_table/FormatTemplate.py | 18 ++- src/dash-table/DataTable.js | 1 + src/dash-table/components/Table/props.ts | 1 + src/dash-table/type/number.ts | 10 +- 5 files changed, 208 insertions(+), 17 deletions(-) diff --git a/dash_table/Format.py b/dash_table/Format.py index 12646251b..90cb209c9 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -1,19 +1,200 @@ from enum import Enum +import json -class Precision(Enum): +class Align(Enum): UNDEFINED = '' - Fixed = 'f' - Significant = 's' + RIGHT = '>' + LEFT = '<' + CENTER = '^' + RIGHT_SIGN = '=' +class GroupSeparator(Enum): + NO = '' + YES = ',' + +class Padding(Enum): + NO = '' + YES = '0' + +class Prefix(Enum): + YOCTO = 10**-24 + ZEPTO = 10**-21 + ATTO = 10**-18 + FEMTO = 10**-15 + PICO = 10**-12 + NANO = 10**-9 + MICRO = 10**-6 + MILLI = 10**-3 + UNDEFINED = None + KILO = 10**3 + MEGA = 10**6 + GIGA = 10**9 + TERA = 10**12 + PETA = 10**15 + EXA = 10**18 + ZETTA = 10**21 + YOTTA = 10**24 class Sign(Enum): UNDEFINED = '' NEGATIVE = '-' - POSTIVE = '+' + POSITIVE = '+' PARANTHESES = '(' SPACE = ' ' +class Symbol(Enum): + UNDEFINED = '' + CURRENCY = '$' + BINARY = '#b' + OCTAL = '#o' + HEX = '#x' + +class Trim(Enum): + NO = '' + YES = '~' + +class Type(Enum): + UNDEFINED = '' + EXPONENT = 'e' + FIXED_POINT = 'f' + DECIMAL_OR_EXPONENT = 'g' + DECIMAL = 'r' + DECIMAL_SI_PREFIX = 's' + PERCENTAGE = '%' + PERCENTAGE_ROUNDED = 'p' + BINARY = 'b' + OCTAL = 'o' + DECIMAL_INTEGER = 'd' + LOWER_CASE_HEX = 'x' + UPPER_CASE_HEX = 'X' + UNICODE = 'c' + +class Format(): + def __init__(self): + self.locale = {} + self.prefix = Prefix.UNDEFINED + self.specifier = { + 'align': Align.UNDEFINED, + 'fill': '', + 'group': GroupSeparator.NO, + 'width': '', + 'padding': Padding.NO, + 'precision': '', + 'sign': Sign.UNDEFINED, + 'symbol': Symbol.UNDEFINED, + 'trim': Trim.NO, + 'type': Type.UNDEFINED + } + + # Specifier + def align(self, align: Align = Align.UNDEFINED): + align = align if isinstance(align, Align) else Align.UNDEFINED + self.specifier['align'] = align + return self + + def fill(self, value: str): + self.specifier['fill'] = value + return self + + def group(self, value: bool = False): + value = value if isinstance(value, bool) else False + value = GroupSeparator.YES if value == True else GroupSeparator.NO + self.specifier['group'] = value + return self + + def pad(self, value: bool = False): + value = value if isinstance(value, bool) else False + value = value.YES if value == True else Padding.NO + self.specifier['padding'] = value + return self + + def precision(self, value: int = ''): + value = '.{}'.format(value) if isinstance(value, int) or value != '' else '' + self.specifier['precision'] = value + return self + + def sign(self, value: Sign = Sign.UNDEFINED): + value = value if isinstance(value, Sign) else Sign.UNDEFINED + self.specifier['sign'] = value + return self + + def symbol(self, value: Symbol = Symbol.UNDEFINED): + value = value if isinstance(value, Symbol) else Symbol.UNDEFINED + self.specifier['symbol'] = value + return self + + def trim(self, value: bool = False): + value = value if isinstance(value, bool) else False + value = Trim.YES if value == True else Trim.NO + self.specifier['trim'] = value + return self + + def type(self, value: Type = Type.UNDEFINED): + value = value if isinstance(value, Type) else Type.UNDEFINED + self.specifier['type'] = value + return self + + def width(self, value: int = ''): + value = value if isinstance(value, int) or value == '' else '' + self.specifier['width'] = str(value) + return self + + def currency_prefix(self, symbol: str): + if 'currency' not in self.locale: + self.locale['currency'] = [symbol, ''] + else: + self.locale['currency'][0] = symbol + + return self + + # Locale + def currency_suffix(self, symbol: str): + if 'currency' not in self.locale: + self.locale['currency'] = ['', symbol] + else: + self.locale['currency'][1] = symbol + + return self + + def decimal_delimitor(self, symbol: str): + self.locale['decimal'] = symbol + return self + + def group_delimitor(self, symbol: str): + self.locale['thousands'] = symbol + return self + + def groups(self, groups: list): + groups = groups if isinstance(groups, list) else \ + [groups] if isinstance(groups, int) else None + + for group in groups: + if not isinstance(group, int): + raise Exception('expected "group" to be an int') + + self.locale['grouping'] = groups + return self + + # Prefix + def si_prefix(self, prefix: Prefix = Prefix.UNDEFINED): + prefix = prefix if isinstance(prefix, Prefix) else Prefix.UNDEFINED + self.prefix = prefix + return self + + def create(self): + f = self.locale.copy() + f['prefix'] = self.prefix.value + f['specifier'] = '{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}'.format( + self.specifier['fill'] if self.specifier['align'] != Align.UNDEFINED else '', + self.specifier['align'].value, + self.specifier['sign'].value, + self.specifier['symbol'].value, + self.specifier['padding'].value, + self.specifier['width'], + self.specifier['group'].value, + self.specifier['precision'], + self.specifier['trim'].value, + self.specifier['type'].value + ) -class SymbolPosition(Enum): - PREFIX = 0 - SUFFIX = 1 \ No newline at end of file + return f \ No newline at end of file diff --git a/dash_table/FormatTemplate.py b/dash_table/FormatTemplate.py index baa5e6bae..9bd5613a0 100644 --- a/dash_table/FormatTemplate.py +++ b/dash_table/FormatTemplate.py @@ -1,17 +1,21 @@ from enum import Enum -from .Format import Precision, Sign +from .Format import Format, Sign, Symbol, Type def money(decimals: int = 2, sign: Sign = Sign.UNDEFINED): decimals = decimals if isinstance(decimals, int) else 2 - return { 'specifier': '{0}$.{1}f'.format(sign.value, decimals) } + return Format() \ + .precision(decimals) \ + .sign(sign) \ + .symbol(Symbol.CURRENCY) \ + .type(Type.FIXED_POINT) \ + .create() def percentage(decimals: int = 0): decimals = decimals if isinstance(decimals, int) else 0 - return { 'specifier': '.{0}%'.format(decimals) } - - -def precision(decimals: int, precision: Precision = Precision.UNDEFINED): - return { 'specifier': '.{0}{1}'.format(decimals, precision.value) } \ No newline at end of file + return Format() \ + .precision(decimals) \ + .type(Type.PERCENTAGE) \ + .create() \ No newline at end of file diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index d227d09d9..c0ea944f0 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -313,6 +313,7 @@ export const propTypes = { percent: PropTypes.string, thousands: PropTypes.string }), + prefix: PropTypes.number, specifier: PropTypes.string }) })), diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index cc1c1c95c..ddb0fbe6e 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -120,6 +120,7 @@ export interface INumberLocale { export interface INumberColumn extends ITypeColumn { format?: { + prefix?: number; specifier: string; } & Partial; presentation?: Presentation.Input | Presentation.Dropdown; diff --git a/src/dash-table/type/number.ts b/src/dash-table/type/number.ts index 9a7625f12..450c09a74 100644 --- a/src/dash-table/type/number.ts +++ b/src/dash-table/type/number.ts @@ -24,12 +24,16 @@ export function getFormatter(options: INumberColumn | undefined) { return; } - return formatLocale( + const locale = formatLocale( R.merge( DEFAULT_LOCALE, - R.omit(['specifier'], options.format || {}) + R.omit(['specifier', 'prefix'], options.format || {}) ) - ).format(options.format.specifier); + ); + + return options.format.prefix ? + locale.formatPrefix(options.format.specifier, options.format.prefix) : + locale.format(options.format.specifier); } export function validate(value: any, options: INumberColumn | undefined): IReconciliation { From a76e9cffc4a8848e86e80653ca0334194630c442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 Feb 2019 14:27:35 -0500 Subject: [PATCH 10/55] - table level `locale_format` prop --- dash_table/DataTable.py | 14 +++- dash_table/metadata.json | 95 +++++++++++++++++++++++ src/dash-table/DataTable.js | 41 +++++++--- src/dash-table/components/CellFactory.tsx | 2 + src/dash-table/components/Table/props.ts | 4 + src/dash-table/derived/cell/contents.tsx | 13 ++-- src/dash-table/type/formatter.ts | 22 +++--- src/dash-table/type/number.ts | 18 ++--- 8 files changed, 171 insertions(+), 38 deletions(-) diff --git a/dash_table/DataTable.py b/dash_table/DataTable.py index 45bf48ba0..193f85261 100644 --- a/dash_table/DataTable.py +++ b/dash_table/DataTable.py @@ -12,6 +12,14 @@ class DataTable(Component): active. - columns (list; optional): Columns describes various aspects about each individual column. `name` and `id` are the only required parameters. +- locale_format (optional): . locale_format has the following type: dict containing keys 'currency', 'decimal', 'grouping', 'numerals', 'percent', 'thousands'. +Those keys have the following types: + - currency (list; optional) + - decimal (string; optional) + - grouping (list; optional) + - numerals (list; optional) + - percent (string; optional) + - thousands (string; optional) - content_style (a value equal to: 'fit', 'grow'; optional): `content_style` toggles between a set of CSS styles for two common behaviors: - `fit`: The table container's width be equal to the width of its content. @@ -387,12 +395,12 @@ class DataTable(Component): Subscribe to [https://github.com/plotly/dash-table/issues/168](https://github.com/plotly/dash-table/issues/168) for updates on the dropdown API.""" @_explicitize_args - def __init__(self, active_cell=Component.UNDEFINED, columns=Component.UNDEFINED, content_style=Component.UNDEFINED, css=Component.UNDEFINED, data=Component.UNDEFINED, data_previous=Component.UNDEFINED, data_timestamp=Component.UNDEFINED, editable=Component.UNDEFINED, end_cell=Component.UNDEFINED, id=Component.UNDEFINED, is_focused=Component.UNDEFINED, merge_duplicate_headers=Component.UNDEFINED, n_fixed_columns=Component.UNDEFINED, n_fixed_rows=Component.UNDEFINED, row_deletable=Component.UNDEFINED, row_selectable=Component.UNDEFINED, selected_cells=Component.UNDEFINED, selected_rows=Component.UNDEFINED, start_cell=Component.UNDEFINED, style_as_list_view=Component.UNDEFINED, pagination_mode=Component.UNDEFINED, pagination_settings=Component.UNDEFINED, navigation=Component.UNDEFINED, column_conditional_dropdowns=Component.UNDEFINED, column_static_dropdown=Component.UNDEFINED, column_static_tooltip=Component.UNDEFINED, column_conditional_tooltips=Component.UNDEFINED, tooltips=Component.UNDEFINED, tooltip_delay=Component.UNDEFINED, tooltip_duration=Component.UNDEFINED, filtering=Component.UNDEFINED, filtering_settings=Component.UNDEFINED, filtering_type=Component.UNDEFINED, filtering_types=Component.UNDEFINED, sorting=Component.UNDEFINED, sorting_type=Component.UNDEFINED, sorting_settings=Component.UNDEFINED, sorting_treat_empty_string_as_none=Component.UNDEFINED, style_table=Component.UNDEFINED, style_cell=Component.UNDEFINED, style_data=Component.UNDEFINED, style_filter=Component.UNDEFINED, style_header=Component.UNDEFINED, style_cell_conditional=Component.UNDEFINED, style_data_conditional=Component.UNDEFINED, style_filter_conditional=Component.UNDEFINED, style_header_conditional=Component.UNDEFINED, virtualization=Component.UNDEFINED, derived_viewport_data=Component.UNDEFINED, derived_viewport_indices=Component.UNDEFINED, derived_viewport_selected_rows=Component.UNDEFINED, derived_virtual_data=Component.UNDEFINED, derived_virtual_indices=Component.UNDEFINED, derived_virtual_selected_rows=Component.UNDEFINED, dropdown_properties=Component.UNDEFINED, **kwargs): - self._prop_names = ['active_cell', 'columns', 'content_style', 'css', 'data', 'data_previous', 'data_timestamp', 'editable', 'end_cell', 'id', 'is_focused', 'merge_duplicate_headers', 'n_fixed_columns', 'n_fixed_rows', 'row_deletable', 'row_selectable', 'selected_cells', 'selected_rows', 'start_cell', 'style_as_list_view', 'pagination_mode', 'pagination_settings', 'navigation', 'column_conditional_dropdowns', 'column_static_dropdown', 'column_static_tooltip', 'column_conditional_tooltips', 'tooltips', 'tooltip_delay', 'tooltip_duration', 'filtering', 'filtering_settings', 'filtering_type', 'filtering_types', 'sorting', 'sorting_type', 'sorting_settings', 'sorting_treat_empty_string_as_none', 'style_table', 'style_cell', 'style_data', 'style_filter', 'style_header', 'style_cell_conditional', 'style_data_conditional', 'style_filter_conditional', 'style_header_conditional', 'virtualization', 'derived_viewport_data', 'derived_viewport_indices', 'derived_viewport_selected_rows', 'derived_virtual_data', 'derived_virtual_indices', 'derived_virtual_selected_rows', 'dropdown_properties'] + def __init__(self, active_cell=Component.UNDEFINED, columns=Component.UNDEFINED, locale_format=Component.UNDEFINED, content_style=Component.UNDEFINED, css=Component.UNDEFINED, data=Component.UNDEFINED, data_previous=Component.UNDEFINED, data_timestamp=Component.UNDEFINED, editable=Component.UNDEFINED, end_cell=Component.UNDEFINED, id=Component.UNDEFINED, is_focused=Component.UNDEFINED, merge_duplicate_headers=Component.UNDEFINED, n_fixed_columns=Component.UNDEFINED, n_fixed_rows=Component.UNDEFINED, row_deletable=Component.UNDEFINED, row_selectable=Component.UNDEFINED, selected_cells=Component.UNDEFINED, selected_rows=Component.UNDEFINED, start_cell=Component.UNDEFINED, style_as_list_view=Component.UNDEFINED, pagination_mode=Component.UNDEFINED, pagination_settings=Component.UNDEFINED, navigation=Component.UNDEFINED, column_conditional_dropdowns=Component.UNDEFINED, column_static_dropdown=Component.UNDEFINED, column_static_tooltip=Component.UNDEFINED, column_conditional_tooltips=Component.UNDEFINED, tooltips=Component.UNDEFINED, tooltip_delay=Component.UNDEFINED, tooltip_duration=Component.UNDEFINED, filtering=Component.UNDEFINED, filtering_settings=Component.UNDEFINED, filtering_type=Component.UNDEFINED, filtering_types=Component.UNDEFINED, sorting=Component.UNDEFINED, sorting_type=Component.UNDEFINED, sorting_settings=Component.UNDEFINED, sorting_treat_empty_string_as_none=Component.UNDEFINED, style_table=Component.UNDEFINED, style_cell=Component.UNDEFINED, style_data=Component.UNDEFINED, style_filter=Component.UNDEFINED, style_header=Component.UNDEFINED, style_cell_conditional=Component.UNDEFINED, style_data_conditional=Component.UNDEFINED, style_filter_conditional=Component.UNDEFINED, style_header_conditional=Component.UNDEFINED, virtualization=Component.UNDEFINED, derived_viewport_data=Component.UNDEFINED, derived_viewport_indices=Component.UNDEFINED, derived_viewport_selected_rows=Component.UNDEFINED, derived_virtual_data=Component.UNDEFINED, derived_virtual_indices=Component.UNDEFINED, derived_virtual_selected_rows=Component.UNDEFINED, dropdown_properties=Component.UNDEFINED, **kwargs): + self._prop_names = ['active_cell', 'columns', 'locale_format', 'content_style', 'css', 'data', 'data_previous', 'data_timestamp', 'editable', 'end_cell', 'id', 'is_focused', 'merge_duplicate_headers', 'n_fixed_columns', 'n_fixed_rows', 'row_deletable', 'row_selectable', 'selected_cells', 'selected_rows', 'start_cell', 'style_as_list_view', 'pagination_mode', 'pagination_settings', 'navigation', 'column_conditional_dropdowns', 'column_static_dropdown', 'column_static_tooltip', 'column_conditional_tooltips', 'tooltips', 'tooltip_delay', 'tooltip_duration', 'filtering', 'filtering_settings', 'filtering_type', 'filtering_types', 'sorting', 'sorting_type', 'sorting_settings', 'sorting_treat_empty_string_as_none', 'style_table', 'style_cell', 'style_data', 'style_filter', 'style_header', 'style_cell_conditional', 'style_data_conditional', 'style_filter_conditional', 'style_header_conditional', 'virtualization', 'derived_viewport_data', 'derived_viewport_indices', 'derived_viewport_selected_rows', 'derived_virtual_data', 'derived_virtual_indices', 'derived_virtual_selected_rows', 'dropdown_properties'] self._type = 'DataTable' self._namespace = 'dash_table' self._valid_wildcard_attributes = [] - self.available_properties = ['active_cell', 'columns', 'content_style', 'css', 'data', 'data_previous', 'data_timestamp', 'editable', 'end_cell', 'id', 'is_focused', 'merge_duplicate_headers', 'n_fixed_columns', 'n_fixed_rows', 'row_deletable', 'row_selectable', 'selected_cells', 'selected_rows', 'start_cell', 'style_as_list_view', 'pagination_mode', 'pagination_settings', 'navigation', 'column_conditional_dropdowns', 'column_static_dropdown', 'column_static_tooltip', 'column_conditional_tooltips', 'tooltips', 'tooltip_delay', 'tooltip_duration', 'filtering', 'filtering_settings', 'filtering_type', 'filtering_types', 'sorting', 'sorting_type', 'sorting_settings', 'sorting_treat_empty_string_as_none', 'style_table', 'style_cell', 'style_data', 'style_filter', 'style_header', 'style_cell_conditional', 'style_data_conditional', 'style_filter_conditional', 'style_header_conditional', 'virtualization', 'derived_viewport_data', 'derived_viewport_indices', 'derived_viewport_selected_rows', 'derived_virtual_data', 'derived_virtual_indices', 'derived_virtual_selected_rows', 'dropdown_properties'] + self.available_properties = ['active_cell', 'columns', 'locale_format', 'content_style', 'css', 'data', 'data_previous', 'data_timestamp', 'editable', 'end_cell', 'id', 'is_focused', 'merge_duplicate_headers', 'n_fixed_columns', 'n_fixed_rows', 'row_deletable', 'row_selectable', 'selected_cells', 'selected_rows', 'start_cell', 'style_as_list_view', 'pagination_mode', 'pagination_settings', 'navigation', 'column_conditional_dropdowns', 'column_static_dropdown', 'column_static_tooltip', 'column_conditional_tooltips', 'tooltips', 'tooltip_delay', 'tooltip_duration', 'filtering', 'filtering_settings', 'filtering_type', 'filtering_types', 'sorting', 'sorting_type', 'sorting_settings', 'sorting_treat_empty_string_as_none', 'style_table', 'style_cell', 'style_data', 'style_filter', 'style_header', 'style_cell_conditional', 'style_data_conditional', 'style_filter_conditional', 'style_header_conditional', 'virtualization', 'derived_viewport_data', 'derived_viewport_indices', 'derived_viewport_selected_rows', 'derived_virtual_data', 'derived_virtual_indices', 'derived_virtual_selected_rows', 'dropdown_properties'] self.available_wildcard_properties = [] _explicit_args = kwargs.pop('_explicit_args') diff --git a/dash_table/metadata.json b/dash_table/metadata.json index 539744ebd..a59b466db 100644 --- a/dash_table/metadata.json +++ b/dash_table/metadata.json @@ -216,6 +216,59 @@ ], "description": "The data-type of the column's data.\n'numeric': represents both floats and ints\n'text': represents a string\n'datetime': a string representing a date or date-time, in the form:\n 'YYYY-MM-DD HH:MM:SS.ssssss' or some truncation thereof. Years must\n have 4 digits, unless you use `validation.allow_YY: true`. Also\n accepts 'T' or 't' between date and time, and allows timezone info\n at the end. To convert these strings to Python `datetime` objects,\n use `dateutil.parser.isoparse`. In R use `parse_iso_8601` from the\n `parsedate` library.\n WARNING: these parsers do not work with 2-digit years, if you use\n `validation.allow_YY: true` and do not coerce to 4-digit years.\n And parsers that do work with 2-digit years may make a different\n guess about the century than we make on the front end.\n'any': represents any type of data\n\nDefaults to 'any' if undefined.\n\nNOTE: This feature has not been fully implemented.\nIn the future, it's data types will impact things like\ntext formatting options in the cell (e.g. display 2 decimals\nfor a number), filtering options and behavior, and editing\nbehavior.\nStay tuned by following [https://github.com/plotly/dash-table/issues/166](https://github.com/plotly/dash-table/issues/166)", "required": false + }, + "format": { + "name": "shape", + "value": { + "locale": { + "name": "shape", + "value": { + "currency": { + "name": "arrayOf", + "value": { + "name": "string" + }, + "required": false + }, + "decimal": { + "name": "string", + "required": false + }, + "grouping": { + "name": "arrayOf", + "value": { + "name": "number" + }, + "required": false + }, + "numerals": { + "name": "arrayOf", + "value": { + "name": "string" + }, + "required": false + }, + "percent": { + "name": "string", + "required": false + }, + "thousands": { + "name": "string", + "required": false + } + }, + "required": false + }, + "prefix": { + "name": "number", + "required": false + }, + "specifier": { + "name": "string", + "required": false + } + }, + "required": false } } } @@ -227,6 +280,48 @@ "computed": false } }, + "locale_format": { + "type": { + "name": "shape", + "value": { + "currency": { + "name": "arrayOf", + "value": { + "name": "string" + }, + "required": false + }, + "decimal": { + "name": "string", + "required": false + }, + "grouping": { + "name": "arrayOf", + "value": { + "name": "number" + }, + "required": false + }, + "numerals": { + "name": "arrayOf", + "value": { + "name": "string" + }, + "required": false + }, + "percent": { + "name": "string", + "required": false + }, + "thousands": { + "name": "string", + "required": false + } + } + }, + "required": false, + "description": "" + }, "content_style": { "type": { "name": "enum", diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index c0ea944f0..5a2756fa0 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -39,14 +39,14 @@ export default class DataTable extends Component { } isValid() { - const { - filtering, - sorting, - pagination_mode - } = this.props; - - return isFrontEnd(pagination_mode) || - (isBackEnd(filtering) && isBackEnd(sorting)); + const { + filtering, + sorting, + pagination_mode + } = this.props; + + return isFrontEnd(pagination_mode) || + (isBackEnd(filtering) && isBackEnd(sorting)); } allValidColumns() { @@ -68,7 +68,21 @@ export default class DataTable extends Component { ) ), columns); } -} + + render() { + if (!this.isValid()) { + Logger.error(`Invalid combination of filtering / sorting / pagination`); + return (
Invalid props combination
); + } + + if (!this.allValidColumns()) { + Logger.error(`Invalid column format`); + return (
Invalid props combination
); + } + + return this.props.id ? () : (); + } + } export const defaultProps = { pagination_mode: 'fe', @@ -318,6 +332,15 @@ export const propTypes = { }) })), + locale_format: PropTypes.shape({ + currency: PropTypes.arrayOf(PropTypes.string), + decimal: PropTypes.string, + grouping: PropTypes.arrayOf(PropTypes.number), + numerals: PropTypes.arrayOf(PropTypes.string), + percent: PropTypes.string, + thousands: PropTypes.string + }), + /** * `content_style` toggles between a set of CSS styles for * two common behaviors: diff --git a/src/dash-table/components/CellFactory.tsx b/src/dash-table/components/CellFactory.tsx index 217606cf0..957836f77 100644 --- a/src/dash-table/components/CellFactory.tsx +++ b/src/dash-table/components/CellFactory.tsx @@ -37,6 +37,7 @@ export default class CellFactory { dropdown_properties, // legacy editable, is_focused, + locale_format, row_deletable, row_selectable, selected_cells, @@ -94,6 +95,7 @@ export default class CellFactory { const contents = this.cellContents( active_cell, columns, + locale_format, virtualized.data, virtualized.offset, editable, diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index ddb0fbe6e..d98f02211 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -118,6 +118,8 @@ export interface INumberLocale { thousands: string; } +export type LocaleFormat = INumberLocale | undefined; + export interface INumberColumn extends ITypeColumn { format?: { prefix?: number; @@ -264,6 +266,7 @@ interface IProps { filtering_settings?: string; filtering_type?: FilteringType; filtering_types?: FilteringType[]; + locale_format?: LocaleFormat; merge_duplicate_headers?: boolean; navigation?: Navigation; n_fixed_columns?: number; @@ -375,6 +378,7 @@ export interface ICellFactoryProps { editable: boolean; id: string; is_focused?: boolean; + locale_format?: LocaleFormat; n_fixed_columns: number; n_fixed_rows: number; paginator: IPaginator; diff --git a/src/dash-table/derived/cell/contents.tsx b/src/dash-table/derived/cell/contents.tsx index 21bd73117..4e2027534 100644 --- a/src/dash-table/derived/cell/contents.tsx +++ b/src/dash-table/derived/cell/contents.tsx @@ -5,12 +5,13 @@ import { ActiveCell, Data, Datum, - IVisibleColumn, - VisibleColumns, + DropdownValues, ICellFactoryProps, IViewportOffset, - DropdownValues, - Presentation + IVisibleColumn, + LocaleFormat, + Presentation, + VisibleColumns } from 'dash-table/components/Table/props'; import CellInput from 'dash-table/components/CellInput'; import derivedCellEventHandlerProps, { Handler } from 'dash-table/derived/cell/eventHandlerProps'; @@ -59,13 +60,15 @@ class Contents { get = memoizeOne(( activeCell: ActiveCell, columns: VisibleColumns, + defaultLocale: LocaleFormat, data: Data, offset: IViewportOffset, editable: boolean, isFocused: boolean, dropdowns: (DropdownValues | undefined)[][] ): JSX.Element[][] => { - const formatters = R.map(getFormatter, columns); + const localizedFormatter = getFormatter(defaultLocale); + const formatters = R.map(localizedFormatter, columns); return mapData( (datum, rowIndex) => mapRow( diff --git a/src/dash-table/type/formatter.ts b/src/dash-table/type/formatter.ts index dfb6a68a7..ec6039e83 100644 --- a/src/dash-table/type/formatter.ts +++ b/src/dash-table/type/formatter.ts @@ -1,18 +1,20 @@ import { ColumnType, - IColumnType + IColumnType, + LocaleFormat } from 'dash-table/components/Table/props'; import { getFormatter as getNumberFormatter } from './number'; const DEFAULT_FORMATTER = (value: any) => value; -export default (c: IColumnType) => { - let formatter; - switch (c.type) { - case ColumnType.Numeric: - formatter = getNumberFormatter(c); - break; - } +export default (locale: LocaleFormat) => + (c: IColumnType) => { + let formatter; + switch (c.type) { + case ColumnType.Numeric: + formatter = getNumberFormatter(locale, c); + break; + } - return formatter || DEFAULT_FORMATTER; -}; \ No newline at end of file + return formatter || DEFAULT_FORMATTER; + }; \ No newline at end of file diff --git a/src/dash-table/type/number.ts b/src/dash-table/type/number.ts index 450c09a74..df1b5155c 100644 --- a/src/dash-table/type/number.ts +++ b/src/dash-table/type/number.ts @@ -2,32 +2,28 @@ import * as R from 'ramda'; import { formatLocale } from 'd3-format'; import isNumeric from 'fast-isnumeric'; -import { INumberColumn } from 'dash-table/components/Table/props'; +import { INumberColumn, LocaleFormat } from 'dash-table/components/Table/props'; import { reconcileNull, isNully } from './null'; import { IReconciliation } from './reconcile'; -const DEFAULT_LOCALE = { - decimal: '.', - thousands: '', - grouping: [3], - currency: ['$', ''] -}; - export function coerce(value: any, options: INumberColumn | undefined): IReconciliation { return isNumeric(value) ? { success: true, value: +value } : reconcileNull(value, options); } -export function getFormatter(options: INumberColumn | undefined) { +export function getFormatter( + defaultLocale: LocaleFormat, + options: INumberColumn | undefined +) { if (!options || !options.format) { return; } const locale = formatLocale( R.merge( - DEFAULT_LOCALE, - R.omit(['specifier', 'prefix'], options.format || {}) + defaultLocale, + R.omit(['specifier', 'prefix'], options.format) ) ); From 58ae65e2a6be3b0c48f631dc7a7c3c69326eb0f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 Feb 2019 15:19:23 -0500 Subject: [PATCH 11/55] - document new props --- src/dash-table/DataTable.js | 68 +++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index 5a2756fa0..9682bd45b 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -199,6 +199,44 @@ export const propTypes = { PropTypes.number ]), + /** + * The formatting applied to the column's data. + * + * This prop is derived from the [d3-format](https://github.com/d3/d3-format) library specification. Apart from + * being structured slightly differently (under a single prop), the usage + * is the same. + * + * 'locale': represents localization specific formatting information + * When left unspecified, will use the default value provided by d3-format. + * + * 'currency': a list of two strings representing the currency + * prefix and suffix symbols + * 'decimal': the string used for the decimal separator + * 'grouping': a list of integers representing the grouping pattern + * 'numerals': a list of ten strings used as replacements for numbers 0-9 + * 'percent': the string used for the percentage symbol + * 'thousands': the string used for the groups separator + * + * 'prefix': a number representing the SI unit to use during formatting + * See `dash_table.Format.Prefix` enumeration for the list of valid values + * 'specifier': represents the rules to apply when formatting the number + * + * dash_table.FormatTemplate contains helper functions to rapidly use certain + * typical number formats. + */ + format: PropTypes.shape({ + locale: PropTypes.shape({ + currency: PropTypes.arrayOf(PropTypes.string), + decimal: PropTypes.string, + grouping: PropTypes.arrayOf(PropTypes.number), + numerals: PropTypes.arrayOf(PropTypes.string), + percent: PropTypes.string, + thousands: PropTypes.string + }), + prefix: PropTypes.number, + specifier: PropTypes.string + }), + /** * If True, then the column and its data is hidden. * This can be useful if you want to transport extra @@ -316,22 +354,24 @@ export const propTypes = { * behavior. * Stay tuned by following [https://github.com/plotly/dash-table/issues/166](https://github.com/plotly/dash-table/issues/166) */ - type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']), - - format: PropTypes.shape({ - locale: PropTypes.shape({ - currency: PropTypes.arrayOf(PropTypes.string), - decimal: PropTypes.string, - grouping: PropTypes.arrayOf(PropTypes.number), - numerals: PropTypes.arrayOf(PropTypes.string), - percent: PropTypes.string, - thousands: PropTypes.string - }), - prefix: PropTypes.number, - specifier: PropTypes.string - }) + type: PropTypes.oneOf(['any', 'numeric', 'text', 'datetime']) })), + /** + * The localization specific formatting information applied to all columns in the table. + * + * This prop is derived from the [d3.formatLocale](https://github.com/d3/d3-format#formatLocale) data structure specification. + * + * When left unspecified, will use the default value provided by d3-format. + * + * 'currency': a list of two strings representing the currency + * prefix and suffix symbols + * 'decimal': the string used for the decimal separator + * 'grouping': a list of integers representing the grouping pattern + * 'numerals': a list of ten strings used as replacements for numbers 0-9 + * 'percent': the string used for the percentage symbol + * 'thousands': the string used for the groups separator + */ locale_format: PropTypes.shape({ currency: PropTypes.arrayOf(PropTypes.string), decimal: PropTypes.string, From 64b2b75678a18d510e6c44c4678083e0f9f0c1da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 Feb 2019 18:28:21 -0500 Subject: [PATCH 12/55] - fix ts import resolution for tests (in IDE) --- tests/cypress/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cypress/tsconfig.json b/tests/cypress/tsconfig.json index f9d9ff27c..d939392d7 100644 --- a/tests/cypress/tsconfig.json +++ b/tests/cypress/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "./../../tsconfig.base.json", "compilerOptions": { - "baseUrl": "./../..", + "baseUrl": ".", "paths": { "cypress/*": ["./src/*"], "core/*": ["./../../src/core/*"], From a287764e6f450177f3dc21fffe6a6b94864efe17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 20 Feb 2019 21:24:05 -0500 Subject: [PATCH 13/55] - unit tests of type/number formatter - fix props (locale, nully) - handle nully case --- dash_table/DataTable.py | 14 ++- dash_table/Format.py | 7 ++ dash_table/metadata.json | 130 +++++++++++--------- src/dash-table/DataTable.js | 7 +- src/dash-table/components/Table/props.ts | 14 ++- src/dash-table/type/formatter.ts | 2 +- src/dash-table/type/number.ts | 44 +++++-- tests/cypress/tests/unit/formatting_test.ts | 129 +++++++++++++++++++ 8 files changed, 274 insertions(+), 73 deletions(-) create mode 100644 tests/cypress/tests/unit/formatting_test.ts diff --git a/dash_table/DataTable.py b/dash_table/DataTable.py index 193f85261..52f956278 100644 --- a/dash_table/DataTable.py +++ b/dash_table/DataTable.py @@ -12,7 +12,19 @@ class DataTable(Component): active. - columns (list; optional): Columns describes various aspects about each individual column. `name` and `id` are the only required parameters. -- locale_format (optional): . locale_format has the following type: dict containing keys 'currency', 'decimal', 'grouping', 'numerals', 'percent', 'thousands'. +- locale_format (optional): The localization specific formatting information applied to all columns in the table. + +This prop is derived from the [d3.formatLocale](https://github.com/d3/d3-format#formatLocale) data structure specification. + +When left unspecified, will use the default value provided by d3-format. + + 'currency': a list of two strings representing the currency + prefix and suffix symbols + 'decimal': the string used for the decimal separator + 'grouping': a list of integers representing the grouping pattern + 'numerals': a list of ten strings used as replacements for numbers 0-9 + 'percent': the string used for the percentage symbol + 'thousands': the string used for the groups separator. locale_format has the following type: dict containing keys 'currency', 'decimal', 'grouping', 'numerals', 'percent', 'thousands'. Those keys have the following types: - currency (list; optional) - decimal (string; optional) diff --git a/dash_table/Format.py b/dash_table/Format.py index 90cb209c9..ad5beacbf 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -72,6 +72,7 @@ class Type(Enum): class Format(): def __init__(self): self.locale = {} + self.nully = '' self.prefix = Prefix.UNDEFINED self.specifier = { 'align': Align.UNDEFINED, @@ -175,6 +176,11 @@ def groups(self, groups: list): self.locale['grouping'] = groups return self + # Nully + def nully(self, nully): + self.nully = nully + return self + # Prefix def si_prefix(self, prefix: Prefix = Prefix.UNDEFINED): prefix = prefix if isinstance(prefix, Prefix) else Prefix.UNDEFINED @@ -183,6 +189,7 @@ def si_prefix(self, prefix: Prefix = Prefix.UNDEFINED): def create(self): f = self.locale.copy() + f['nully'] = self.nully f['prefix'] = self.prefix.value f['specifier'] = '{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}'.format( self.specifier['fill'] if self.specifier['align'] != Align.UNDEFINED else '', diff --git a/dash_table/metadata.json b/dash_table/metadata.json index a59b466db..ee17e7675 100644 --- a/dash_table/metadata.json +++ b/dash_table/metadata.json @@ -2,7 +2,22 @@ "src/dash-table/DataTable.js": { "description": "", "displayName": "DataTable", - "methods": [], + "methods": [ + { + "name": "isValid", + "docblock": null, + "modifiers": [], + "params": [], + "returns": null + }, + { + "name": "allValidColumns", + "docblock": null, + "modifiers": [], + "params": [], + "returns": null + } + ], "props": { "active_cell": { "type": { @@ -57,6 +72,64 @@ "description": "If True, then the name of this column is editable.\nIf there are multiple column headers (if `name` is a list of strings),\nthen `editable_name` can refer to _which_ column header should be\neditable by setting it to the column header index.\nAlso, updating the name in a merged column header cell will\nupdate the name of each column.", "required": false }, + "format": { + "name": "shape", + "value": { + "locale": { + "name": "shape", + "value": { + "currency": { + "name": "arrayOf", + "value": { + "name": "string" + }, + "required": false + }, + "decimal": { + "name": "string", + "required": false + }, + "grouping": { + "name": "arrayOf", + "value": { + "name": "number" + }, + "required": false + }, + "numerals": { + "name": "arrayOf", + "value": { + "name": "string" + }, + "required": false + }, + "percent": { + "name": "string", + "required": false + }, + "thousands": { + "name": "string", + "required": false + } + }, + "required": false + }, + "nully": { + "name": "any", + "required": false + }, + "prefix": { + "name": "number", + "required": false + }, + "specifier": { + "name": "string", + "required": true + } + }, + "description": "The formatting applied to the column's data.\n\nThis prop is derived from the [d3-format](https://github.com/d3/d3-format) library specification. Apart from\nbeing structured slightly differently (under a single prop), the usage\nis the same.\n\n'locale': represents localization specific formatting information\n When left unspecified, will use the default value provided by d3-format.\n\n 'currency': a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': the string used for the decimal separator\n 'grouping': a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': the string used for the percentage symbol\n 'thousands': the string used for the groups separator\n\n'nully': a value that will be used in place of the nully value during formatting\n If the value type matches the column type, it will be formatted normally\n'prefix': a number representing the SI unit to use during formatting\n See `dash_table.Format.Prefix` enumeration for the list of valid values\n'specifier': (mandatory) represents the rules to apply when formatting the number\n\ndash_table.FormatTemplate contains helper functions to rapidly use certain\ntypical number formats.", + "required": false + }, "hidden": { "name": "bool", "description": "If True, then the column and its data is hidden.\nThis can be useful if you want to transport extra\nmeta data (like a data index) to and from callbacks\nbut you don't necessarily want to display that data.", @@ -216,59 +289,6 @@ ], "description": "The data-type of the column's data.\n'numeric': represents both floats and ints\n'text': represents a string\n'datetime': a string representing a date or date-time, in the form:\n 'YYYY-MM-DD HH:MM:SS.ssssss' or some truncation thereof. Years must\n have 4 digits, unless you use `validation.allow_YY: true`. Also\n accepts 'T' or 't' between date and time, and allows timezone info\n at the end. To convert these strings to Python `datetime` objects,\n use `dateutil.parser.isoparse`. In R use `parse_iso_8601` from the\n `parsedate` library.\n WARNING: these parsers do not work with 2-digit years, if you use\n `validation.allow_YY: true` and do not coerce to 4-digit years.\n And parsers that do work with 2-digit years may make a different\n guess about the century than we make on the front end.\n'any': represents any type of data\n\nDefaults to 'any' if undefined.\n\nNOTE: This feature has not been fully implemented.\nIn the future, it's data types will impact things like\ntext formatting options in the cell (e.g. display 2 decimals\nfor a number), filtering options and behavior, and editing\nbehavior.\nStay tuned by following [https://github.com/plotly/dash-table/issues/166](https://github.com/plotly/dash-table/issues/166)", "required": false - }, - "format": { - "name": "shape", - "value": { - "locale": { - "name": "shape", - "value": { - "currency": { - "name": "arrayOf", - "value": { - "name": "string" - }, - "required": false - }, - "decimal": { - "name": "string", - "required": false - }, - "grouping": { - "name": "arrayOf", - "value": { - "name": "number" - }, - "required": false - }, - "numerals": { - "name": "arrayOf", - "value": { - "name": "string" - }, - "required": false - }, - "percent": { - "name": "string", - "required": false - }, - "thousands": { - "name": "string", - "required": false - } - }, - "required": false - }, - "prefix": { - "name": "number", - "required": false - }, - "specifier": { - "name": "string", - "required": false - } - }, - "required": false } } } @@ -320,7 +340,7 @@ } }, "required": false, - "description": "" + "description": "The localization specific formatting information applied to all columns in the table.\n\nThis prop is derived from the [d3.formatLocale](https://github.com/d3/d3-format#formatLocale) data structure specification.\n\nWhen left unspecified, will use the default value provided by d3-format.\n\n 'currency': a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': the string used for the decimal separator\n 'grouping': a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': the string used for the percentage symbol\n 'thousands': the string used for the groups separator" }, "content_style": { "type": { diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index 9682bd45b..f1c8d7400 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -217,9 +217,11 @@ export const propTypes = { * 'percent': the string used for the percentage symbol * 'thousands': the string used for the groups separator * + * 'nully': a value that will be used in place of the nully value during formatting + * If the value type matches the column type, it will be formatted normally * 'prefix': a number representing the SI unit to use during formatting * See `dash_table.Format.Prefix` enumeration for the list of valid values - * 'specifier': represents the rules to apply when formatting the number + * 'specifier': (mandatory) represents the rules to apply when formatting the number * * dash_table.FormatTemplate contains helper functions to rapidly use certain * typical number formats. @@ -233,8 +235,9 @@ export const propTypes = { percent: PropTypes.string, thousands: PropTypes.string }), + nully: PropTypes.any, prefix: PropTypes.number, - specifier: PropTypes.string + specifier: PropTypes.string.isRequired }), /** diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index d98f02211..78d1fc194 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -118,13 +118,17 @@ export interface INumberLocale { thousands: string; } -export type LocaleFormat = INumberLocale | undefined; +export type LocaleFormat = Partial | undefined; + +export type NumberFormat = ({ + locale?: Partial; + nully?: string | number; + prefix?: number; + specifier: string; +}) | undefined; export interface INumberColumn extends ITypeColumn { - format?: { - prefix?: number; - specifier: string; - } & Partial; + format?: NumberFormat; presentation?: Presentation.Input | Presentation.Dropdown; type: ColumnType.Numeric; } diff --git a/src/dash-table/type/formatter.ts b/src/dash-table/type/formatter.ts index ec6039e83..5823d905d 100644 --- a/src/dash-table/type/formatter.ts +++ b/src/dash-table/type/formatter.ts @@ -12,7 +12,7 @@ export default (locale: LocaleFormat) => let formatter; switch (c.type) { case ColumnType.Numeric: - formatter = getNumberFormatter(locale, c); + formatter = getNumberFormatter(locale, c.format); break; } diff --git a/src/dash-table/type/number.ts b/src/dash-table/type/number.ts index df1b5155c..8b45b02c7 100644 --- a/src/dash-table/type/number.ts +++ b/src/dash-table/type/number.ts @@ -2,10 +2,19 @@ import * as R from 'ramda'; import { formatLocale } from 'd3-format'; import isNumeric from 'fast-isnumeric'; -import { INumberColumn, LocaleFormat } from 'dash-table/components/Table/props'; +import { INumberColumn, LocaleFormat, NumberFormat } from 'dash-table/components/Table/props'; import { reconcileNull, isNully } from './null'; import { IReconciliation } from './reconcile'; +const DEFAULT_LOCALE = { + currency: ['$', ''], + decimal: '.', + thousands: ',', + grouping: [3], + percent: '%' +}; +const DEFAULT_NULLY = ''; + export function coerce(value: any, options: INumberColumn | undefined): IReconciliation { return isNumeric(value) ? { success: true, value: +value } : @@ -14,22 +23,39 @@ export function coerce(value: any, options: INumberColumn | undefined): IReconci export function getFormatter( defaultLocale: LocaleFormat, - options: INumberColumn | undefined + format: NumberFormat ) { - if (!options || !options.format) { + if (!format) { return; } const locale = formatLocale( - R.merge( + R.mergeAll([ + DEFAULT_LOCALE, defaultLocale, - R.omit(['specifier', 'prefix'], options.format) - ) + format.locale + ]) ); - return options.format.prefix ? - locale.formatPrefix(options.format.specifier, options.format.prefix) : - locale.format(options.format.specifier); + const numberFormatter = format.prefix ? + locale.formatPrefix(format.specifier, format.prefix) : + locale.format(format.specifier); + + const nully = typeof format.nully === 'undefined' ? + DEFAULT_NULLY : + format.nully; + + return (value: any) => { + if (isNully(value)) { + return typeof nully === 'number' ? + numberFormatter(nully) : + nully; + } else if (typeof value === 'number') { + return numberFormatter(value); + } else { + return value; + } + }; } export function validate(value: any, options: INumberColumn | undefined): IReconciliation { diff --git a/tests/cypress/tests/unit/formatting_test.ts b/tests/cypress/tests/unit/formatting_test.ts new file mode 100644 index 000000000..9dcbc5a40 --- /dev/null +++ b/tests/cypress/tests/unit/formatting_test.ts @@ -0,0 +1,129 @@ +import { getFormatter } from 'dash-table/type/number'; + +describe('formatting', () => { + describe('number', () => { + it('returns undefined formatter with undefined format', () => { + const formatter = getFormatter(undefined, undefined); + assert.isNotOk(formatter); + }); + + describe('without nully handling / default locale', () => { + it('formats currency', () => { + const formatter = getFormatter(undefined, { + specifier: '$.2f' + }); + + assert.isOk(formatter); + + if (formatter) { + expect(formatter(0)).to.equal('$0.00'); + expect(formatter(1)).to.equal('$1.00'); + expect(formatter(-1)).to.equal('-$1.00'); + expect(formatter(1.23)).to.equal('$1.23'); + expect(formatter(1.232)).to.equal('$1.23'); + expect(formatter(1.239)).to.equal('$1.24'); + expect(formatter(1766)).to.equal('$1766.00'); + expect(formatter(''), 'Empty string case').to.equal(''); + expect(formatter('foo'), 'Foo string case').to.equal('foo'); + expect(formatter(true), 'Empty string case').to.equal(true); + expect(formatter(NaN), 'NaN case').to.equal(''); + expect(formatter(Infinity), 'Infinity case').to.equal(''); + expect(formatter(-Infinity), '-Infinity case').to.equal(''); + expect(formatter(null as any), 'null case').to.equal(''); + expect(formatter(undefined as any), 'undef case').to.equal(''); + } + }); + }); + + describe('with nully handling / default locale', () => { + it('formats significant digits and grouping separator', () => { + const formatter = getFormatter(undefined, { + nully: 42.42, + specifier: ',.2r' + }); + + assert.isOk(formatter); + + if (formatter) { + expect(formatter(0)).to.equal('0.0'); + expect(formatter(0.13)).to.equal('0.13'); + expect(formatter(0.131)).to.equal('0.13'); + expect(formatter(1.23)).to.equal('1.2'); + expect(formatter(1.299)).to.equal('1.3'); + expect(formatter(1299)).to.equal('1,300'); + expect(formatter(1299431)).to.equal('1,300,000'); + expect(formatter(''), 'Empty string case').to.equal(''); + expect(formatter('foo'), 'Foo string case').to.equal('foo'); + expect(formatter(true), 'Empty string case').to.equal(true); + expect(formatter(NaN), 'NaN case').to.equal('42'); + expect(formatter(Infinity), 'Infinity case').to.equal('42'); + expect(formatter(-Infinity), '-Infinity case').to.equal('42'); + expect(formatter(null as any), 'null case').to.equal('42'); + expect(formatter(undefined as any), 'undef case').to.equal('42'); + } + }); + }); + + describe('with nully handling / partial locale override', () => { + it('formats significant digits and grouping separator', () => { + const formatter = getFormatter(undefined, { + locale: { + decimal: 'x', + grouping: [2, 1] + }, + nully: '42.4242', + specifier: ',.2f' + }); + + assert.isOk(formatter); + + if (formatter) { + expect(formatter(0)).to.equal('0x00'); + expect(formatter(0.13)).to.equal('0x13'); + expect(formatter(0.131)).to.equal('0x13'); + expect(formatter(1.23)).to.equal('1x23'); + expect(formatter(1.299)).to.equal('1x30'); + expect(formatter(1299)).to.equal('1,2,99x00'); + expect(formatter(1299431)).to.equal('1,2,99,4,31x00'); + expect(formatter(''), 'Empty string case').to.equal(''); + expect(formatter('foo'), 'Foo string case').to.equal('foo'); + expect(formatter(true), 'Empty string case').to.equal(true); + expect(formatter(NaN), 'NaN case').to.equal('42.4242'); + expect(formatter(Infinity), 'Infinity case').to.equal('42.4242'); + expect(formatter(-Infinity), '-Infinity case').to.equal('42.4242'); + expect(formatter(null as any), 'null case').to.equal('42.4242'); + expect(formatter(undefined as any), 'undef case').to.equal('42.4242'); + } + }); + }); + + describe('without nully handling / default locale / si prefix', () => { + it('formats currency', () => { + const formatter = getFormatter(undefined, { + prefix: 0.001, + specifier: '.0f' + }); + + assert.isOk(formatter); + + if (formatter) { + expect(formatter(0)).to.equal('0m'); + expect(formatter(1)).to.equal('1000m'); + expect(formatter(-1)).to.equal('-1000m'); + expect(formatter(1.23)).to.equal('1230m'); + expect(formatter(1.232)).to.equal('1232m'); + expect(formatter(1.239)).to.equal('1239m'); + expect(formatter(1766)).to.equal('1766000m'); + expect(formatter(''), 'Empty string case').to.equal(''); + expect(formatter('foo'), 'Foo string case').to.equal('foo'); + expect(formatter(true), 'Empty string case').to.equal(true); + expect(formatter(NaN), 'NaN case').to.equal(''); + expect(formatter(Infinity), 'Infinity case').to.equal(''); + expect(formatter(-Infinity), '-Infinity case').to.equal(''); + expect(formatter(null as any), 'null case').to.equal(''); + expect(formatter(undefined as any), 'undef case').to.equal(''); + } + }); + }); + }); +}); \ No newline at end of file From 8ecd9969af7c42734d0f7d1469f894233158ab7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 21 Feb 2019 10:55:15 -0500 Subject: [PATCH 14/55] - push data table validation up the stack - push data table (columns) sanitation / BI logic up the stack --- dash_table/metadata.json | 17 +----- src/dash-table/DataTable.js | 68 ++++------------------- src/dash-table/DataTable.sanitize.ts | 50 +++++++++++++++++ src/dash-table/DataTable.validate.ts | 56 +++++++++++++++++++ src/dash-table/components/CellFactory.tsx | 2 - src/dash-table/components/Table/props.ts | 11 ++-- src/dash-table/derived/cell/contents.tsx | 5 +- src/dash-table/type/formatter.ts | 22 ++++---- src/dash-table/type/number.ts | 37 +++--------- 9 files changed, 140 insertions(+), 128 deletions(-) create mode 100644 src/dash-table/DataTable.sanitize.ts create mode 100644 src/dash-table/DataTable.validate.ts diff --git a/dash_table/metadata.json b/dash_table/metadata.json index ee17e7675..08158a854 100644 --- a/dash_table/metadata.json +++ b/dash_table/metadata.json @@ -2,22 +2,7 @@ "src/dash-table/DataTable.js": { "description": "", "displayName": "DataTable", - "methods": [ - { - "name": "isValid", - "docblock": null, - "modifiers": [], - "params": [], - "returns": null - }, - { - "name": "allValidColumns", - "docblock": null, - "modifiers": [], - "params": [], - "returns": null - } - ], + "methods": [], "props": { "active_cell": { "type": { diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index f1c8d7400..a7fc1c84a 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -7,14 +7,8 @@ import RealTable from 'dash-table/components/Table'; import Logger from 'core/Logger'; import genRandomId from './utils/generate'; - -function isFrontEnd(value) { - return ['fe', true, false].indexOf(value) !== -1; -} - -function isBackEnd(value) { - return ['be', false].indexOf(value) !== -1; -} +import isValidProps from './DataTable.validate'; +import sanitizeProps from './DataTable.sanitize'; export default class DataTable extends Component { constructor(props) { @@ -25,64 +19,24 @@ export default class DataTable extends Component { } render() { - if (!this.isValid()) { - Logger.error(`Invalid combination of filtering / sorting / pagination`); + if (isValidProps(this.props)) { + return this.props.id ? () : (); + } else { return (
Invalid props combination
); } - - if (!this.allValidColumns()) { - Logger.error(`Invalid column format`); - return (
Invalid props combination
); - } - - return this.props.id ? () : (); - } - - isValid() { - const { - filtering, - sorting, - pagination_mode - } = this.props; - - return isFrontEnd(pagination_mode) || - (isBackEnd(filtering) && isBackEnd(sorting)); - } - - allValidColumns() { - const { - columns - } = this.props; - - return !R.has(column => - column.format && ( - ( - column.format.currency && - column.format.currency.length !== 2 - ) || ( - column.format.grouping && - column.format.grouping.length === 0 - ) || ( - column.format.numerals && - column.format.numerals.length !== 10 - ) - ), columns); } render() { - if (!this.isValid()) { - Logger.error(`Invalid combination of filtering / sorting / pagination`); + if (!isValidProps(this.props)) { return (
Invalid props combination
); } - if (!this.allValidColumns()) { - Logger.error(`Invalid column format`); - return (
Invalid props combination
); - } - - return this.props.id ? () : (); - } + const sanitizedProps = sanitizeProps(this.props); + return this.props.id ? + () : + (); } +} export const defaultProps = { pagination_mode: 'fe', diff --git a/src/dash-table/DataTable.sanitize.ts b/src/dash-table/DataTable.sanitize.ts new file mode 100644 index 000000000..7f9e0134f --- /dev/null +++ b/src/dash-table/DataTable.sanitize.ts @@ -0,0 +1,50 @@ +import * as R from 'ramda'; + +import { memoizeOne } from 'core/memoizer'; + +import { Columns, ColumnType, INumberLocale } from './components/Table/props'; + +const DEFAULT_LOCALE = { + currency: ['$', ''], + decimal: '.', + thousands: ',', + grouping: [3], + percent: '%' +}; + +const DEFAULT_NULLY = ''; + +const applyDefaultToLocale = memoizeOne((locale: INumberLocale) => + R.merge( + DEFAULT_LOCALE, + locale + ) +); + +const applyDefaultsToColumns = memoizeOne( + (defaultLocale: INumberLocale, columns: Columns) => R.map(column => { + if (column.type === ColumnType.Numeric && column.format) { + column.format.locale = R.mergeAll([ + DEFAULT_LOCALE, + defaultLocale, + column.format.locale + ]); + column.format.nully = column.format.nully === undefined ? + DEFAULT_NULLY : + column.format.nully; + } + return column; + }, columns) +); + +export default (props: any) => { + const locale_format = applyDefaultToLocale(props.locale_format); + + return R.mergeAll([ + props, + { + columns: applyDefaultsToColumns(locale_format, props.columns), + locale_format + } + ]); +}; \ No newline at end of file diff --git a/src/dash-table/DataTable.validate.ts b/src/dash-table/DataTable.validate.ts new file mode 100644 index 000000000..29c3a2e27 --- /dev/null +++ b/src/dash-table/DataTable.validate.ts @@ -0,0 +1,56 @@ +import * as R from 'ramda'; + +import Logger from 'core/Logger'; + +function isFrontEnd(value: any) { + return ['fe', true, false].indexOf(value) !== -1; +} + +function isBackEnd(value: any) { + return ['be', false].indexOf(value) !== -1; +} + +function validColumns(props: any) { + const { + columns + } = props; + + return !R.any((column: any) => + column.format && ( + ( + column.format.currency && + column.format.currency.length !== 2 + ) || ( + column.format.grouping && + column.format.grouping.length === 0 + ) || ( + column.format.numerals && + column.format.numerals.length !== 10 + ) + ))(columns); +} + +function validFSP(props: any) { + const { + filtering, + sorting, + pagination_mode + } = props; + + return isFrontEnd(pagination_mode) || + (isBackEnd(filtering) && isBackEnd(sorting)); +} + +export default (props: any): boolean => { + if (!validFSP(props)) { + Logger.error(`Invalid combination of filtering / sorting / pagination`); + return false; + } + + if (!validColumns(props)) { + Logger.error(`Invalid column format`); + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/dash-table/components/CellFactory.tsx b/src/dash-table/components/CellFactory.tsx index 957836f77..217606cf0 100644 --- a/src/dash-table/components/CellFactory.tsx +++ b/src/dash-table/components/CellFactory.tsx @@ -37,7 +37,6 @@ export default class CellFactory { dropdown_properties, // legacy editable, is_focused, - locale_format, row_deletable, row_selectable, selected_cells, @@ -95,7 +94,6 @@ export default class CellFactory { const contents = this.cellContents( active_cell, columns, - locale_format, virtualized.data, virtualized.offset, editable, diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 78d1fc194..1853fb2c6 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -113,16 +113,14 @@ export interface INumberLocale { currency: [string, string]; decimal: string; grouping: number[]; - numerals: string[]; + numerals?: string[]; percent: string; thousands: string; } -export type LocaleFormat = Partial | undefined; - export type NumberFormat = ({ - locale?: Partial; - nully?: string | number; + locale: INumberLocale; + nully: any; prefix?: number; specifier: string; }) | undefined; @@ -270,7 +268,7 @@ interface IProps { filtering_settings?: string; filtering_type?: FilteringType; filtering_types?: FilteringType[]; - locale_format?: LocaleFormat; + locale_format: INumberLocale; merge_duplicate_headers?: boolean; navigation?: Navigation; n_fixed_columns?: number; @@ -382,7 +380,6 @@ export interface ICellFactoryProps { editable: boolean; id: string; is_focused?: boolean; - locale_format?: LocaleFormat; n_fixed_columns: number; n_fixed_rows: number; paginator: IPaginator; diff --git a/src/dash-table/derived/cell/contents.tsx b/src/dash-table/derived/cell/contents.tsx index 4e2027534..4a7a282ee 100644 --- a/src/dash-table/derived/cell/contents.tsx +++ b/src/dash-table/derived/cell/contents.tsx @@ -9,7 +9,6 @@ import { ICellFactoryProps, IViewportOffset, IVisibleColumn, - LocaleFormat, Presentation, VisibleColumns } from 'dash-table/components/Table/props'; @@ -60,15 +59,13 @@ class Contents { get = memoizeOne(( activeCell: ActiveCell, columns: VisibleColumns, - defaultLocale: LocaleFormat, data: Data, offset: IViewportOffset, editable: boolean, isFocused: boolean, dropdowns: (DropdownValues | undefined)[][] ): JSX.Element[][] => { - const localizedFormatter = getFormatter(defaultLocale); - const formatters = R.map(localizedFormatter, columns); + const formatters = R.map(getFormatter, columns); return mapData( (datum, rowIndex) => mapRow( diff --git a/src/dash-table/type/formatter.ts b/src/dash-table/type/formatter.ts index 5823d905d..0fae8667d 100644 --- a/src/dash-table/type/formatter.ts +++ b/src/dash-table/type/formatter.ts @@ -1,20 +1,18 @@ import { ColumnType, - IColumnType, - LocaleFormat + IColumnType } from 'dash-table/components/Table/props'; import { getFormatter as getNumberFormatter } from './number'; const DEFAULT_FORMATTER = (value: any) => value; -export default (locale: LocaleFormat) => - (c: IColumnType) => { - let formatter; - switch (c.type) { - case ColumnType.Numeric: - formatter = getNumberFormatter(locale, c.format); - break; - } +export default (c: IColumnType) => { + let formatter; + switch (c.type) { + case ColumnType.Numeric: + formatter = getNumberFormatter(c.format); + break; + } - return formatter || DEFAULT_FORMATTER; - }; \ No newline at end of file + return formatter || DEFAULT_FORMATTER; +}; \ No newline at end of file diff --git a/src/dash-table/type/number.ts b/src/dash-table/type/number.ts index 8b45b02c7..211ca2a50 100644 --- a/src/dash-table/type/number.ts +++ b/src/dash-table/type/number.ts @@ -1,55 +1,32 @@ -import * as R from 'ramda'; import { formatLocale } from 'd3-format'; import isNumeric from 'fast-isnumeric'; -import { INumberColumn, LocaleFormat, NumberFormat } from 'dash-table/components/Table/props'; +import { INumberColumn, NumberFormat } from 'dash-table/components/Table/props'; import { reconcileNull, isNully } from './null'; import { IReconciliation } from './reconcile'; -const DEFAULT_LOCALE = { - currency: ['$', ''], - decimal: '.', - thousands: ',', - grouping: [3], - percent: '%' -}; -const DEFAULT_NULLY = ''; - export function coerce(value: any, options: INumberColumn | undefined): IReconciliation { return isNumeric(value) ? { success: true, value: +value } : reconcileNull(value, options); } -export function getFormatter( - defaultLocale: LocaleFormat, - format: NumberFormat -) { +export function getFormatter(format: NumberFormat) { if (!format) { - return; + return (value: any) => value; } - const locale = formatLocale( - R.mergeAll([ - DEFAULT_LOCALE, - defaultLocale, - format.locale - ]) - ); + const locale = formatLocale(format.locale); const numberFormatter = format.prefix ? locale.formatPrefix(format.specifier, format.prefix) : locale.format(format.specifier); - const nully = typeof format.nully === 'undefined' ? - DEFAULT_NULLY : - format.nully; - return (value: any) => { if (isNully(value)) { - return typeof nully === 'number' ? - numberFormatter(nully) : - nully; + return typeof format.nully === 'number' ? + numberFormatter(format.nully) : + format.nully; } else if (typeof value === 'number') { return numberFormatter(value); } else { From df174fb8a72f0ba5700710e240b9aae51c618bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 21 Feb 2019 10:57:39 -0500 Subject: [PATCH 15/55] fix lint --- src/dash-table/DataTable.validate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dash-table/DataTable.validate.ts b/src/dash-table/DataTable.validate.ts index 29c3a2e27..81cd5df17 100644 --- a/src/dash-table/DataTable.validate.ts +++ b/src/dash-table/DataTable.validate.ts @@ -53,4 +53,4 @@ export default (props: any): boolean => { } return true; -} \ No newline at end of file +}; \ No newline at end of file From 36c9155bd444a88e28f197e41094863737805314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 21 Feb 2019 11:18:45 -0500 Subject: [PATCH 16/55] - isolate column sanitation (clone) - update formatting tests --- src/dash-table/DataTable.sanitize.ts | 37 ++--- tests/cypress/tests/unit/formatting_test.ts | 159 ++++++++++---------- 2 files changed, 100 insertions(+), 96 deletions(-) diff --git a/src/dash-table/DataTable.sanitize.ts b/src/dash-table/DataTable.sanitize.ts index 7f9e0134f..0cabca4e1 100644 --- a/src/dash-table/DataTable.sanitize.ts +++ b/src/dash-table/DataTable.sanitize.ts @@ -4,7 +4,7 @@ import { memoizeOne } from 'core/memoizer'; import { Columns, ColumnType, INumberLocale } from './components/Table/props'; -const DEFAULT_LOCALE = { +const DEFAULT_LOCALE: INumberLocale = { currency: ['$', ''], decimal: '.', thousands: ',', @@ -14,26 +14,17 @@ const DEFAULT_LOCALE = { const DEFAULT_NULLY = ''; -const applyDefaultToLocale = memoizeOne((locale: INumberLocale) => - R.merge( - DEFAULT_LOCALE, - locale - ) -); +const applyDefaultToLocale = memoizeOne((locale: INumberLocale) => getLocale(locale)); const applyDefaultsToColumns = memoizeOne( (defaultLocale: INumberLocale, columns: Columns) => R.map(column => { - if (column.type === ColumnType.Numeric && column.format) { - column.format.locale = R.mergeAll([ - DEFAULT_LOCALE, - defaultLocale, - column.format.locale - ]); - column.format.nully = column.format.nully === undefined ? - DEFAULT_NULLY : - column.format.nully; + const c = R.clone(column); + + if (c.type === ColumnType.Numeric && c.format) { + c.format.locale = getLocale(defaultLocale, c.format.locale); + c.format.nully = getNully(c.format.nully); } - return column; + return c; }, columns) ); @@ -47,4 +38,14 @@ export default (props: any) => { locale_format } ]); -}; \ No newline at end of file +}; + +export const getLocale = (...locales: Partial[]): INumberLocale => + R.mergeAll([ + DEFAULT_LOCALE, + ...locales + ]); + +export const getNully = (nully?: any) => nully === undefined ? + DEFAULT_NULLY : + nully; \ No newline at end of file diff --git a/tests/cypress/tests/unit/formatting_test.ts b/tests/cypress/tests/unit/formatting_test.ts index 9dcbc5a40..eef056287 100644 --- a/tests/cypress/tests/unit/formatting_test.ts +++ b/tests/cypress/tests/unit/formatting_test.ts @@ -1,128 +1,131 @@ import { getFormatter } from 'dash-table/type/number'; +import { getLocale, getNully } from 'dash-table/DataTable.sanitize'; describe('formatting', () => { describe('number', () => { - it('returns undefined formatter with undefined format', () => { - const formatter = getFormatter(undefined, undefined); - assert.isNotOk(formatter); + it('returns value with undefined format', () => { + const formatter = getFormatter(undefined); + assert.isOk(formatter); + + expect(formatter(0)).to.equal(0); + expect(formatter(1.766)).to.equal(1.766); + expect(formatter('foo')).to.equal('foo'); + expect(isNaN(formatter(NaN))).to.equal(true); }); describe('without nully handling / default locale', () => { it('formats currency', () => { - const formatter = getFormatter(undefined, { + const formatter = getFormatter({ + locale: getLocale(), + nully: getNully(), specifier: '$.2f' }); assert.isOk(formatter); - if (formatter) { - expect(formatter(0)).to.equal('$0.00'); - expect(formatter(1)).to.equal('$1.00'); - expect(formatter(-1)).to.equal('-$1.00'); - expect(formatter(1.23)).to.equal('$1.23'); - expect(formatter(1.232)).to.equal('$1.23'); - expect(formatter(1.239)).to.equal('$1.24'); - expect(formatter(1766)).to.equal('$1766.00'); - expect(formatter(''), 'Empty string case').to.equal(''); - expect(formatter('foo'), 'Foo string case').to.equal('foo'); - expect(formatter(true), 'Empty string case').to.equal(true); - expect(formatter(NaN), 'NaN case').to.equal(''); - expect(formatter(Infinity), 'Infinity case').to.equal(''); - expect(formatter(-Infinity), '-Infinity case').to.equal(''); - expect(formatter(null as any), 'null case').to.equal(''); - expect(formatter(undefined as any), 'undef case').to.equal(''); - } + expect(formatter(0)).to.equal('$0.00'); + expect(formatter(1)).to.equal('$1.00'); + expect(formatter(-1)).to.equal('-$1.00'); + expect(formatter(1.23)).to.equal('$1.23'); + expect(formatter(1.232)).to.equal('$1.23'); + expect(formatter(1.239)).to.equal('$1.24'); + expect(formatter(1766)).to.equal('$1766.00'); + expect(formatter(''), 'Empty string case').to.equal(''); + expect(formatter('foo'), 'Foo string case').to.equal('foo'); + expect(formatter(true), 'Empty string case').to.equal(true); + expect(formatter(NaN), 'NaN case').to.equal(''); + expect(formatter(Infinity), 'Infinity case').to.equal(''); + expect(formatter(-Infinity), '-Infinity case').to.equal(''); + expect(formatter(null as any), 'null case').to.equal(''); + expect(formatter(undefined as any), 'undef case').to.equal(''); }); }); describe('with nully handling / default locale', () => { it('formats significant digits and grouping separator', () => { - const formatter = getFormatter(undefined, { + const formatter = getFormatter({ + locale: getLocale(), nully: 42.42, specifier: ',.2r' }); assert.isOk(formatter); - if (formatter) { - expect(formatter(0)).to.equal('0.0'); - expect(formatter(0.13)).to.equal('0.13'); - expect(formatter(0.131)).to.equal('0.13'); - expect(formatter(1.23)).to.equal('1.2'); - expect(formatter(1.299)).to.equal('1.3'); - expect(formatter(1299)).to.equal('1,300'); - expect(formatter(1299431)).to.equal('1,300,000'); - expect(formatter(''), 'Empty string case').to.equal(''); - expect(formatter('foo'), 'Foo string case').to.equal('foo'); - expect(formatter(true), 'Empty string case').to.equal(true); - expect(formatter(NaN), 'NaN case').to.equal('42'); - expect(formatter(Infinity), 'Infinity case').to.equal('42'); - expect(formatter(-Infinity), '-Infinity case').to.equal('42'); - expect(formatter(null as any), 'null case').to.equal('42'); - expect(formatter(undefined as any), 'undef case').to.equal('42'); - } + expect(formatter(0)).to.equal('0.0'); + expect(formatter(0.13)).to.equal('0.13'); + expect(formatter(0.131)).to.equal('0.13'); + expect(formatter(1.23)).to.equal('1.2'); + expect(formatter(1.299)).to.equal('1.3'); + expect(formatter(1299)).to.equal('1,300'); + expect(formatter(1299431)).to.equal('1,300,000'); + expect(formatter(''), 'Empty string case').to.equal(''); + expect(formatter('foo'), 'Foo string case').to.equal('foo'); + expect(formatter(true), 'Empty string case').to.equal(true); + expect(formatter(NaN), 'NaN case').to.equal('42'); + expect(formatter(Infinity), 'Infinity case').to.equal('42'); + expect(formatter(-Infinity), '-Infinity case').to.equal('42'); + expect(formatter(null as any), 'null case').to.equal('42'); + expect(formatter(undefined as any), 'undef case').to.equal('42'); }); }); describe('with nully handling / partial locale override', () => { it('formats significant digits and grouping separator', () => { - const formatter = getFormatter(undefined, { - locale: { + const formatter = getFormatter({ + locale: getLocale({ decimal: 'x', grouping: [2, 1] - }, - nully: '42.4242', + }), + nully: getNully('42.4242'), specifier: ',.2f' }); assert.isOk(formatter); - if (formatter) { - expect(formatter(0)).to.equal('0x00'); - expect(formatter(0.13)).to.equal('0x13'); - expect(formatter(0.131)).to.equal('0x13'); - expect(formatter(1.23)).to.equal('1x23'); - expect(formatter(1.299)).to.equal('1x30'); - expect(formatter(1299)).to.equal('1,2,99x00'); - expect(formatter(1299431)).to.equal('1,2,99,4,31x00'); - expect(formatter(''), 'Empty string case').to.equal(''); - expect(formatter('foo'), 'Foo string case').to.equal('foo'); - expect(formatter(true), 'Empty string case').to.equal(true); - expect(formatter(NaN), 'NaN case').to.equal('42.4242'); - expect(formatter(Infinity), 'Infinity case').to.equal('42.4242'); - expect(formatter(-Infinity), '-Infinity case').to.equal('42.4242'); - expect(formatter(null as any), 'null case').to.equal('42.4242'); - expect(formatter(undefined as any), 'undef case').to.equal('42.4242'); - } + expect(formatter(0)).to.equal('0x00'); + expect(formatter(0.13)).to.equal('0x13'); + expect(formatter(0.131)).to.equal('0x13'); + expect(formatter(1.23)).to.equal('1x23'); + expect(formatter(1.299)).to.equal('1x30'); + expect(formatter(1299)).to.equal('1,2,99x00'); + expect(formatter(1299431)).to.equal('1,2,99,4,31x00'); + expect(formatter(''), 'Empty string case').to.equal(''); + expect(formatter('foo'), 'Foo string case').to.equal('foo'); + expect(formatter(true), 'Empty string case').to.equal(true); + expect(formatter(NaN), 'NaN case').to.equal('42.4242'); + expect(formatter(Infinity), 'Infinity case').to.equal('42.4242'); + expect(formatter(-Infinity), '-Infinity case').to.equal('42.4242'); + expect(formatter(null as any), 'null case').to.equal('42.4242'); + expect(formatter(undefined as any), 'undef case').to.equal('42.4242'); }); }); describe('without nully handling / default locale / si prefix', () => { it('formats currency', () => { - const formatter = getFormatter(undefined, { + const formatter = getFormatter({ + locale: getLocale(), + nully: getNully(), prefix: 0.001, specifier: '.0f' }); assert.isOk(formatter); - if (formatter) { - expect(formatter(0)).to.equal('0m'); - expect(formatter(1)).to.equal('1000m'); - expect(formatter(-1)).to.equal('-1000m'); - expect(formatter(1.23)).to.equal('1230m'); - expect(formatter(1.232)).to.equal('1232m'); - expect(formatter(1.239)).to.equal('1239m'); - expect(formatter(1766)).to.equal('1766000m'); - expect(formatter(''), 'Empty string case').to.equal(''); - expect(formatter('foo'), 'Foo string case').to.equal('foo'); - expect(formatter(true), 'Empty string case').to.equal(true); - expect(formatter(NaN), 'NaN case').to.equal(''); - expect(formatter(Infinity), 'Infinity case').to.equal(''); - expect(formatter(-Infinity), '-Infinity case').to.equal(''); - expect(formatter(null as any), 'null case').to.equal(''); - expect(formatter(undefined as any), 'undef case').to.equal(''); - } + expect(formatter(0)).to.equal('0m'); + expect(formatter(1)).to.equal('1000m'); + expect(formatter(-1)).to.equal('-1000m'); + expect(formatter(1.23)).to.equal('1230m'); + expect(formatter(1.232)).to.equal('1232m'); + expect(formatter(1.239)).to.equal('1239m'); + expect(formatter(1766)).to.equal('1766000m'); + expect(formatter(''), 'Empty string case').to.equal(''); + expect(formatter('foo'), 'Foo string case').to.equal('foo'); + expect(formatter(true), 'Empty string case').to.equal(true); + expect(formatter(NaN), 'NaN case').to.equal(''); + expect(formatter(Infinity), 'Infinity case').to.equal(''); + expect(formatter(-Infinity), '-Infinity case').to.equal(''); + expect(formatter(null as any), 'null case').to.equal(''); + expect(formatter(undefined as any), 'undef case').to.equal(''); }); }); }); From fb0d29e9e689bc8b7111ba7857f51985f3aad0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 21 Feb 2019 11:53:29 -0500 Subject: [PATCH 17/55] - isolate d3 idiosyncracy (thousands) from API --- dash_table/DataTable.py | 6 +++--- dash_table/metadata.json | 20 ++++++++++---------- src/dash-table/DataTable.js | 12 ++++++------ src/dash-table/DataTable.sanitize.ts | 6 +++--- src/dash-table/components/Table/props.ts | 2 +- src/dash-table/type/number.ts | 9 +++++++-- tests/cypress/tests/unit/formatting_test.ts | 5 +++-- 7 files changed, 33 insertions(+), 27 deletions(-) diff --git a/dash_table/DataTable.py b/dash_table/DataTable.py index 52f956278..6901bfa94 100644 --- a/dash_table/DataTable.py +++ b/dash_table/DataTable.py @@ -21,17 +21,17 @@ class DataTable(Component): 'currency': a list of two strings representing the currency prefix and suffix symbols 'decimal': the string used for the decimal separator + 'group': the string used for the groups separator 'grouping': a list of integers representing the grouping pattern 'numerals': a list of ten strings used as replacements for numbers 0-9 - 'percent': the string used for the percentage symbol - 'thousands': the string used for the groups separator. locale_format has the following type: dict containing keys 'currency', 'decimal', 'grouping', 'numerals', 'percent', 'thousands'. + 'percent': the string used for the percentage symbol. locale_format has the following type: dict containing keys 'currency', 'decimal', 'group', 'grouping', 'numerals', 'percent'. Those keys have the following types: - currency (list; optional) - decimal (string; optional) + - group (string; optional) - grouping (list; optional) - numerals (list; optional) - percent (string; optional) - - thousands (string; optional) - content_style (a value equal to: 'fit', 'grow'; optional): `content_style` toggles between a set of CSS styles for two common behaviors: - `fit`: The table container's width be equal to the width of its content. diff --git a/dash_table/metadata.json b/dash_table/metadata.json index 08158a854..2f2366114 100644 --- a/dash_table/metadata.json +++ b/dash_table/metadata.json @@ -74,6 +74,10 @@ "name": "string", "required": false }, + "group": { + "name": "string", + "required": false + }, "grouping": { "name": "arrayOf", "value": { @@ -91,10 +95,6 @@ "percent": { "name": "string", "required": false - }, - "thousands": { - "name": "string", - "required": false } }, "required": false @@ -112,7 +112,7 @@ "required": true } }, - "description": "The formatting applied to the column's data.\n\nThis prop is derived from the [d3-format](https://github.com/d3/d3-format) library specification. Apart from\nbeing structured slightly differently (under a single prop), the usage\nis the same.\n\n'locale': represents localization specific formatting information\n When left unspecified, will use the default value provided by d3-format.\n\n 'currency': a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': the string used for the decimal separator\n 'grouping': a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': the string used for the percentage symbol\n 'thousands': the string used for the groups separator\n\n'nully': a value that will be used in place of the nully value during formatting\n If the value type matches the column type, it will be formatted normally\n'prefix': a number representing the SI unit to use during formatting\n See `dash_table.Format.Prefix` enumeration for the list of valid values\n'specifier': (mandatory) represents the rules to apply when formatting the number\n\ndash_table.FormatTemplate contains helper functions to rapidly use certain\ntypical number formats.", + "description": "The formatting applied to the column's data.\n\nThis prop is derived from the [d3-format](https://github.com/d3/d3-format) library specification. Apart from\nbeing structured slightly differently (under a single prop), the usage\nis the same.\n\n'locale': represents localization specific formatting information\n When left unspecified, will use the default value provided by d3-format.\n\n 'currency': a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': the string used for the decimal separator\n 'group': the string used for the groups separator\n 'grouping': a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': the string used for the percentage symbol\n\n'nully': a value that will be used in place of the nully value during formatting\n If the value type matches the column type, it will be formatted normally\n'prefix': a number representing the SI unit to use during formatting\n See `dash_table.Format.Prefix` enumeration for the list of valid values\n'specifier': (mandatory) represents the rules to apply when formatting the number\n\ndash_table.FormatTemplate contains helper functions to rapidly use certain\ntypical number formats.", "required": false }, "hidden": { @@ -300,6 +300,10 @@ "name": "string", "required": false }, + "group": { + "name": "string", + "required": false + }, "grouping": { "name": "arrayOf", "value": { @@ -317,15 +321,11 @@ "percent": { "name": "string", "required": false - }, - "thousands": { - "name": "string", - "required": false } } }, "required": false, - "description": "The localization specific formatting information applied to all columns in the table.\n\nThis prop is derived from the [d3.formatLocale](https://github.com/d3/d3-format#formatLocale) data structure specification.\n\nWhen left unspecified, will use the default value provided by d3-format.\n\n 'currency': a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': the string used for the decimal separator\n 'grouping': a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': the string used for the percentage symbol\n 'thousands': the string used for the groups separator" + "description": "The localization specific formatting information applied to all columns in the table.\n\nThis prop is derived from the [d3.formatLocale](https://github.com/d3/d3-format#formatLocale) data structure specification.\n\nWhen left unspecified, will use the default value provided by d3-format.\n\n 'currency': a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': the string used for the decimal separator\n 'group': the string used for the groups separator\n 'grouping': a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': the string used for the percentage symbol" }, "content_style": { "type": { diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index a7fc1c84a..3d2924b80 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -166,10 +166,10 @@ export const propTypes = { * 'currency': a list of two strings representing the currency * prefix and suffix symbols * 'decimal': the string used for the decimal separator + * 'group': the string used for the groups separator * 'grouping': a list of integers representing the grouping pattern * 'numerals': a list of ten strings used as replacements for numbers 0-9 * 'percent': the string used for the percentage symbol - * 'thousands': the string used for the groups separator * * 'nully': a value that will be used in place of the nully value during formatting * If the value type matches the column type, it will be formatted normally @@ -184,10 +184,10 @@ export const propTypes = { locale: PropTypes.shape({ currency: PropTypes.arrayOf(PropTypes.string), decimal: PropTypes.string, + group: PropTypes.string, grouping: PropTypes.arrayOf(PropTypes.number), numerals: PropTypes.arrayOf(PropTypes.string), - percent: PropTypes.string, - thousands: PropTypes.string + percent: PropTypes.string }), nully: PropTypes.any, prefix: PropTypes.number, @@ -324,18 +324,18 @@ export const propTypes = { * 'currency': a list of two strings representing the currency * prefix and suffix symbols * 'decimal': the string used for the decimal separator + * 'group': the string used for the groups separator * 'grouping': a list of integers representing the grouping pattern * 'numerals': a list of ten strings used as replacements for numbers 0-9 * 'percent': the string used for the percentage symbol - * 'thousands': the string used for the groups separator */ locale_format: PropTypes.shape({ currency: PropTypes.arrayOf(PropTypes.string), decimal: PropTypes.string, + group: PropTypes.string, grouping: PropTypes.arrayOf(PropTypes.number), numerals: PropTypes.arrayOf(PropTypes.string), - percent: PropTypes.string, - thousands: PropTypes.string + percent: PropTypes.string }), /** diff --git a/src/dash-table/DataTable.sanitize.ts b/src/dash-table/DataTable.sanitize.ts index 0cabca4e1..e3a8deff2 100644 --- a/src/dash-table/DataTable.sanitize.ts +++ b/src/dash-table/DataTable.sanitize.ts @@ -4,10 +4,10 @@ import { memoizeOne } from 'core/memoizer'; import { Columns, ColumnType, INumberLocale } from './components/Table/props'; -const DEFAULT_LOCALE: INumberLocale = { +const D3_DEFAULT_LOCALE: INumberLocale = { currency: ['$', ''], decimal: '.', - thousands: ',', + group: ',', grouping: [3], percent: '%' }; @@ -42,7 +42,7 @@ export default (props: any) => { export const getLocale = (...locales: Partial[]): INumberLocale => R.mergeAll([ - DEFAULT_LOCALE, + D3_DEFAULT_LOCALE, ...locales ]); diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 1853fb2c6..173cd720f 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -112,10 +112,10 @@ export interface ITypeColumn { export interface INumberLocale { currency: [string, string]; decimal: string; + group: string; grouping: number[]; numerals?: string[]; percent: string; - thousands: string; } export type NumberFormat = ({ diff --git a/src/dash-table/type/number.ts b/src/dash-table/type/number.ts index 211ca2a50..aabdcb228 100644 --- a/src/dash-table/type/number.ts +++ b/src/dash-table/type/number.ts @@ -1,10 +1,15 @@ import { formatLocale } from 'd3-format'; import isNumeric from 'fast-isnumeric'; -import { INumberColumn, NumberFormat } from 'dash-table/components/Table/props'; +import { INumberColumn, INumberLocale, NumberFormat } from 'dash-table/components/Table/props'; import { reconcileNull, isNully } from './null'; import { IReconciliation } from './reconcile'; +const convertToD3 = ({ group, ...others }: INumberLocale) => ({ + thousands: group, + ...others +}); + export function coerce(value: any, options: INumberColumn | undefined): IReconciliation { return isNumeric(value) ? { success: true, value: +value } : @@ -16,7 +21,7 @@ export function getFormatter(format: NumberFormat) { return (value: any) => value; } - const locale = formatLocale(format.locale); + const locale = formatLocale(convertToD3(format.locale)); const numberFormatter = format.prefix ? locale.formatPrefix(format.specifier, format.prefix) : diff --git a/tests/cypress/tests/unit/formatting_test.ts b/tests/cypress/tests/unit/formatting_test.ts index eef056287..4d55fe2b4 100644 --- a/tests/cypress/tests/unit/formatting_test.ts +++ b/tests/cypress/tests/unit/formatting_test.ts @@ -74,6 +74,7 @@ describe('formatting', () => { const formatter = getFormatter({ locale: getLocale({ decimal: 'x', + group: 'y', grouping: [2, 1] }), nully: getNully('42.4242'), @@ -87,8 +88,8 @@ describe('formatting', () => { expect(formatter(0.131)).to.equal('0x13'); expect(formatter(1.23)).to.equal('1x23'); expect(formatter(1.299)).to.equal('1x30'); - expect(formatter(1299)).to.equal('1,2,99x00'); - expect(formatter(1299431)).to.equal('1,2,99,4,31x00'); + expect(formatter(1299)).to.equal('1y2y99x00'); + expect(formatter(1299431)).to.equal('1y2y99y4y31x00'); expect(formatter(''), 'Empty string case').to.equal(''); expect(formatter('foo'), 'Foo string case').to.equal('foo'); expect(formatter(true), 'Empty string case').to.equal(true); From 66190662b33d19ce0eccea10eaa4fbfad4794171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 21 Feb 2019 13:09:04 -0500 Subject: [PATCH 18/55] - separate thousands nested prop, tests and defaults --- dash_table/DataTable.py | 14 ++++---- dash_table/metadata.json | 12 +++++-- src/dash-table/DataTable.js | 38 +++++++++------------ src/dash-table/DataTable.sanitize.ts | 3 +- src/dash-table/components/Table/props.ts | 1 + src/dash-table/type/number.ts | 27 +++++++++------ tests/cypress/tests/unit/formatting_test.ts | 20 +++++++++++ 7 files changed, 75 insertions(+), 40 deletions(-) diff --git a/dash_table/DataTable.py b/dash_table/DataTable.py index 6901bfa94..c4c2456f2 100644 --- a/dash_table/DataTable.py +++ b/dash_table/DataTable.py @@ -16,15 +16,16 @@ class DataTable(Component): This prop is derived from the [d3.formatLocale](https://github.com/d3/d3-format#formatLocale) data structure specification. -When left unspecified, will use the default value provided by d3-format. +When left unspecified, each individual nested prop will default to a pre-determined value. - 'currency': a list of two strings representing the currency + 'currency': (default: '$') a list of two strings representing the currency prefix and suffix symbols - 'decimal': the string used for the decimal separator - 'group': the string used for the groups separator - 'grouping': a list of integers representing the grouping pattern + 'decimal': (default: '.') the string used for the decimal separator + 'group': (default: ',') the string used for the groups separator + 'grouping': (default: [3]) a list of integers representing the grouping pattern 'numerals': a list of ten strings used as replacements for numbers 0-9 - 'percent': the string used for the percentage symbol. locale_format has the following type: dict containing keys 'currency', 'decimal', 'group', 'grouping', 'numerals', 'percent'. + 'percent': (default: '%') the string used for the percentage symbol + 'separate_4digits': (default: True) separate integers with 4-digits or less. locale_format has the following type: dict containing keys 'currency', 'decimal', 'group', 'grouping', 'numerals', 'percent', 'separate_4digits'. Those keys have the following types: - currency (list; optional) - decimal (string; optional) @@ -32,6 +33,7 @@ class DataTable(Component): - grouping (list; optional) - numerals (list; optional) - percent (string; optional) + - separate_4digits (boolean; optional) - content_style (a value equal to: 'fit', 'grow'; optional): `content_style` toggles between a set of CSS styles for two common behaviors: - `fit`: The table container's width be equal to the width of its content. diff --git a/dash_table/metadata.json b/dash_table/metadata.json index 2f2366114..9886a2fdc 100644 --- a/dash_table/metadata.json +++ b/dash_table/metadata.json @@ -95,6 +95,10 @@ "percent": { "name": "string", "required": false + }, + "separate_4digits": { + "name": "bool", + "required": false } }, "required": false @@ -112,7 +116,7 @@ "required": true } }, - "description": "The formatting applied to the column's data.\n\nThis prop is derived from the [d3-format](https://github.com/d3/d3-format) library specification. Apart from\nbeing structured slightly differently (under a single prop), the usage\nis the same.\n\n'locale': represents localization specific formatting information\n When left unspecified, will use the default value provided by d3-format.\n\n 'currency': a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': the string used for the decimal separator\n 'group': the string used for the groups separator\n 'grouping': a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': the string used for the percentage symbol\n\n'nully': a value that will be used in place of the nully value during formatting\n If the value type matches the column type, it will be formatted normally\n'prefix': a number representing the SI unit to use during formatting\n See `dash_table.Format.Prefix` enumeration for the list of valid values\n'specifier': (mandatory) represents the rules to apply when formatting the number\n\ndash_table.FormatTemplate contains helper functions to rapidly use certain\ntypical number formats.", + "description": "The formatting applied to the column's data.\n\nThis prop is derived from the [d3-format](https://github.com/d3/d3-format) library specification. Apart from\nbeing structured slightly differently (under a single prop), the usage\nis the same.\n\n'locale': represents localization specific formatting information\n When left unspecified, will use the default value provided by d3-format.\n\n 'currency': (default: '$') a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': (default: '.') the string used for the decimal separator\n 'group': (default: ',') the string used for the groups separator\n 'grouping': (default: [3]) a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': (default: '%') the string used for the percentage symbol\n 'separate_4digits': (default: True) separate integers with 4-digits or less\n\n'nully': a value that will be used in place of the nully value during formatting\n If the value type matches the column type, it will be formatted normally\n'prefix': a number representing the SI unit to use during formatting\n See `dash_table.Format.Prefix` enumeration for the list of valid values\n'specifier': (mandatory) represents the rules to apply when formatting the number\n\ndash_table.FormatTemplate contains helper functions to rapidly use certain\ntypical number formats.", "required": false }, "hidden": { @@ -321,11 +325,15 @@ "percent": { "name": "string", "required": false + }, + "separate_4digits": { + "name": "bool", + "required": false } } }, "required": false, - "description": "The localization specific formatting information applied to all columns in the table.\n\nThis prop is derived from the [d3.formatLocale](https://github.com/d3/d3-format#formatLocale) data structure specification.\n\nWhen left unspecified, will use the default value provided by d3-format.\n\n 'currency': a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': the string used for the decimal separator\n 'group': the string used for the groups separator\n 'grouping': a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': the string used for the percentage symbol" + "description": "The localization specific formatting information applied to all columns in the table.\n\nThis prop is derived from the [d3.formatLocale](https://github.com/d3/d3-format#formatLocale) data structure specification.\n\nWhen left unspecified, each individual nested prop will default to a pre-determined value.\n\n 'currency': (default: '$') a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': (default: '.') the string used for the decimal separator\n 'group': (default: ',') the string used for the groups separator\n 'grouping': (default: [3]) a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': (default: '%') the string used for the percentage symbol\n 'separate_4digits': (default: True) separate integers with 4-digits or less" }, "content_style": { "type": { diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index 3d2924b80..de65b36d2 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -18,14 +18,6 @@ export default class DataTable extends Component { this.getId = () => (id = id || genRandomId('table-')); } - render() { - if (isValidProps(this.props)) { - return this.props.id ? () : (); - } else { - return (
Invalid props combination
); - } - } - render() { if (!isValidProps(this.props)) { return (
Invalid props combination
); @@ -163,13 +155,14 @@ export const propTypes = { * 'locale': represents localization specific formatting information * When left unspecified, will use the default value provided by d3-format. * - * 'currency': a list of two strings representing the currency + * 'currency': (default: '$') a list of two strings representing the currency * prefix and suffix symbols - * 'decimal': the string used for the decimal separator - * 'group': the string used for the groups separator - * 'grouping': a list of integers representing the grouping pattern + * 'decimal': (default: '.') the string used for the decimal separator + * 'group': (default: ',') the string used for the groups separator + * 'grouping': (default: [3]) a list of integers representing the grouping pattern * 'numerals': a list of ten strings used as replacements for numbers 0-9 - * 'percent': the string used for the percentage symbol + * 'percent': (default: '%') the string used for the percentage symbol + * 'separate_4digits': (default: True) separate integers with 4-digits or less * * 'nully': a value that will be used in place of the nully value during formatting * If the value type matches the column type, it will be formatted normally @@ -187,7 +180,8 @@ export const propTypes = { group: PropTypes.string, grouping: PropTypes.arrayOf(PropTypes.number), numerals: PropTypes.arrayOf(PropTypes.string), - percent: PropTypes.string + percent: PropTypes.string, + separate_4digits: PropTypes.bool }), nully: PropTypes.any, prefix: PropTypes.number, @@ -319,15 +313,16 @@ export const propTypes = { * * This prop is derived from the [d3.formatLocale](https://github.com/d3/d3-format#formatLocale) data structure specification. * - * When left unspecified, will use the default value provided by d3-format. + * When left unspecified, each individual nested prop will default to a pre-determined value. * - * 'currency': a list of two strings representing the currency + * 'currency': (default: '$') a list of two strings representing the currency * prefix and suffix symbols - * 'decimal': the string used for the decimal separator - * 'group': the string used for the groups separator - * 'grouping': a list of integers representing the grouping pattern + * 'decimal': (default: '.') the string used for the decimal separator + * 'group': (default: ',') the string used for the groups separator + * 'grouping': (default: [3]) a list of integers representing the grouping pattern * 'numerals': a list of ten strings used as replacements for numbers 0-9 - * 'percent': the string used for the percentage symbol + * 'percent': (default: '%') the string used for the percentage symbol + * 'separate_4digits': (default: True) separate integers with 4-digits or less */ locale_format: PropTypes.shape({ currency: PropTypes.arrayOf(PropTypes.string), @@ -335,7 +330,8 @@ export const propTypes = { group: PropTypes.string, grouping: PropTypes.arrayOf(PropTypes.number), numerals: PropTypes.arrayOf(PropTypes.string), - percent: PropTypes.string + percent: PropTypes.string, + separate_4digits: PropTypes.bool }), /** diff --git a/src/dash-table/DataTable.sanitize.ts b/src/dash-table/DataTable.sanitize.ts index e3a8deff2..6cb6575a0 100644 --- a/src/dash-table/DataTable.sanitize.ts +++ b/src/dash-table/DataTable.sanitize.ts @@ -9,7 +9,8 @@ const D3_DEFAULT_LOCALE: INumberLocale = { decimal: '.', group: ',', grouping: [3], - percent: '%' + percent: '%', + separate_4digits: true }; const DEFAULT_NULLY = ''; diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 173cd720f..2e565b2c7 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -116,6 +116,7 @@ export interface INumberLocale { grouping: number[]; numerals?: string[]; percent: string; + separate_4digits?: boolean; } export type NumberFormat = ({ diff --git a/src/dash-table/type/number.ts b/src/dash-table/type/number.ts index aabdcb228..187c6c303 100644 --- a/src/dash-table/type/number.ts +++ b/src/dash-table/type/number.ts @@ -1,3 +1,4 @@ +import * as R from 'ramda'; import { formatLocale } from 'd3-format'; import isNumeric from 'fast-isnumeric'; @@ -7,7 +8,7 @@ import { IReconciliation } from './reconcile'; const convertToD3 = ({ group, ...others }: INumberLocale) => ({ thousands: group, - ...others + ...R.omit(['separate_4digits'], others) }); export function coerce(value: any, options: INumberColumn | undefined): IReconciliation { @@ -27,16 +28,22 @@ export function getFormatter(format: NumberFormat) { locale.formatPrefix(format.specifier, format.prefix) : locale.format(format.specifier); + const thousandsSpecifier = format.locale.separate_4digits ? + format.specifier : + format.specifier.replace(/,/, ''); + + const thousandsFormatter = format.prefix ? + locale.formatPrefix(thousandsSpecifier, format.prefix) : + locale.format(thousandsSpecifier); + return (value: any) => { - if (isNully(value)) { - return typeof format.nully === 'number' ? - numberFormatter(format.nully) : - format.nully; - } else if (typeof value === 'number') { - return numberFormatter(value); - } else { - return value; - } + value = isNully(value) ? format.nully : value; + + return typeof value !== 'number' ? + value : + Math.abs(value) < 10000 ? + thousandsFormatter(value) : + numberFormatter(value); }; } diff --git a/tests/cypress/tests/unit/formatting_test.ts b/tests/cypress/tests/unit/formatting_test.ts index 4d55fe2b4..5734fa058 100644 --- a/tests/cypress/tests/unit/formatting_test.ts +++ b/tests/cypress/tests/unit/formatting_test.ts @@ -101,6 +101,26 @@ describe('formatting', () => { }); }); + describe('with nully handling / partial locale override with separate_4digits', () => { + it('formats significant digits and grouping separator', () => { + const formatter = getFormatter({ + locale: getLocale({ + grouping: [2, 1], + separate_4digits: false + }), + nully: getNully('42.4242'), + specifier: ',.2f' + }); + + assert.isOk(formatter); + + expect(formatter(-1299)).to.equal('-1299.00'); + expect(formatter(-1299431)).to.equal('-1,2,99,4,31.00'); + expect(formatter(1299)).to.equal('1299.00'); + expect(formatter(1299431)).to.equal('1,2,99,4,31.00'); + }); + }); + describe('without nully handling / default locale / si prefix', () => { it('formats currency', () => { const formatter = getFormatter({ From 6523f9c9512fd3c69ede63e6903304306985e695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 21 Feb 2019 13:29:00 -0500 Subject: [PATCH 19/55] - update changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 094ac0799..a33c2fd15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added +[#189](https://github.com/plotly/dash-table/issues/189) +- Added `format` nested prop to columns + - Applied to columns with `type=numeric` (more to come) + - Uses [d3-format](https://github.com/d3/d3-format) under the hood + - `format.locale` for localization configuration + - `format.prefix` for SI prefix configuration + - `format.specifier` for formatting configuration + - `format.separate_4digits` to configure grouping behavior for numbers with 4 digits or less + - Python helpers (dash_table.FormatTemplate) +- Added `locale_format` prop to table (default localization configuration, merged with column.format.locale) + [#342](https://github.com/plotly/dash-core/issues/342) - Added `column_type` condition to style `if`; allows applying styles based on the type of the column for props - `style_cell_conditional` From dc13b4ec523a7e413b44cc9fc5a3efe7900e576f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 21 Feb 2019 14:24:06 -0500 Subject: [PATCH 20/55] - formatting app mode with a few variations baked in --- dash_table/Format.py | 4 ++-- demo/AppMode.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/dash_table/Format.py b/dash_table/Format.py index ad5beacbf..8170368d6 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -88,9 +88,9 @@ def __init__(self): } # Specifier - def align(self, align: Align = Align.UNDEFINED): + def align(self, align: Align = Align.UNDEFINED, count: int): align = align if isinstance(align, Align) else Align.UNDEFINED - self.specifier['align'] = align + self.specifier['align'] = ('{0}{1}'.format(align, count) if align is not Align.UNDEFINED else align) return self def fill(self, value: str): diff --git a/demo/AppMode.ts b/demo/AppMode.ts index 60d02ce61..23557ae5e 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -19,6 +19,7 @@ export enum AppMode { Filtering = 'filtering', FixedTooltips = 'fixed,tooltips', FixedVirtualized = 'fixed,virtualized', + Formatting = 'formatting', ReadOnly = 'readonly', ColumnsInSpace = 'columnsInSpace', Tooltips = 'tooltips', @@ -260,6 +261,49 @@ function getFixedVirtualizedState() { }; } +function getFormattingState() { + const state = getDefaultState(); + + R.forEach((datum: any) => { + if (datum.eee % 2 === 0) { + datum.eee = undefined; + } else if (datum.eee % 10 === 5) { + datum.eee = `xx-${datum.eee}-xx`; + } + }, state.tableProps.data as any); + + R.forEach((column: any) => { + if (column.id === 'rows') { + column.format = { + specifier: '.^5' + }; + } else if (column.id === 'ccc') { + column.format = { + locale: { + separate_4digits: false + }, + prefix: 1000, + specifier: '.3f' + }; + } else if (column.id === 'ddd') { + column.format = { + locale: { + currency: ['($ eq.) ', ' £'], + separate_4digits: false + }, + specifier: '$,.2f' + }; + } else if (column.id === 'eee') { + column.format = { + nully: 'N/A', + specifier: '' + }; + } + }, state.tableProps.columns as any); + + return state; +} + function getState() { const mode = Environment.searchParams.get('mode'); @@ -272,6 +316,8 @@ function getState() { return getFixedTooltipsState(); case AppMode.FixedVirtualized: return getFixedVirtualizedState(); + case AppMode.Formatting: + return getFormattingState(); case AppMode.ReadOnly: return getReadonlyState(); case AppMode.ColumnsInSpace: From fc95c5f89fef91095ee3a089c7a6d10ee6e26c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 21 Feb 2019 16:42:17 -0500 Subject: [PATCH 21/55] - fix clipboard behavior when copying nully data - fix reconciliation code for the various cases - add table editing tests involving formatting --- demo/AppMode.ts | 14 +++++- src/dash-table/type/reconcile.ts | 14 +++--- src/dash-table/utils/TableClipboardHelper.ts | 6 ++- .../tests/standalone/formatting_test.ts | 48 +++++++++++++++++++ 4 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 tests/cypress/tests/standalone/formatting_test.ts diff --git a/demo/AppMode.ts b/demo/AppMode.ts index 23557ae5e..4f6e902bf 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -288,16 +288,28 @@ function getFormattingState() { } else if (column.id === 'ddd') { column.format = { locale: { - currency: ['($ eq.) ', ' £'], + currency: ['eq. $ ', ''], separate_4digits: false }, + nully: 0, specifier: '$,.2f' }; + column.on_change = { + action: 'coerce', + failure: 'default' + }; + column.validation = { + allow_nully: true + }; } else if (column.id === 'eee') { column.format = { nully: 'N/A', specifier: '' }; + column.on_change = { + action: 'coerce', + failure: 'default' + }; } }, state.tableProps.columns as any); diff --git a/src/dash-table/type/reconcile.ts b/src/dash-table/type/reconcile.ts index e5339991d..cf8808eed 100644 --- a/src/dash-table/type/reconcile.ts +++ b/src/dash-table/type/reconcile.ts @@ -63,16 +63,14 @@ function doFailureRecovery(result: IReconciliation, c: IColumnType) { const failure = (c && c.on_change && c.on_change.failure) || ChangeFailure.Reject; result.failure = failure; - // If Skip/Prevent - if (failure !== ChangeFailure.Default) { - return result; + if (failure === ChangeFailure.Default) { + const defaultValue = (c && c.validation && c.validation.default) || null; + result.success = true; + result.value = defaultValue; + } else if (failure === ChangeFailure.Accept) { + result.success = true; } - // If Default, apply default - const defaultValue = (c && c.validation && c.validation.default) || null; - result.success = true; - result.value = defaultValue; - return result; } diff --git a/src/dash-table/utils/TableClipboardHelper.ts b/src/dash-table/utils/TableClipboardHelper.ts index e2a5e9d1d..bbdaae0b8 100644 --- a/src/dash-table/utils/TableClipboardHelper.ts +++ b/src/dash-table/utils/TableClipboardHelper.ts @@ -20,7 +20,7 @@ export default class TableClipboardHelper { R.props(selectedCols, R.props(R.pluck('id', columns) as any, row) as any) ); - const value = SheetClip.prototype.stringify(df); + const value = `\`\`\`__json__${JSON.stringify(df)}__nosj__\`\`\``; Logger.trace('TableClipboard -- set clipboard data: ', value); @@ -43,7 +43,9 @@ export default class TableClipboardHelper { return; } - const values = SheetClip.prototype.parse(text); + const values = /^```__json__(.*)__nosj__```$/.test(text) ? + JSON.parse(text.replace(/^```__json__|__nosj__```$/g, '')) : + SheetClip.prototype.parse(text); return applyClipboardToData( values, diff --git a/tests/cypress/tests/standalone/formatting_test.ts b/tests/cypress/tests/standalone/formatting_test.ts new file mode 100644 index 000000000..0ab43d02a --- /dev/null +++ b/tests/cypress/tests/standalone/formatting_test.ts @@ -0,0 +1,48 @@ +import DashTable from 'cypress/DashTable'; +import DOM from 'cypress/DOM'; +import Key from 'cypress/Key'; +import { AppMode } from 'demo/AppMode'; + +describe('formatting', () => { + beforeEach(() => { + cy.visit(`http://localhost:8080?mode=${AppMode.Formatting}`); + DashTable.toggleScroll(false); + }); + + it('can edit formatted cell', () => { + DashTable.getCellById(1, 'eee').within( + () => cy.get('.dash-cell-value').should('have.html', 'N/A') + ); + DashTable.getCellById(1, 'eee').click(); + DOM.focused.type(`1${Key.Enter}`); + DashTable.getCellById(1, 'eee').within( + () => cy.get('.dash-cell-value').should('have.html', '1') + ); + DashTable.getCellById(1, 'eee').click(); + DOM.focused.type(`abc${Key.Enter}`); + DashTable.getCellById(1, 'eee').within( + () => cy.get('.dash-cell-value').should('have.html', 'N/A') + ); + }); + + it('can copy formatted cell and reformat based on destination cell rules', () => { + DashTable.getCellById(2, 'eee').within( + () => cy.get('.dash-cell-value').should('have.html', '3') + ); + DashTable.getCellById(2, 'eee').click(); + DOM.focused.type(`${Key.Shift}${Key.ArrowDown}`); + DOM.focused.type(`${Key.Meta}c`); + + DashTable.getCellById(2, 'ddd').click(); + DOM.focused.type(`${Key.Meta}v`); + + DashTable.getCellById(2, 'eee').click(); + + DashTable.getCellById(2, 'ddd').within( + () => cy.get('.dash-cell-value').should('have.html', 'eq. $ 3.00') + ); + DashTable.getCellById(3, 'ddd').within( + () => cy.get('.dash-cell-value').should('have.html', 'eq. $ 0.00') + ); + }); +}); \ No newline at end of file From f33280c99e33f23494918641fb53d3077f061a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 21 Feb 2019 17:00:37 -0500 Subject: [PATCH 22/55] - fix clipboard behavior on nully cases --- src/dash-table/components/CellInput/index.tsx | 7 ++++++- src/dash-table/utils/TableClipboardHelper.ts | 11 ++++++++--- src/dash-table/utils/applyClipboardToData.ts | 2 +- tests/cypress/tests/standalone/formatting_test.ts | 5 ++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/dash-table/components/CellInput/index.tsx b/src/dash-table/components/CellInput/index.tsx index 6be343ade..4f4437cd9 100644 --- a/src/dash-table/components/CellInput/index.tsx +++ b/src/dash-table/components/CellInput/index.tsx @@ -47,6 +47,11 @@ export default class CellInput extends PureComponent { value } = this.props; + // input does not handle `null` correct (causes console error) + const sanitizedValue = this.state.value === null ? + undefined : + this.state.value; + return (
{value} @@ -62,7 +67,7 @@ export default class CellInput extends PureComponent { onKeyDown={this.handleKeyDown} onMouseUp={onMouseUp} onPaste={onPaste} - value={this.state.value} + value={sanitizedValue} />
); } diff --git a/src/dash-table/utils/TableClipboardHelper.ts b/src/dash-table/utils/TableClipboardHelper.ts index bbdaae0b8..7d7698209 100644 --- a/src/dash-table/utils/TableClipboardHelper.ts +++ b/src/dash-table/utils/TableClipboardHelper.ts @@ -8,6 +8,8 @@ import { ActiveCell, Columns, Data, SelectedCells } from 'dash-table/components/ import applyClipboardToData from './applyClipboardToData'; export default class TableClipboardHelper { + private static lastLocalCopy: any[][] = [[]]; + public static toClipboard(e: any, selectedCells: SelectedCells, columns: Columns, data: Data) { const selectedRows = R.uniq(R.pluck(0, selectedCells).sort((a, b) => a - b)); const selectedCols: any = R.uniq(R.pluck(1, selectedCells).sort((a, b) => a - b)); @@ -20,7 +22,8 @@ export default class TableClipboardHelper { R.props(selectedCols, R.props(R.pluck('id', columns) as any, row) as any) ); - const value = `\`\`\`__json__${JSON.stringify(df)}__nosj__\`\`\``; + const value = SheetClip.prototype.stringify(df); + TableClipboardHelper.lastLocalCopy = df; Logger.trace('TableClipboard -- set clipboard data: ', value); @@ -43,8 +46,10 @@ export default class TableClipboardHelper { return; } - const values = /^```__json__(.*)__nosj__```$/.test(text) ? - JSON.parse(text.replace(/^```__json__|__nosj__```$/g, '')) : + const localDf = SheetClip.prototype.stringify(TableClipboardHelper.lastLocalCopy); + + const values = localDf === text ? + TableClipboardHelper.lastLocalCopy : SheetClip.prototype.parse(text); return applyClipboardToData( diff --git a/src/dash-table/utils/applyClipboardToData.ts b/src/dash-table/utils/applyClipboardToData.ts index 9b197e764..7973a891d 100644 --- a/src/dash-table/utils/applyClipboardToData.ts +++ b/src/dash-table/utils/applyClipboardToData.ts @@ -7,7 +7,7 @@ import reconcile from 'dash-table/type/reconcile'; import isEditable from 'dash-table/derived/cell/isEditable'; export default ( - values: string[][], + values: any[][], activeCell: ActiveCell, derived_viewport_indices: number[], columns: Columns, diff --git a/tests/cypress/tests/standalone/formatting_test.ts b/tests/cypress/tests/standalone/formatting_test.ts index 0ab43d02a..1ec0483b0 100644 --- a/tests/cypress/tests/standalone/formatting_test.ts +++ b/tests/cypress/tests/standalone/formatting_test.ts @@ -30,7 +30,7 @@ describe('formatting', () => { () => cy.get('.dash-cell-value').should('have.html', '3') ); DashTable.getCellById(2, 'eee').click(); - DOM.focused.type(`${Key.Shift}${Key.ArrowDown}`); + DOM.focused.type(`${Key.Shift}${Key.ArrowDown}${Key.ArrowDown}`); DOM.focused.type(`${Key.Meta}c`); DashTable.getCellById(2, 'ddd').click(); @@ -44,5 +44,8 @@ describe('formatting', () => { DashTable.getCellById(3, 'ddd').within( () => cy.get('.dash-cell-value').should('have.html', 'eq. $ 0.00') ); + DashTable.getCellById(4, 'ddd').within( + () => cy.get('.dash-cell-value').should('have.html', 'eq. $ 0.00') + ); }); }); \ No newline at end of file From d4ad09452860718d8c2b9a5af1da788335a6095a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Feb 2019 09:54:19 -0500 Subject: [PATCH 23/55] - update py implementation for helpers - update linting to include non-generated py files --- dash_table/Format.py | 302 ++++++++++++++++++++--------------- dash_table/FormatTemplate.py | 23 +-- package.json | 4 +- 3 files changed, 187 insertions(+), 142 deletions(-) diff --git a/dash_table/Format.py b/dash_table/Format.py index 8170368d6..f0c1d321d 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -1,146 +1,182 @@ -from enum import Enum import json +from typing import Union +import collections + + +def get_named_tuple(name: str, dict: dict): + return collections.namedtuple(name, dict.keys())(*dict.values()) + + +Align = get_named_tuple('align', { + 'default': '', + 'left': '<', + 'right': '>', + 'center': '^', + "right_sign": '=' +}) + +Group = get_named_tuple('group', { + 'no': '', + 'yes': ',' +}) + +Padding = get_named_tuple('padding', { + 'no': '', + 'yes': '0' +}) + +Prefix = get_named_tuple('prefix', { + 'yocto': 10**-24, + 'zepto': 10**-21, + 'atto': 10**-18, + 'femto': 10**-15, + 'pico': 10**-12, + 'nano': 10**-9, + 'micro': 10**-6, + 'milli': 10**-3, + 'none': None, + 'kilo': 10**3, + 'mega': 10**6, + 'giga': 10**9, + 'tera': 10**12, + 'peta': 10**15, + 'exa': 10**18, + 'zetta': 10**21, + 'yotta': 10**24 +}) + +Scheme = get_named_tuple('scheme', { + 'default': '', + 'decimal': 'r', + 'decimal_integer': 'd', + 'decimal_or_exponent': 'g', + 'decimal_si_prefix': 's', + 'exponent': 'e', + 'fixed': 'f', + 'percentage': '%', + 'percentage_rounded': 'p', + 'binary': 'b', + 'octal': 'o', + 'lower_case_hex': 'x', + 'upper_case_hex': 'X', + 'unicode': 'c' +}) + +Sign = get_named_tuple('sign', { + 'default': '', + 'negative': '-', + 'positive': '+', + 'parantheses': '(', + 'space': ' ' +}) + +Symbol = get_named_tuple('symbol', { + 'none': '', + 'currency': '$', + 'binary': '#b', + 'octal': '#o', + 'hex': '#x' +}) + +Trim = get_named_tuple('trim', { + 'no': '', + 'yes': '~' +}) -class Align(Enum): - UNDEFINED = '' - RIGHT = '>' - LEFT = '<' - CENTER = '^' - RIGHT_SIGN = '=' - -class GroupSeparator(Enum): - NO = '' - YES = ',' - -class Padding(Enum): - NO = '' - YES = '0' - -class Prefix(Enum): - YOCTO = 10**-24 - ZEPTO = 10**-21 - ATTO = 10**-18 - FEMTO = 10**-15 - PICO = 10**-12 - NANO = 10**-9 - MICRO = 10**-6 - MILLI = 10**-3 - UNDEFINED = None - KILO = 10**3 - MEGA = 10**6 - GIGA = 10**9 - TERA = 10**12 - PETA = 10**15 - EXA = 10**18 - ZETTA = 10**21 - YOTTA = 10**24 - -class Sign(Enum): - UNDEFINED = '' - NEGATIVE = '-' - POSITIVE = '+' - PARANTHESES = '(' - SPACE = ' ' - -class Symbol(Enum): - UNDEFINED = '' - CURRENCY = '$' - BINARY = '#b' - OCTAL = '#o' - HEX = '#x' - -class Trim(Enum): - NO = '' - YES = '~' - -class Type(Enum): - UNDEFINED = '' - EXPONENT = 'e' - FIXED_POINT = 'f' - DECIMAL_OR_EXPONENT = 'g' - DECIMAL = 'r' - DECIMAL_SI_PREFIX = 's' - PERCENTAGE = '%' - PERCENTAGE_ROUNDED = 'p' - BINARY = 'b' - OCTAL = 'o' - DECIMAL_INTEGER = 'd' - LOWER_CASE_HEX = 'x' - UPPER_CASE_HEX = 'X' - UNICODE = 'c' class Format(): def __init__(self): self.locale = {} self.nully = '' - self.prefix = Prefix.UNDEFINED + self.prefix = Prefix.none self.specifier = { - 'align': Align.UNDEFINED, + 'align': Align.default, 'fill': '', - 'group': GroupSeparator.NO, + 'group': Group.no, 'width': '', - 'padding': Padding.NO, + 'padding': Padding.no, 'precision': '', - 'sign': Sign.UNDEFINED, - 'symbol': Symbol.UNDEFINED, - 'trim': Trim.NO, - 'type': Type.UNDEFINED + 'sign': Sign.default, + 'symbol': Symbol.none, + 'trim': Trim.no, + 'type': Scheme.default } # Specifier - def align(self, align: Align = Align.UNDEFINED, count: int): - align = align if isinstance(align, Align) else Align.UNDEFINED - self.specifier['align'] = ('{0}{1}'.format(align, count) if align is not Align.UNDEFINED else align) + def align(self, value): + if value not in Align: + raise Exception('expected value to be one of', str(list(Align))) + + self.specifier['align'] = value return self - def fill(self, value: str): + def fill(self, value): self.specifier['fill'] = value return self - def group(self, value: bool = False): - value = value if isinstance(value, bool) else False - value = GroupSeparator.YES if value == True else GroupSeparator.NO + def group(self, value): + if isinstance(value, bool): + value = Group.yes if value else Group.no + + if value not in Group: + raise Exception('expected value to be one of', str(list(Group))) + self.specifier['group'] = value return self - def pad(self, value: bool = False): - value = value if isinstance(value, bool) else False - value = value.YES if value == True else Padding.NO + def padding(self, value, count=0): + if isinstance(value, bool): + value = Padding.yes if value else Padding.no + + if value not in Padding: + raise Exception('expected value to be one of', str(list(Padding))) + + if not isinstance(count, int) or count < 0: + raise Exception('expected count to be a non-negative integer', str(count)) + self.specifier['padding'] = value + self.specifier['width'] = count return self - def precision(self, value: int = ''): - value = '.{}'.format(value) if isinstance(value, int) or value != '' else '' - self.specifier['precision'] = value + def scheme(self, value, precision=0): + if value not in Scheme: + raise Exception('expected value to be one of', str(list(Scheme))) + + if precision is not None and (not isinstance(precision, int) or precision < 0): + raise Exception('expected precision to be a non-negative integer or None', str(precision)) + + self.specifier['precision'] = '.{0}'.format(precision) if precision is not None else '' + self.specifier['type'] = value return self - def sign(self, value: Sign = Sign.UNDEFINED): - value = value if isinstance(value, Sign) else Sign.UNDEFINED + def sign(self, value): + if value not in Sign: + raise Exception('expected value to be one of', str(list(Sign))) + self.specifier['sign'] = value return self - def symbol(self, value: Symbol = Symbol.UNDEFINED): - value = value if isinstance(value, Symbol) else Symbol.UNDEFINED + def symbol(self, value): + if value not in Symbol: + raise Exception('expected value to be one of', str(list(Symbol))) + self.specifier['symbol'] = value return self - def trim(self, value: bool = False): - value = value if isinstance(value, bool) else False - value = Trim.YES if value == True else Trim.NO - self.specifier['trim'] = value - return self + def trim(self, value): + if isinstance(value, bool): + value = Trim.yes if value else Trim.no - def type(self, value: Type = Type.UNDEFINED): - value = value if isinstance(value, Type) else Type.UNDEFINED - self.specifier['type'] = value - return self + if value not in Trim: + raise Exception('expected value to be one of', str(list(Trim))) - def width(self, value: int = ''): - value = value if isinstance(value, int) or value == '' else '' - self.specifier['width'] = str(value) + self.specifier['trim'] = value return self - def currency_prefix(self, symbol: str): + def currency_prefix(self, symbol): + if not isinstance(symbol, str): + raise Exception('expected symbol to be a string') + if 'currency' not in self.locale: self.locale['currency'] = [symbol, ''] else: @@ -149,7 +185,10 @@ def currency_prefix(self, symbol: str): return self # Locale - def currency_suffix(self, symbol: str): + def currency_suffix(self, symbol): + if not isinstance(symbol, str): + raise Exception('expected symbol to be a string') + if 'currency' not in self.locale: self.locale['currency'] = ['', symbol] else: @@ -157,21 +196,30 @@ def currency_suffix(self, symbol: str): return self - def decimal_delimitor(self, symbol: str): + def decimal_delimitor(self, symbol): + if not isinstance(symbol, str): + raise Exception('expected symbol to be a string') + self.locale['decimal'] = symbol return self - def group_delimitor(self, symbol: str): - self.locale['thousands'] = symbol + def group_delimitor(self, symbol): + if not isinstance(symbol, str): + raise Exception('expected symbol to be a string') + + self.locale['group'] = symbol return self - def groups(self, groups: list): + def groups(self, groups): groups = groups if isinstance(groups, list) else \ [groups] if isinstance(groups, int) else None + if not isinstance(groups, list): + raise Exception('expected groups to be an integer or a list of integers') + for group in groups: - if not isinstance(group, int): - raise Exception('expected "group" to be an int') + if not isinstance(group, int) or group < 0: + raise Exception('expected entry to be a non-negative integer') self.locale['grouping'] = groups return self @@ -182,26 +230,28 @@ def nully(self, nully): return self # Prefix - def si_prefix(self, prefix: Prefix = Prefix.UNDEFINED): - prefix = prefix if isinstance(prefix, Prefix) else Prefix.UNDEFINED - self.prefix = prefix + def si_prefix(self, value): + if value not in Prefix: + raise Exception('expected value to be one of', str(list(Prefix))) + + self.prefix = value return self def create(self): f = self.locale.copy() f['nully'] = self.nully - f['prefix'] = self.prefix.value + f['prefix'] = self.prefix f['specifier'] = '{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}'.format( - self.specifier['fill'] if self.specifier['align'] != Align.UNDEFINED else '', - self.specifier['align'].value, - self.specifier['sign'].value, - self.specifier['symbol'].value, - self.specifier['padding'].value, + self.specifier['fill'] if self.specifier['align'] != Align.default else '', + self.specifier['align'], + self.specifier['sign'], + self.specifier['symbol'], + self.specifier['padding'], self.specifier['width'], - self.specifier['group'].value, + self.specifier['group'], self.specifier['precision'], - self.specifier['trim'].value, - self.specifier['type'].value + self.specifier['trim'], + self.specifier['type'] ) - return f \ No newline at end of file + return f diff --git a/dash_table/FormatTemplate.py b/dash_table/FormatTemplate.py index 9bd5613a0..a19cb07e1 100644 --- a/dash_table/FormatTemplate.py +++ b/dash_table/FormatTemplate.py @@ -1,21 +1,14 @@ from enum import Enum -from .Format import Format, Sign, Symbol, Type +from .Format import Format, Scheme, Sign, Symbol -def money(decimals: int = 2, sign: Sign = Sign.UNDEFINED): - decimals = decimals if isinstance(decimals, int) else 2 - return Format() \ - .precision(decimals) \ - .sign(sign) \ - .symbol(Symbol.CURRENCY) \ - .type(Type.FIXED_POINT) \ - .create() +def money(decimals, sign=Sign.default): + return Format().sign(sign).scheme(Scheme.fixed, decimals).symbol(Symbol.currency).create() -def percentage(decimals: int = 0): - decimals = decimals if isinstance(decimals, int) else 0 +def percentage(decimals, rounded: bool=True): + if not isinstance(rounded, bool): + raise Exception('expected rounded to be a boolean') - return Format() \ - .precision(decimals) \ - .type(Type.PERCENTAGE) \ - .create() \ No newline at end of file + rounded = Scheme.percentage_rounded if rounded else Scheme.percentage + return Format().scheme(rounded, decimals).create() diff --git a/package.json b/package.json index 56f1cc81c..e4cabc7f8 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "private::host_dash8082": "python tests/cypress/dash/v_copy_paste.py", "private::host_dash8083": "python tests/cypress/dash/v_fe_page.py", "private::host_js": "http-server ./dash_table -c-1 --silent", + "private::lint:ts": "tslint '{src,demo,tests}/**/*.{js,ts,tsx}' --exclude '**/@Types/*.*'", + "private::lint:py": "flake8 --exclude=DataTable.py,__init__.py,_imports_.py --ignore=E501,F401,F841,F811 dash_table", "private::wait_dash8081": "wait-on http://localhost:8081", "private::wait_dash8082": "wait-on http://localhost:8082", "private::wait_dash8083": "wait-on http://localhost:8083", @@ -31,7 +33,7 @@ "private::runtests-v1": "run-s private::runtests:unit private::runtests:standalone private::runtests:server", "build.watch": "webpack-dev-server --content-base dash_table --mode development", "build": "run-s private::build:js private::build:py", - "lint": "tslint '{src,demo,tests}/**/*.{js,ts,tsx}' --exclude '**/@Types/*.*'", + "lint": "run-s private::lint:*", "test-v0": "run-p --race private::host* private::runtests-v0", "test-v1": "run-p --race private::host* private::runtests-v1", "test.visual": "build-storybook && percy-storybook", From b9655e99ddd1bec5141856625ba268dc27e41e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Feb 2019 10:28:51 -0500 Subject: [PATCH 24/55] - kwargs (a first attempt, I'm sure) --- dash_table/Format.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/dash_table/Format.py b/dash_table/Format.py index f0c1d321d..63712844b 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -1,5 +1,6 @@ import json from typing import Union +import inspect import collections @@ -85,7 +86,7 @@ def get_named_tuple(name: str, dict: dict): class Format(): - def __init__(self): + def __init__(self, **kwargs): self.locale = {} self.nully = '' self.prefix = Prefix.none @@ -102,6 +103,14 @@ def __init__(self): 'type': Scheme.default } + valid_methods = [m for m in dir(self) if m != '__init__' and m != 'create' and inspect.ismethod(getattr(self, m))] + + for arg in kwargs: + if arg not in valid_methods: + raise Exception('{0} is not a format method. Expected one of'.format(arg), str(list(valid_methods))) + + getattr(self, arg)(kwargs[arg]) + # Specifier def align(self, value): if value not in Align: @@ -124,28 +133,34 @@ def group(self, value): self.specifier['group'] = value return self - def padding(self, value, count=0): + def padding(self, value): if isinstance(value, bool): value = Padding.yes if value else Padding.no if value not in Padding: raise Exception('expected value to be one of', str(list(Padding))) - if not isinstance(count, int) or count < 0: - raise Exception('expected count to be a non-negative integer', str(count)) - self.specifier['padding'] = value - self.specifier['width'] = count return self - def scheme(self, value, precision=0): + def padding_width(self, value): + if not isinstance(value, int) or value < 0: + raise Exception('expected value to be a non-negative integer or None', str(value)) + + self.specifier['width'] = value + return self + + def precision(self, value): + if not isinstance(value, int) or value < 0: + raise Exception('expected value to be a non-negative integer or None', str(value)) + + self.specifier['precision'] = '.{0}'.format(value) if value is not None else '' + return self + + def scheme(self, value): if value not in Scheme: raise Exception('expected value to be one of', str(list(Scheme))) - if precision is not None and (not isinstance(precision, int) or precision < 0): - raise Exception('expected precision to be a non-negative integer or None', str(precision)) - - self.specifier['precision'] = '.{0}'.format(precision) if precision is not None else '' self.specifier['type'] = value return self From 1b5b47194c6443d73316d6e7a34771a7057e3f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Feb 2019 11:09:25 -0500 Subject: [PATCH 25/55] - remove unused imports --- dash_table/Format.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dash_table/Format.py b/dash_table/Format.py index 63712844b..6a2274a3a 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -1,5 +1,3 @@ -import json -from typing import Union import inspect import collections From 1fcc8514bccd955f9468cd357a6827d17223076f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Feb 2019 11:16:43 -0500 Subject: [PATCH 26/55] - default specifier / sanitation --- src/dash-table/DataTable.js | 4 ++-- src/dash-table/DataTable.sanitize.ts | 6 ++++++ tests/cypress/tests/unit/formatting_test.ts | 12 ++++++------ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index de65b36d2..8616f16af 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -168,7 +168,7 @@ export const propTypes = { * If the value type matches the column type, it will be formatted normally * 'prefix': a number representing the SI unit to use during formatting * See `dash_table.Format.Prefix` enumeration for the list of valid values - * 'specifier': (mandatory) represents the rules to apply when formatting the number + * 'specifier': (default: '') represents the rules to apply when formatting the number * * dash_table.FormatTemplate contains helper functions to rapidly use certain * typical number formats. @@ -185,7 +185,7 @@ export const propTypes = { }), nully: PropTypes.any, prefix: PropTypes.number, - specifier: PropTypes.string.isRequired + specifier: PropTypes.string }), /** diff --git a/src/dash-table/DataTable.sanitize.ts b/src/dash-table/DataTable.sanitize.ts index 6cb6575a0..bed443175 100644 --- a/src/dash-table/DataTable.sanitize.ts +++ b/src/dash-table/DataTable.sanitize.ts @@ -14,6 +14,7 @@ const D3_DEFAULT_LOCALE: INumberLocale = { }; const DEFAULT_NULLY = ''; +const DEFAULT_SPECIFIER = ''; const applyDefaultToLocale = memoizeOne((locale: INumberLocale) => getLocale(locale)); @@ -24,6 +25,7 @@ const applyDefaultsToColumns = memoizeOne( if (c.type === ColumnType.Numeric && c.format) { c.format.locale = getLocale(defaultLocale, c.format.locale); c.format.nully = getNully(c.format.nully); + c.format.specifier = getSpecifier(c.format.specifier); } return c; }, columns) @@ -47,6 +49,10 @@ export const getLocale = (...locales: Partial[]): INumberLocale = ...locales ]); +export const getSpecifier = (specifier?: string) => specifier === undefined ? + DEFAULT_SPECIFIER : + specifier; + export const getNully = (nully?: any) => nully === undefined ? DEFAULT_NULLY : nully; \ No newline at end of file diff --git a/tests/cypress/tests/unit/formatting_test.ts b/tests/cypress/tests/unit/formatting_test.ts index 5734fa058..1614eef98 100644 --- a/tests/cypress/tests/unit/formatting_test.ts +++ b/tests/cypress/tests/unit/formatting_test.ts @@ -1,5 +1,5 @@ import { getFormatter } from 'dash-table/type/number'; -import { getLocale, getNully } from 'dash-table/DataTable.sanitize'; +import { getLocale, getNully, getSpecifier } from 'dash-table/DataTable.sanitize'; describe('formatting', () => { describe('number', () => { @@ -18,7 +18,7 @@ describe('formatting', () => { const formatter = getFormatter({ locale: getLocale(), nully: getNully(), - specifier: '$.2f' + specifier: getSpecifier('$.2f') }); assert.isOk(formatter); @@ -46,7 +46,7 @@ describe('formatting', () => { const formatter = getFormatter({ locale: getLocale(), nully: 42.42, - specifier: ',.2r' + specifier: getSpecifier(',.2r') }); assert.isOk(formatter); @@ -78,7 +78,7 @@ describe('formatting', () => { grouping: [2, 1] }), nully: getNully('42.4242'), - specifier: ',.2f' + specifier: getSpecifier(',.2f') }); assert.isOk(formatter); @@ -109,7 +109,7 @@ describe('formatting', () => { separate_4digits: false }), nully: getNully('42.4242'), - specifier: ',.2f' + specifier: getSpecifier(',.2f') }); assert.isOk(formatter); @@ -127,7 +127,7 @@ describe('formatting', () => { locale: getLocale(), nully: getNully(), prefix: 0.001, - specifier: '.0f' + specifier: getSpecifier('.0f') }); assert.isOk(formatter); From e294ee1926d4e87a847e7dbd5e07420a9ab4f1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Feb 2019 14:21:03 -0500 Subject: [PATCH 27/55] make sure flake8 is present for linting job! --- .circleci/config.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8a0c5bd5f..16f615c78 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -137,11 +137,15 @@ jobs: "node": docker: - - image: circleci/node:8.11.3 + - image: circleci/python:3.6.7-node steps: - checkout + - run: + name: Create virtual env + command: python -m venv || virtualenv venv + - restore_cache: key: deps1-{{ .Branch }}-{{ checksum "package-lock.json" }}-{{ checksum "package.json" }} @@ -154,6 +158,13 @@ jobs: paths: - node_modules + - run: + name: Install requirements + command: | + sudo pip install virtualenv --upgrade + . venv/bin/activate + pip install flake8 --quiet + - run: name: Run eslint command: npm run lint From f9c599c612dc946600e40574acc7c086a0b80800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Feb 2019 14:34:49 -0500 Subject: [PATCH 28/55] exec-sh? --- package.json | 3 ++- scripts/lint.js | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 scripts/lint.js diff --git a/package.json b/package.json index ecf02d0f6..f51a4ae8e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "private::host_dash8083": "python tests/cypress/dash/v_fe_page.py", "private::host_js": "http-server ./dash_table -c-1 --silent", "private::lint:ts": "tslint '{src,demo,tests}/**/*.{js,ts,tsx}' --exclude '**/@Types/*.*'", - "private::lint:py": "flake8 --exclude=DataTable.py,__init__.py,_imports_.py --ignore=E501,F401,F841,F811 dash_table", + "private::lint:py": "node scripts/lint.js", "private::wait_dash8081": "wait-on http://localhost:8081", "private::wait_dash8082": "wait-on http://localhost:8082", "private::wait_dash8083": "wait-on http://localhost:8083", @@ -63,6 +63,7 @@ "css-loader": "^2.1.0", "cypress": "^3.1.5", "d3-format": "^1.3.2", + "exec-sh": "^0.3.2", "fast-isnumeric": "^1.1.2", "file-loader": "^3.0.1", "http-server": "^0.11.1", diff --git a/scripts/lint.js b/scripts/lint.js new file mode 100644 index 000000000..c02fb2fc1 --- /dev/null +++ b/scripts/lint.js @@ -0,0 +1,10 @@ +const execSh = require('exec-sh'); + +execSh( + 'flake8 --exclude=DataTable.py,__init__.py,_imports_.py --ignore=E501,F401,F841,F811 dash_table', + err => { + if (err) { + throw err; + } + } +); \ No newline at end of file From 88ee72171f12a289bedf2e57e5b7123a490c2116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Feb 2019 14:37:15 -0500 Subject: [PATCH 29/55] . --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 16f615c78..e6fdc0549 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -167,7 +167,7 @@ jobs: - run: name: Run eslint - command: npm run lint + command: flake8 --exclude=DataTable.py,__init__.py,_imports_.py --ignore=E501,F401,F841,F811 dash_table when: always From 5c13118c4cc3e924ee5d3362663d3fc4e379dfa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Feb 2019 14:41:04 -0500 Subject: [PATCH 30/55] . --- .circleci/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e6fdc0549..66b21a5c0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -161,9 +161,8 @@ jobs: - run: name: Install requirements command: | - sudo pip install virtualenv --upgrade . venv/bin/activate - pip install flake8 --quiet + pip install -r requirements-base.txt --quiet - run: name: Run eslint From c3c338fe6bf01ddfda29384ceed4f7812ff240b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Feb 2019 14:51:25 -0500 Subject: [PATCH 31/55] . --- .circleci/config.yml | 4 +++- package.json | 3 +-- scripts/lint.js | 10 ---------- 3 files changed, 4 insertions(+), 13 deletions(-) delete mode 100644 scripts/lint.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 66b21a5c0..2e5028bd0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -166,7 +166,9 @@ jobs: - run: name: Run eslint - command: flake8 --exclude=DataTable.py,__init__.py,_imports_.py --ignore=E501,F401,F841,F811 dash_table + command: | + . venv/bin/activate + npm run lint when: always diff --git a/package.json b/package.json index f51a4ae8e..fa5f89c8f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "private::host_dash8083": "python tests/cypress/dash/v_fe_page.py", "private::host_js": "http-server ./dash_table -c-1 --silent", "private::lint:ts": "tslint '{src,demo,tests}/**/*.{js,ts,tsx}' --exclude '**/@Types/*.*'", - "private::lint:py": "node scripts/lint.js", + "private::lint:py": "node scripts/publish.jsflake8 --exclude=DataTable.py,__init__.py,_imports_.py --ignore=E501,F401,F841,F811 dash_table", "private::wait_dash8081": "wait-on http://localhost:8081", "private::wait_dash8082": "wait-on http://localhost:8082", "private::wait_dash8083": "wait-on http://localhost:8083", @@ -63,7 +63,6 @@ "css-loader": "^2.1.0", "cypress": "^3.1.5", "d3-format": "^1.3.2", - "exec-sh": "^0.3.2", "fast-isnumeric": "^1.1.2", "file-loader": "^3.0.1", "http-server": "^0.11.1", diff --git a/scripts/lint.js b/scripts/lint.js deleted file mode 100644 index c02fb2fc1..000000000 --- a/scripts/lint.js +++ /dev/null @@ -1,10 +0,0 @@ -const execSh = require('exec-sh'); - -execSh( - 'flake8 --exclude=DataTable.py,__init__.py,_imports_.py --ignore=E501,F401,F841,F811 dash_table', - err => { - if (err) { - throw err; - } - } -); \ No newline at end of file From 164bde9ecbc3b299e01c2299a47d88a1217cd2dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Feb 2019 14:54:29 -0500 Subject: [PATCH 32/55] . --- .circleci/config.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2e5028bd0..6aac24b9c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -162,7 +162,7 @@ jobs: name: Install requirements command: | . venv/bin/activate - pip install -r requirements-base.txt --quiet + pip install flake8 --quiet - run: name: Run eslint diff --git a/package.json b/package.json index fa5f89c8f..ecf02d0f6 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "private::host_dash8083": "python tests/cypress/dash/v_fe_page.py", "private::host_js": "http-server ./dash_table -c-1 --silent", "private::lint:ts": "tslint '{src,demo,tests}/**/*.{js,ts,tsx}' --exclude '**/@Types/*.*'", - "private::lint:py": "node scripts/publish.jsflake8 --exclude=DataTable.py,__init__.py,_imports_.py --ignore=E501,F401,F841,F811 dash_table", + "private::lint:py": "flake8 --exclude=DataTable.py,__init__.py,_imports_.py --ignore=E501,F401,F841,F811 dash_table", "private::wait_dash8081": "wait-on http://localhost:8081", "private::wait_dash8082": "wait-on http://localhost:8082", "private::wait_dash8083": "wait-on http://localhost:8083", From 351395a91e935c81c146c97d717b226e8dfa2387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Fri, 22 Feb 2019 14:58:46 -0500 Subject: [PATCH 33/55] . --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6aac24b9c..2e5028bd0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -162,7 +162,7 @@ jobs: name: Install requirements command: | . venv/bin/activate - pip install flake8 --quiet + pip install -r requirements-base.txt --quiet - run: name: Run eslint From 07bad5667823003e5b66a436d1d1eaae2ee8f5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 25 Feb 2019 11:26:32 -0500 Subject: [PATCH 34/55] - update Format class --- dash_table/Format.py | 92 +++++++++++++++++++++------------------- dash_table/metadata.json | 4 +- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/dash_table/Format.py b/dash_table/Format.py index 6a2274a3a..a4b828be5 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -85,10 +85,10 @@ def get_named_tuple(name: str, dict: dict): class Format(): def __init__(self, **kwargs): - self.locale = {} - self.nully = '' - self.prefix = Prefix.none - self.specifier = { + self._locale = {} + self._nully = '' + self._prefix = Prefix.none + self._specifier = { 'align': Align.default, 'fill': '', 'group': Group.no, @@ -101,24 +101,27 @@ def __init__(self, **kwargs): 'type': Scheme.default } - valid_methods = [m for m in dir(self) if m != '__init__' and m != 'create' and inspect.ismethod(getattr(self, m))] + valid_methods = [m for m in dir(self.__class__) if m[0] != '_' and m != 'create'] - for arg in kwargs: - if arg not in valid_methods: - raise Exception('{0} is not a format method. Expected one of'.format(arg), str(list(valid_methods))) + for kw, val in kwargs.items(): + if kw not in valid_methods: + raise Exception('{0} is not a format method. Expected one of'.format(kw), str(list(valid_methods))) - getattr(self, arg)(kwargs[arg]) + getattr(self, kw)(val) # Specifier def align(self, value): if value not in Align: raise Exception('expected value to be one of', str(list(Align))) - self.specifier['align'] = value + self._specifier['align'] = value return self def fill(self, value): - self.specifier['fill'] = value + if not isinstance(value, str) or len(value) != 1: + raise Exception('expected value to be a string of length one') + + self._specifier['fill'] = value return self def group(self, value): @@ -128,7 +131,7 @@ def group(self, value): if value not in Group: raise Exception('expected value to be one of', str(list(Group))) - self.specifier['group'] = value + self._specifier['group'] = value return self def padding(self, value): @@ -138,42 +141,42 @@ def padding(self, value): if value not in Padding: raise Exception('expected value to be one of', str(list(Padding))) - self.specifier['padding'] = value + self._specifier['padding'] = value return self def padding_width(self, value): if not isinstance(value, int) or value < 0: raise Exception('expected value to be a non-negative integer or None', str(value)) - self.specifier['width'] = value + self._specifier['width'] = value return self def precision(self, value): if not isinstance(value, int) or value < 0: raise Exception('expected value to be a non-negative integer or None', str(value)) - self.specifier['precision'] = '.{0}'.format(value) if value is not None else '' + self._specifier['precision'] = '.{0}'.format(value) if value is not None else '' return self def scheme(self, value): if value not in Scheme: raise Exception('expected value to be one of', str(list(Scheme))) - self.specifier['type'] = value + self._specifier['type'] = value return self def sign(self, value): if value not in Sign: raise Exception('expected value to be one of', str(list(Sign))) - self.specifier['sign'] = value + self._specifier['sign'] = value return self def symbol(self, value): if value not in Symbol: raise Exception('expected value to be one of', str(list(Symbol))) - self.specifier['symbol'] = value + self._specifier['symbol'] = value return self def trim(self, value): @@ -183,17 +186,17 @@ def trim(self, value): if value not in Trim: raise Exception('expected value to be one of', str(list(Trim))) - self.specifier['trim'] = value + self._specifier['trim'] = value return self def currency_prefix(self, symbol): if not isinstance(symbol, str): raise Exception('expected symbol to be a string') - if 'currency' not in self.locale: - self.locale['currency'] = [symbol, ''] + if 'currency' not in self._locale: + self._locale['currency'] = [symbol, ''] else: - self.locale['currency'][0] = symbol + self._locale['currency'][0] = symbol return self @@ -202,10 +205,10 @@ def currency_suffix(self, symbol): if not isinstance(symbol, str): raise Exception('expected symbol to be a string') - if 'currency' not in self.locale: - self.locale['currency'] = ['', symbol] + if 'currency' not in self._locale: + self._locale['currency'] = ['', symbol] else: - self.locale['currency'][1] = symbol + self._locale['currency'][1] = symbol return self @@ -213,14 +216,14 @@ def decimal_delimitor(self, symbol): if not isinstance(symbol, str): raise Exception('expected symbol to be a string') - self.locale['decimal'] = symbol + self._locale['decimal'] = symbol return self def group_delimitor(self, symbol): if not isinstance(symbol, str): raise Exception('expected symbol to be a string') - self.locale['group'] = symbol + self._locale['group'] = symbol return self def groups(self, groups): @@ -234,12 +237,12 @@ def groups(self, groups): if not isinstance(group, int) or group < 0: raise Exception('expected entry to be a non-negative integer') - self.locale['grouping'] = groups + self._locale['grouping'] = groups return self # Nully - def nully(self, nully): - self.nully = nully + def nully(self, value): + self._nully = value return self # Prefix @@ -247,24 +250,25 @@ def si_prefix(self, value): if value not in Prefix: raise Exception('expected value to be one of', str(list(Prefix))) - self.prefix = value + self._prefix = value return self def create(self): - f = self.locale.copy() - f['nully'] = self.nully - f['prefix'] = self.prefix + f = {} + f['locale'] = self._locale.copy() + f['nully'] = self._nully + f['prefix'] = self._prefix f['specifier'] = '{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}'.format( - self.specifier['fill'] if self.specifier['align'] != Align.default else '', - self.specifier['align'], - self.specifier['sign'], - self.specifier['symbol'], - self.specifier['padding'], - self.specifier['width'], - self.specifier['group'], - self.specifier['precision'], - self.specifier['trim'], - self.specifier['type'] + self._specifier['fill'] if self._specifier['align'] != Align.default else '', + self._specifier['align'], + self._specifier['sign'], + self._specifier['symbol'], + self._specifier['padding'], + self._specifier['width'], + self._specifier['group'], + self._specifier['precision'], + self._specifier['trim'], + self._specifier['type'] ) return f diff --git a/dash_table/metadata.json b/dash_table/metadata.json index 9886a2fdc..9b011efaf 100644 --- a/dash_table/metadata.json +++ b/dash_table/metadata.json @@ -113,10 +113,10 @@ }, "specifier": { "name": "string", - "required": true + "required": false } }, - "description": "The formatting applied to the column's data.\n\nThis prop is derived from the [d3-format](https://github.com/d3/d3-format) library specification. Apart from\nbeing structured slightly differently (under a single prop), the usage\nis the same.\n\n'locale': represents localization specific formatting information\n When left unspecified, will use the default value provided by d3-format.\n\n 'currency': (default: '$') a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': (default: '.') the string used for the decimal separator\n 'group': (default: ',') the string used for the groups separator\n 'grouping': (default: [3]) a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': (default: '%') the string used for the percentage symbol\n 'separate_4digits': (default: True) separate integers with 4-digits or less\n\n'nully': a value that will be used in place of the nully value during formatting\n If the value type matches the column type, it will be formatted normally\n'prefix': a number representing the SI unit to use during formatting\n See `dash_table.Format.Prefix` enumeration for the list of valid values\n'specifier': (mandatory) represents the rules to apply when formatting the number\n\ndash_table.FormatTemplate contains helper functions to rapidly use certain\ntypical number formats.", + "description": "The formatting applied to the column's data.\n\nThis prop is derived from the [d3-format](https://github.com/d3/d3-format) library specification. Apart from\nbeing structured slightly differently (under a single prop), the usage\nis the same.\n\n'locale': represents localization specific formatting information\n When left unspecified, will use the default value provided by d3-format.\n\n 'currency': (default: '$') a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': (default: '.') the string used for the decimal separator\n 'group': (default: ',') the string used for the groups separator\n 'grouping': (default: [3]) a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': (default: '%') the string used for the percentage symbol\n 'separate_4digits': (default: True) separate integers with 4-digits or less\n\n'nully': a value that will be used in place of the nully value during formatting\n If the value type matches the column type, it will be formatted normally\n'prefix': a number representing the SI unit to use during formatting\n See `dash_table.Format.Prefix` enumeration for the list of valid values\n'specifier': (default: '') represents the rules to apply when formatting the number\n\ndash_table.FormatTemplate contains helper functions to rapidly use certain\ntypical number formats.", "required": false }, "hidden": { From 7f3c222da126081768fc42447eaf9cc1985ef425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 25 Feb 2019 12:53:56 -0500 Subject: [PATCH 35/55] - to_plotly_json --- dash_table/Format.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dash_table/Format.py b/dash_table/Format.py index a4b828be5..ad318eb9e 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -1,6 +1,5 @@ -import inspect import collections - +import inspect def get_named_tuple(name: str, dict: dict): return collections.namedtuple(name, dict.keys())(*dict.values()) @@ -101,7 +100,7 @@ def __init__(self, **kwargs): 'type': Scheme.default } - valid_methods = [m for m in dir(self.__class__) if m[0] != '_' and m != 'create'] + valid_methods = [m for m in dir(self.__class__) if m[0] != '_' and m != 'to_plotly_json'] for kw, val in kwargs.items(): if kw not in valid_methods: @@ -253,7 +252,7 @@ def si_prefix(self, value): self._prefix = value return self - def create(self): + def to_plotly_json(self): f = {} f['locale'] = self._locale.copy() f['nully'] = self._nully From 121dc862d81d8e93f61e760ae959bbc27049d3ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 25 Feb 2019 13:06:49 -0500 Subject: [PATCH 36/55] - rename currency to symbol --- dash_table/DataTable.py | 6 +-- dash_table/Format.py | 46 ++++++++++----------- dash_table/metadata.json | 8 ++-- demo/AppMode.ts | 2 +- src/dash-table/DataTable.js | 8 ++-- src/dash-table/DataTable.sanitize.ts | 2 +- src/dash-table/DataTable.validate.ts | 4 +- src/dash-table/components/Table/props.ts | 2 +- src/dash-table/type/number.ts | 5 ++- tests/cypress/tests/unit/formatting_test.ts | 4 +- 10 files changed, 44 insertions(+), 43 deletions(-) diff --git a/dash_table/DataTable.py b/dash_table/DataTable.py index c4c2456f2..0a97c692d 100644 --- a/dash_table/DataTable.py +++ b/dash_table/DataTable.py @@ -18,16 +18,16 @@ class DataTable(Component): When left unspecified, each individual nested prop will default to a pre-determined value. - 'currency': (default: '$') a list of two strings representing the currency + 'symbol': (default: ['', $']) a list of two strings representing the prefix and suffix symbols 'decimal': (default: '.') the string used for the decimal separator 'group': (default: ',') the string used for the groups separator 'grouping': (default: [3]) a list of integers representing the grouping pattern 'numerals': a list of ten strings used as replacements for numbers 0-9 'percent': (default: '%') the string used for the percentage symbol - 'separate_4digits': (default: True) separate integers with 4-digits or less. locale_format has the following type: dict containing keys 'currency', 'decimal', 'group', 'grouping', 'numerals', 'percent', 'separate_4digits'. + 'separate_4digits': (default: True) separate integers with 4-digits or less. locale_format has the following type: dict containing keys 'symbol', 'decimal', 'group', 'grouping', 'numerals', 'percent', 'separate_4digits'. Those keys have the following types: - - currency (list; optional) + - symbol (list; optional) - decimal (string; optional) - group (string; optional) - grouping (list; optional) diff --git a/dash_table/Format.py b/dash_table/Format.py index ad318eb9e..9319e6b6a 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -69,8 +69,8 @@ def get_named_tuple(name: str, dict: dict): }) Symbol = get_named_tuple('symbol', { - 'none': '', - 'currency': '$', + 'no': '', + 'yes': '$', 'binary': '#b', 'octal': '#o', 'hex': '#x' @@ -95,7 +95,7 @@ def __init__(self, **kwargs): 'padding': Padding.no, 'precision': '', 'sign': Sign.default, - 'symbol': Symbol.none, + 'symbol': Symbol.no, 'trim': Trim.no, 'type': Scheme.default } @@ -188,41 +188,41 @@ def trim(self, value): self._specifier['trim'] = value return self - def currency_prefix(self, symbol): - if not isinstance(symbol, str): - raise Exception('expected symbol to be a string') + def symbol_prefix(self, value): + if not isinstance(value, str): + raise Exception('expected value to be a string') - if 'currency' not in self._locale: - self._locale['currency'] = [symbol, ''] + if 'symbol' not in self._locale: + self._locale['symbol'] = [value, ''] else: - self._locale['currency'][0] = symbol + self._locale['symbol'][0] = value return self # Locale - def currency_suffix(self, symbol): - if not isinstance(symbol, str): - raise Exception('expected symbol to be a string') + def symbol_suffix(self, value): + if not isinstance(value, str): + raise Exception('expected value to be a string') - if 'currency' not in self._locale: - self._locale['currency'] = ['', symbol] + if 'symbol' not in self._locale: + self._locale['symbol'] = ['', value] else: - self._locale['currency'][1] = symbol + self._locale['symbol'][1] = value return self - def decimal_delimitor(self, symbol): - if not isinstance(symbol, str): - raise Exception('expected symbol to be a string') + def decimal_delimitor(self, value): + if not isinstance(value, str): + raise Exception('expected value to be a string') - self._locale['decimal'] = symbol + self._locale['decimal'] = value return self - def group_delimitor(self, symbol): - if not isinstance(symbol, str): - raise Exception('expected symbol to be a string') + def group_delimitor(self, value): + if not isinstance(value, str): + raise Exception('expected value to be a string') - self._locale['group'] = symbol + self._locale['group'] = value return self def groups(self, groups): diff --git a/dash_table/metadata.json b/dash_table/metadata.json index 9b011efaf..ef69747c2 100644 --- a/dash_table/metadata.json +++ b/dash_table/metadata.json @@ -63,7 +63,7 @@ "locale": { "name": "shape", "value": { - "currency": { + "symbol": { "name": "arrayOf", "value": { "name": "string" @@ -116,7 +116,7 @@ "required": false } }, - "description": "The formatting applied to the column's data.\n\nThis prop is derived from the [d3-format](https://github.com/d3/d3-format) library specification. Apart from\nbeing structured slightly differently (under a single prop), the usage\nis the same.\n\n'locale': represents localization specific formatting information\n When left unspecified, will use the default value provided by d3-format.\n\n 'currency': (default: '$') a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': (default: '.') the string used for the decimal separator\n 'group': (default: ',') the string used for the groups separator\n 'grouping': (default: [3]) a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': (default: '%') the string used for the percentage symbol\n 'separate_4digits': (default: True) separate integers with 4-digits or less\n\n'nully': a value that will be used in place of the nully value during formatting\n If the value type matches the column type, it will be formatted normally\n'prefix': a number representing the SI unit to use during formatting\n See `dash_table.Format.Prefix` enumeration for the list of valid values\n'specifier': (default: '') represents the rules to apply when formatting the number\n\ndash_table.FormatTemplate contains helper functions to rapidly use certain\ntypical number formats.", + "description": "The formatting applied to the column's data.\n\nThis prop is derived from the [d3-format](https://github.com/d3/d3-format) library specification. Apart from\nbeing structured slightly differently (under a single prop), the usage\nis the same.\n\n'locale': represents localization specific formatting information\n When left unspecified, will use the default value provided by d3-format.\n\n 'symbol': (default: ['', $']) a list of two strings representing the\n prefix and suffix symbols\n 'decimal': (default: '.') the string used for the decimal separator\n 'group': (default: ',') the string used for the groups separator\n 'grouping': (default: [3]) a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': (default: '%') the string used for the percentage symbol\n 'separate_4digits': (default: True) separate integers with 4-digits or less\n\n'nully': a value that will be used in place of the nully value during formatting\n If the value type matches the column type, it will be formatted normally\n'prefix': a number representing the SI unit to use during formatting\n See `dash_table.Format.Prefix` enumeration for the list of valid values\n'specifier': (default: '') represents the rules to apply when formatting the number\n\ndash_table.FormatTemplate contains helper functions to rapidly use certain\ntypical number formats.", "required": false }, "hidden": { @@ -293,7 +293,7 @@ "type": { "name": "shape", "value": { - "currency": { + "symbol": { "name": "arrayOf", "value": { "name": "string" @@ -333,7 +333,7 @@ } }, "required": false, - "description": "The localization specific formatting information applied to all columns in the table.\n\nThis prop is derived from the [d3.formatLocale](https://github.com/d3/d3-format#formatLocale) data structure specification.\n\nWhen left unspecified, each individual nested prop will default to a pre-determined value.\n\n 'currency': (default: '$') a list of two strings representing the currency\n prefix and suffix symbols\n 'decimal': (default: '.') the string used for the decimal separator\n 'group': (default: ',') the string used for the groups separator\n 'grouping': (default: [3]) a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': (default: '%') the string used for the percentage symbol\n 'separate_4digits': (default: True) separate integers with 4-digits or less" + "description": "The localization specific formatting information applied to all columns in the table.\n\nThis prop is derived from the [d3.formatLocale](https://github.com/d3/d3-format#formatLocale) data structure specification.\n\nWhen left unspecified, each individual nested prop will default to a pre-determined value.\n\n 'symbol': (default: ['', $']) a list of two strings representing the\n prefix and suffix symbols\n 'decimal': (default: '.') the string used for the decimal separator\n 'group': (default: ',') the string used for the groups separator\n 'grouping': (default: [3]) a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': (default: '%') the string used for the percentage symbol\n 'separate_4digits': (default: True) separate integers with 4-digits or less" }, "content_style": { "type": { diff --git a/demo/AppMode.ts b/demo/AppMode.ts index 4f6e902bf..631c922a4 100644 --- a/demo/AppMode.ts +++ b/demo/AppMode.ts @@ -288,7 +288,7 @@ function getFormattingState() { } else if (column.id === 'ddd') { column.format = { locale: { - currency: ['eq. $ ', ''], + symbol: ['eq. $ ', ''], separate_4digits: false }, nully: 0, diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index 8616f16af..c96f40a11 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -155,7 +155,7 @@ export const propTypes = { * 'locale': represents localization specific formatting information * When left unspecified, will use the default value provided by d3-format. * - * 'currency': (default: '$') a list of two strings representing the currency + * 'symbol': (default: ['', $']) a list of two strings representing the * prefix and suffix symbols * 'decimal': (default: '.') the string used for the decimal separator * 'group': (default: ',') the string used for the groups separator @@ -175,7 +175,7 @@ export const propTypes = { */ format: PropTypes.shape({ locale: PropTypes.shape({ - currency: PropTypes.arrayOf(PropTypes.string), + symbol: PropTypes.arrayOf(PropTypes.string), decimal: PropTypes.string, group: PropTypes.string, grouping: PropTypes.arrayOf(PropTypes.number), @@ -315,7 +315,7 @@ export const propTypes = { * * When left unspecified, each individual nested prop will default to a pre-determined value. * - * 'currency': (default: '$') a list of two strings representing the currency + * 'symbol': (default: ['', $']) a list of two strings representing the * prefix and suffix symbols * 'decimal': (default: '.') the string used for the decimal separator * 'group': (default: ',') the string used for the groups separator @@ -325,7 +325,7 @@ export const propTypes = { * 'separate_4digits': (default: True) separate integers with 4-digits or less */ locale_format: PropTypes.shape({ - currency: PropTypes.arrayOf(PropTypes.string), + symbol: PropTypes.arrayOf(PropTypes.string), decimal: PropTypes.string, group: PropTypes.string, grouping: PropTypes.arrayOf(PropTypes.number), diff --git a/src/dash-table/DataTable.sanitize.ts b/src/dash-table/DataTable.sanitize.ts index bed443175..df34e6219 100644 --- a/src/dash-table/DataTable.sanitize.ts +++ b/src/dash-table/DataTable.sanitize.ts @@ -5,7 +5,7 @@ import { memoizeOne } from 'core/memoizer'; import { Columns, ColumnType, INumberLocale } from './components/Table/props'; const D3_DEFAULT_LOCALE: INumberLocale = { - currency: ['$', ''], + symbol: ['$', ''], decimal: '.', group: ',', grouping: [3], diff --git a/src/dash-table/DataTable.validate.ts b/src/dash-table/DataTable.validate.ts index 81cd5df17..6fc29134e 100644 --- a/src/dash-table/DataTable.validate.ts +++ b/src/dash-table/DataTable.validate.ts @@ -18,8 +18,8 @@ function validColumns(props: any) { return !R.any((column: any) => column.format && ( ( - column.format.currency && - column.format.currency.length !== 2 + column.format.symbol && + column.format.symbol.length !== 2 ) || ( column.format.grouping && column.format.grouping.length === 0 diff --git a/src/dash-table/components/Table/props.ts b/src/dash-table/components/Table/props.ts index 2e565b2c7..d8b42501a 100644 --- a/src/dash-table/components/Table/props.ts +++ b/src/dash-table/components/Table/props.ts @@ -110,7 +110,7 @@ export interface ITypeColumn { } export interface INumberLocale { - currency: [string, string]; + symbol: [string, string]; decimal: string; group: string; grouping: number[]; diff --git a/src/dash-table/type/number.ts b/src/dash-table/type/number.ts index 187c6c303..84b318eed 100644 --- a/src/dash-table/type/number.ts +++ b/src/dash-table/type/number.ts @@ -6,9 +6,10 @@ import { INumberColumn, INumberLocale, NumberFormat } from 'dash-table/component import { reconcileNull, isNully } from './null'; import { IReconciliation } from './reconcile'; -const convertToD3 = ({ group, ...others }: INumberLocale) => ({ +const convertToD3 = ({ group, symbol, ...others }: INumberLocale) => ({ + currency: symbol, thousands: group, - ...R.omit(['separate_4digits'], others) + ...R.omit(['separate_4digits', 'symbol'], others) }); export function coerce(value: any, options: INumberColumn | undefined): IReconciliation { diff --git a/tests/cypress/tests/unit/formatting_test.ts b/tests/cypress/tests/unit/formatting_test.ts index 1614eef98..5ac54544d 100644 --- a/tests/cypress/tests/unit/formatting_test.ts +++ b/tests/cypress/tests/unit/formatting_test.ts @@ -14,7 +14,7 @@ describe('formatting', () => { }); describe('without nully handling / default locale', () => { - it('formats currency', () => { + it('formats symbol', () => { const formatter = getFormatter({ locale: getLocale(), nully: getNully(), @@ -122,7 +122,7 @@ describe('formatting', () => { }); describe('without nully handling / default locale / si prefix', () => { - it('formats currency', () => { + it('formats symbol', () => { const formatter = getFormatter({ locale: getLocale(), nully: getNully(), From b6181839c6c742533eae050eca5641f1f4fa10d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 25 Feb 2019 13:09:39 -0500 Subject: [PATCH 37/55] fix lint --- dash_table/Format.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dash_table/Format.py b/dash_table/Format.py index 9319e6b6a..b14dc44f5 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -1,6 +1,7 @@ import collections import inspect + def get_named_tuple(name: str, dict: dict): return collections.namedtuple(name, dict.keys())(*dict.values()) From 1938bee94e854a8c60a48b1ed77f78c0cbc8e05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 25 Feb 2019 14:14:24 -0500 Subject: [PATCH 38/55] - format unit tests --- dash_table/Format.py | 7 +- package.json | 3 +- tests/unit/format_test.py | 233 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 tests/unit/format_test.py diff --git a/dash_table/Format.py b/dash_table/Format.py index b14dc44f5..448456014 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -227,14 +227,13 @@ def group_delimitor(self, value): return self def groups(self, groups): - groups = groups if isinstance(groups, list) else \ - [groups] if isinstance(groups, int) else None + groups = groups if isinstance(groups, list) else [groups] if isinstance(groups, int) else None - if not isinstance(groups, list): + if not isinstance(groups, list) or len(groups) == 0: raise Exception('expected groups to be an integer or a list of integers') for group in groups: - if not isinstance(group, int) or group < 0: + if not isinstance(group, int) or group <= 0: raise Exception('expected entry to be a non-negative integer') self._locale['grouping'] = groups diff --git a/package.json b/package.json index b1666fc3c..73084a346 100644 --- a/package.json +++ b/package.json @@ -26,11 +26,12 @@ "private::wait_dash8083": "wait-on http://localhost:8083", "private::wait_js": "wait-on http://localhost:8080", "private::opentests": "cypress open", + "private::runtests:python": "python -m unittest tests/unit/format_test.py", "private::runtests:unit": "cypress run --browser chrome --spec 'tests/cypress/tests/unit/**/*'", "private::runtests:standalone": "cypress run --browser chrome --spec 'tests/cypress/tests/standalone/**/*'", "private::runtests:server": "cypress run --browser chrome --spec 'tests/cypress/tests/server/**/*'", "private::runtests-v0": "run-s private::runtests:server", - "private::runtests-v1": "run-s private::runtests:unit private::runtests:standalone private::runtests:server", + "private::runtests-v1": "run-s private::runtests:python private::runtests:unit private::runtests:standalone private::runtests:server", "build.watch": "webpack-dev-server --content-base dash_table --mode development", "build": "run-s private::build:js private::build:py", "lint": "run-s private::lint:*", diff --git a/tests/unit/format_test.py b/tests/unit/format_test.py new file mode 100644 index 000000000..e57ba4fff --- /dev/null +++ b/tests/unit/format_test.py @@ -0,0 +1,233 @@ +import unittest + +import dash_table.Format as f +from dash_table.Format import Format + +class FormatTest(unittest.TestCase): + def validate_complex(self, res): + self.assertEqual(res['locale']['symbol'][0], 'a') + self.assertEqual(res['locale']['symbol'][1], 'bc') + self.assertEqual(res['locale']['decimal'], 'x') + self.assertEqual(res['locale']['group'], 'y') + self.assertEqual(res['nully'], 'N/A') + self.assertEqual(res['prefix'], None) + self.assertEqual(res['specifier'], '.^($010,.6s') + + def test_complex_and_valid_in_ctor(self): + res = Format( + align=f.Align.center, + fill='.', + group=f.Group.yes, + padding=True, + padding_width=10, + precision=6, + scheme='s', + sign=f.Sign.parantheses, + symbol=f.Symbol.yes, + symbol_prefix='a', + symbol_suffix='bc', + decimal_delimitor='x', + group_delimitor='y', + groups=[2, 2, 2, 3], + nully='N/A', + si_prefix=f.Prefix.none + ) + + self.validate_complex(res.to_plotly_json()) + + def test_complex_and_valid_in_fluent(self): + res = Format().align(f.Align.center).fill('.').group(f.Group.yes).padding(True).padding_width(10).precision(6).scheme('s').sign(f.Sign.parantheses).symbol(f.Symbol.yes).symbol_prefix('a').symbol_suffix('bc').decimal_delimitor('x').group_delimitor('y').groups([2, 2, 2, 3]).nully('N/A').si_prefix(f.Prefix.none) + + self.validate_complex(res.to_plotly_json()) + + def test_valid_align_named(self): + Format().align(f.Align.center) + + def test_valid_align_string(self): + Format().align('=') + + def test_invalid_align_string(self): + self.assertRaises(Exception, Format().align, 'i') + + def test_invalid_align_type(self): + self.assertRaises(Exception, Format().align, 7) + + def test_valid_fill(self): + Format().fill('.') + + def test_invalid_fill_length(self): + self.assertRaises(Exception, Format().fill, 'invalid') + + def test_invalid_fill_type(self): + self.assertRaises(Exception, Format().fill, 7) + + def test_valid_group_bool(self): + Format().group(True) + + def test_valid_group_string(self): + Format().group(',') + + def test_valid_group_named(self): + Format().group(f.Group.no) + + def test_invalid_group_type(self): + self.assertRaises(Exception, Format().group, 7) + + def test_invalid_group_string(self): + self.assertRaises(Exception, Format().group, 'invalid') + + def test_valid_padding_bool(self): + Format().padding(False) + + def test_valid_padding_string(self): + Format().padding('0') + + def test_valid_padding_named(self): + Format().padding(f.Padding.no) + + def test_invalid_padding_type(self): + self.assertRaises(Exception, Format().padding, 7) + + def test_invalid_padding_string(self): + self.assertRaises(Exception, Format().padding, 'invalid') + + def test_valid_padding_width(self): + Format().padding_width(10) + + def test_valid_padding_width_0(self): + Format().padding_width(0) + + def test_invalid_padding_width_negative(self): + self.assertRaises(Exception, Format().padding_width, -10) + + def test_invalid_padding_width_type(self): + self.assertRaises(Exception, Format().padding_width, 7.7) + + def test_valid_precision(self): + Format().precision(10) + + def test_valid_precision_0(self): + Format().precision(0) + + def test_invalid_precision_negative(self): + self.assertRaises(Exception, Format().precision, -10) + + def test_invalid_precision_type(self): + self.assertRaises(Exception, Format().precision, 7.7) + + def test_valid_prefix_number(self): + Format().si_prefix(10**-24) + + def test_valid_prefix_named(self): + Format().si_prefix(f.Prefix.micro) + + def test_invalid_prefix_number(self): + self.assertRaises(Exception, Format().si_prefix, 10**-23) + + def test_invalid_prefix_type(self): + self.assertRaises(Exception, Format().si_prefix, '10**-23') + + def test_valid_scheme_string(self): + Format().scheme('s') + + def test_valid_scheme_named(self): + Format().scheme(f.Scheme.decimal) + + def test_invalid_scheme_string(self): + self.assertRaises(Exception, Format().scheme, 'invalid') + + def test_invalid_scheme_type(self): + self.assertRaises(Exception, Format().scheme, 7) + + def test_valid_sign_string(self): + Format().sign('+') + + def test_valid_sign_named(self): + Format().sign(f.Sign.space) + + def test_invalid_sign_string(self): + self.assertRaises(Exception, Format().sign, 'invalid') + + def test_invalid_sign_type(self): + self.assertRaises(Exception, Format().sign, 7) + + def test_valid_symbol_string(self): + Format().symbol('$') + + def test_valid_symbol_named(self): + Format().symbol(f.Symbol.hex) + + def test_invalid_symbol_string(self): + self.assertRaises(Exception, Format().symbol, 'invalid') + + def test_invalid_symbol_type(self): + self.assertRaises(Exception, Format().symbol, 7) + + def test_valid_symbol_prefix(self): + Format().symbol_prefix('abc+-') + + def test_invalid_symbol_prefix_type(self): + self.assertRaises(Exception, Format().symbol_prefix, 7) + + def test_valid_symbol_suffix(self): + Format().symbol_suffix('abc+-') + + def test_invalid_symbol_suffix(self): + self.assertRaises(Exception, Format().symbol_suffix, 7) + + def test_valid_trim_boolean(self): + Format().trim(False) + + def test_valid_trim_string(self): + Format().trim('~') + + def test_valid_trim_named(self): + Format().trim(f.Trim.yes) + + def test_invalid_trim_string(self): + self.assertRaises(Exception, Format().trim, 'invalid') + + def test_invalid_trim_type(self): + self.assertRaises(Exception, Format().trim, 7) + + def test_valid_decimal_delimitor(self): + Format().decimal_delimitor('xyz') + + def test_invalid_decimal_delimitor(self): + self.assertRaises(Exception, Format().decimal_delimitor, 7) + + def test_valid_group_delimiator(self): + Format().group_delimitor('xyz') + + def test_invalid_group_delimitor(self): + self.assertRaises(Exception, Format().group_delimitor, 7) + + def test_valid_groups(self): + Format().groups([3]) + + def test_valid_groups_single(self): + Format().groups(3) + + def test_valid_groups_multi(self): + Format().groups([2, 2, 3]) + + def test_invalid_groups_single_0(self): + self.assertRaises(Exception, Format().groups, 0) + + def test_invalid_groups_single_negative(self): + self.assertRaises(Exception, Format().groups, -7) + + def test_invalid_groups_single_type(self): + self.assertRaises(Exception, Format().groups, 7.7) + + def test_invalid_groups_empty(self): + self.assertRaises(Exception, Format().groups, []) + + def test_invalid_groups_nested_type(self): + self.assertRaises(Exception, Format().groups, [7.7, 7]) + + def test_invalid_groups_nested_0(self): + self.assertRaises(Exception, Format().groups, [3, 3, 0]) + + def test_invalid_groups_nested_negative(self): + self.assertRaises(Exception, Format().groups, [3, 3, -7]) \ No newline at end of file From a2d5babd25f41e635193db18df27ac408bf86184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 25 Feb 2019 14:16:11 -0500 Subject: [PATCH 39/55] - update templates --- dash_table/FormatTemplate.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/dash_table/FormatTemplate.py b/dash_table/FormatTemplate.py index a19cb07e1..a8738496c 100644 --- a/dash_table/FormatTemplate.py +++ b/dash_table/FormatTemplate.py @@ -3,7 +3,12 @@ def money(decimals, sign=Sign.default): - return Format().sign(sign).scheme(Scheme.fixed, decimals).symbol(Symbol.currency).create() + return Format( + sign=sign, + scheme=Scheme.fixed, + precision=decimals + symbol=Symbol.yes + ) def percentage(decimals, rounded: bool=True): @@ -11,4 +16,7 @@ def percentage(decimals, rounded: bool=True): raise Exception('expected rounded to be a boolean') rounded = Scheme.percentage_rounded if rounded else Scheme.percentage - return Format().scheme(rounded, decimals).create() + return Format( + scheme=rounded, + precision=decimals + ) From 4c3a91a045ce3ee7c88128bc9ca7156086da2b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Mon, 25 Feb 2019 14:21:16 -0500 Subject: [PATCH 40/55] - sanity on templates --- dash_table/FormatTemplate.py | 2 +- tests/unit/format_test.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/dash_table/FormatTemplate.py b/dash_table/FormatTemplate.py index a8738496c..6f29eb5b7 100644 --- a/dash_table/FormatTemplate.py +++ b/dash_table/FormatTemplate.py @@ -6,7 +6,7 @@ def money(decimals, sign=Sign.default): return Format( sign=sign, scheme=Scheme.fixed, - precision=decimals + precision=decimals, symbol=Symbol.yes ) diff --git a/tests/unit/format_test.py b/tests/unit/format_test.py index e57ba4fff..6f2b920a0 100644 --- a/tests/unit/format_test.py +++ b/tests/unit/format_test.py @@ -2,6 +2,7 @@ import dash_table.Format as f from dash_table.Format import Format +import dash_table.FormatTemplate as FormatTemplate class FormatTest(unittest.TestCase): def validate_complex(self, res): @@ -40,6 +41,16 @@ def test_complex_and_valid_in_fluent(self): self.validate_complex(res.to_plotly_json()) + def test_money_template(self): + res = FormatTemplate.money(2).to_plotly_json() + + self.assertEqual(res['specifier'], '$.2f') + + def test_percentage_template(self): + res = FormatTemplate.percentage(1, False).to_plotly_json() + + self.assertEqual(res['specifier'], '.1%') + def test_valid_align_named(self): Format().align(f.Align.center) From 52a4d3020836c4986a4fe108dadaa893c2bcb04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Feb 2019 10:20:44 -0500 Subject: [PATCH 41/55] - improve format templates --- dash_table/FormatTemplate.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dash_table/FormatTemplate.py b/dash_table/FormatTemplate.py index 6f29eb5b7..6c84614fe 100644 --- a/dash_table/FormatTemplate.py +++ b/dash_table/FormatTemplate.py @@ -1,17 +1,18 @@ from enum import Enum -from .Format import Format, Scheme, Sign, Symbol +from .Format import Format, Group, Scheme, Sign, Symbol def money(decimals, sign=Sign.default): return Format( - sign=sign, - scheme=Scheme.fixed, + group=Group.yes precision=decimals, + scheme=Scheme.fixed, + sign=sign, symbol=Symbol.yes ) -def percentage(decimals, rounded: bool=True): +def percentage(decimals, rounded: bool=False): if not isinstance(rounded, bool): raise Exception('expected rounded to be a boolean') From 3956b50bdb9771c858494a882a4aed05359a4f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Feb 2019 10:47:42 -0500 Subject: [PATCH 42/55] - fix format tempates --- dash_table/FormatTemplate.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dash_table/FormatTemplate.py b/dash_table/FormatTemplate.py index 6c84614fe..78a034e60 100644 --- a/dash_table/FormatTemplate.py +++ b/dash_table/FormatTemplate.py @@ -1,10 +1,9 @@ from enum import Enum from .Format import Format, Group, Scheme, Sign, Symbol - def money(decimals, sign=Sign.default): return Format( - group=Group.yes + group=Group.yes, precision=decimals, scheme=Scheme.fixed, sign=sign, @@ -12,7 +11,7 @@ def money(decimals, sign=Sign.default): ) -def percentage(decimals, rounded: bool=False): +def percentage(decimals, rounded=False): if not isinstance(rounded, bool): raise Exception('expected rounded to be a boolean') From fd623854999c0cfe428a47e584c57970efc1286c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Feb 2019 10:50:55 -0500 Subject: [PATCH 43/55] fix lint --- dash_table/FormatTemplate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dash_table/FormatTemplate.py b/dash_table/FormatTemplate.py index 78a034e60..f8220e6d6 100644 --- a/dash_table/FormatTemplate.py +++ b/dash_table/FormatTemplate.py @@ -1,6 +1,7 @@ from enum import Enum from .Format import Format, Group, Scheme, Sign, Symbol + def money(decimals, sign=Sign.default): return Format( group=Group.yes, From 4ab6a2cdbd667f53240e31aab846d9a3aeb17cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Feb 2019 10:59:18 -0500 Subject: [PATCH 44/55] - update py formatting tests --- tests/unit/format_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/format_test.py b/tests/unit/format_test.py index 6f2b920a0..d8101fc53 100644 --- a/tests/unit/format_test.py +++ b/tests/unit/format_test.py @@ -44,10 +44,10 @@ def test_complex_and_valid_in_fluent(self): def test_money_template(self): res = FormatTemplate.money(2).to_plotly_json() - self.assertEqual(res['specifier'], '$.2f') + self.assertEqual(res['specifier'], '$,.2f') def test_percentage_template(self): - res = FormatTemplate.percentage(1, False).to_plotly_json() + res = FormatTemplate.percentage(1).to_plotly_json() self.assertEqual(res['specifier'], '.1%') From eb98da7197e3963172cc53de542ef1b22333a059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Feb 2019 11:21:48 -0500 Subject: [PATCH 45/55] py27 fixes --- dash_table/Format.py | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dash_table/Format.py b/dash_table/Format.py index 448456014..6f95037f1 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -2,7 +2,7 @@ import inspect -def get_named_tuple(name: str, dict: dict): +def get_named_tuple(name, dict): return collections.namedtuple(name, dict.keys())(*dict.values()) diff --git a/package.json b/package.json index 73084a346..388490f1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dash-table", - "version": "3.5.0", + "version": "3.6.0rc4", "description": "Dash table", "main": "dash_table/bundle.js", "scripts": { From f2acd4eeb1e91d481f2d391b4de0db456d19449d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Feb 2019 11:22:03 -0500 Subject: [PATCH 46/55] revert version change --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 388490f1f..73084a346 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dash-table", - "version": "3.6.0rc4", + "version": "3.5.0", "description": "Dash table", "main": "dash_table/bundle.js", "scripts": { From ba926f10405b7aba8ad00b600356d8b955d2cc9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Tue, 26 Feb 2019 16:06:36 -0500 Subject: [PATCH 47/55] py27 and py37 support w/ unicode check --- dash_table/Format.py | 23 ++++++++++++++++++----- package.json | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/dash_table/Format.py b/dash_table/Format.py index 6f95037f1..67def455d 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -1,5 +1,6 @@ import collections import inspect +import sys def get_named_tuple(name, dict): @@ -83,6 +84,18 @@ def get_named_tuple(name, dict): }) +def is_string_py2(value): + return isinstance(value, str) or isinstance(value, unicode) + + +def is_string_py3(value): + return isinstance(value, str) + + +def is_string(value): + return is_string_py3(value) if sys.version_info >= (3, 0) else is_string_py2(value) + + class Format(): def __init__(self, **kwargs): self._locale = {} @@ -118,7 +131,7 @@ def align(self, value): return self def fill(self, value): - if not isinstance(value, str) or len(value) != 1: + if not is_string(value) or len(value) != 1: raise Exception('expected value to be a string of length one') self._specifier['fill'] = value @@ -190,7 +203,7 @@ def trim(self, value): return self def symbol_prefix(self, value): - if not isinstance(value, str): + if not is_string(value): raise Exception('expected value to be a string') if 'symbol' not in self._locale: @@ -202,7 +215,7 @@ def symbol_prefix(self, value): # Locale def symbol_suffix(self, value): - if not isinstance(value, str): + if not is_string(value): raise Exception('expected value to be a string') if 'symbol' not in self._locale: @@ -213,14 +226,14 @@ def symbol_suffix(self, value): return self def decimal_delimitor(self, value): - if not isinstance(value, str): + if not is_string(value): raise Exception('expected value to be a string') self._locale['decimal'] = value return self def group_delimitor(self, value): - if not isinstance(value, str): + if not is_string(value): raise Exception('expected value to be a string') self._locale['group'] = value diff --git a/package.json b/package.json index 73084a346..f6d8074c4 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "private::host_dash8083": "python tests/cypress/dash/v_fe_page.py", "private::host_js": "http-server ./dash_table -c-1 --silent", "private::lint:ts": "tslint '{src,demo,tests}/**/*.{js,ts,tsx}' --exclude '**/@Types/*.*'", - "private::lint:py": "flake8 --exclude=DataTable.py,__init__.py,_imports_.py --ignore=E501,F401,F841,F811 dash_table", + "private::lint:py": "flake8 --exclude=DataTable.py,__init__.py,_imports_.py --ignore=E501,F401,F841,F811,F821 dash_table", "private::wait_dash8081": "wait-on http://localhost:8081", "private::wait_dash8082": "wait-on http://localhost:8082", "private::wait_dash8083": "wait-on http://localhost:8083", From 0b24ca89385f11f5f3ae30b102f818a3ebbcf407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 27 Feb 2019 08:25:23 -0500 Subject: [PATCH 48/55] str/unicode detection without version flag --- dash_table/Format.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/dash_table/Format.py b/dash_table/Format.py index 67def455d..b9f91d6e1 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -84,16 +84,8 @@ def get_named_tuple(name, dict): }) -def is_string_py2(value): - return isinstance(value, str) or isinstance(value, unicode) - - -def is_string_py3(value): - return isinstance(value, str) - - def is_string(value): - return is_string_py3(value) if sys.version_info >= (3, 0) else is_string_py2(value) + return isinstance(value, (str, u''.__class__)) class Format(): From 53aa524851d22575ca26c8ba380385e363a83327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 27 Feb 2019 11:15:15 -0500 Subject: [PATCH 49/55] - fix pr comments --- dash_table/Format.py | 95 ++++++++++++--------- src/dash-table/DataTable.js | 10 ++- tests/cypress/tests/unit/formatting_test.ts | 8 +- tests/unit/format_test.py | 76 +++++++++-------- 4 files changed, 104 insertions(+), 85 deletions(-) diff --git a/dash_table/Format.py b/dash_table/Format.py index b9f91d6e1..b8553600c 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -84,10 +84,6 @@ def get_named_tuple(name, dict): }) -def is_string(value): - return isinstance(value, (str, u''.__class__)) - - class Format(): def __init__(self, **kwargs): self._locale = {} @@ -110,21 +106,43 @@ def __init__(self, **kwargs): for kw, val in kwargs.items(): if kw not in valid_methods: - raise Exception('{0} is not a format method. Expected one of'.format(kw), str(list(valid_methods))) + raise TypeError('{0} is not a format method. Expected one of'.format(kw), str(list(valid_methods))) getattr(self, kw)(val) + def _validate_char(self, value): + self._validate_string(value) + + if len(value) != 1: + raise ValueError('excepted value to a string of length one') + + def _validate_non_negative_integer_or_none(self, value): + if value is None: + return + + if not isinstance(value, int): + raise TypeError('expected value to be an integer') + + if value < 0: + raise ValueError('expected value to be non-negative', str(value)) + + def _validate_named(self, value, named_values): + if value not in named_values: + raise TypeError('expceted value to be one of', str(list(named_values))) + + def _validate_string(self, value): + if not isinstance(value, (str, u''.__class__)): + raise TypeError('expected value to be a string') + # Specifier def align(self, value): - if value not in Align: - raise Exception('expected value to be one of', str(list(Align))) + self._validate_named(value, Align) self._specifier['align'] = value return self def fill(self, value): - if not is_string(value) or len(value) != 1: - raise Exception('expected value to be a string of length one') + self._validate_char(value) self._specifier['fill'] = value return self @@ -133,8 +151,7 @@ def group(self, value): if isinstance(value, bool): value = Group.yes if value else Group.no - if value not in Group: - raise Exception('expected value to be one of', str(list(Group))) + self._validate_named(value, Group) self._specifier['group'] = value return self @@ -143,43 +160,37 @@ def padding(self, value): if isinstance(value, bool): value = Padding.yes if value else Padding.no - if value not in Padding: - raise Exception('expected value to be one of', str(list(Padding))) + self._validate_named(value, Padding) self._specifier['padding'] = value return self def padding_width(self, value): - if not isinstance(value, int) or value < 0: - raise Exception('expected value to be a non-negative integer or None', str(value)) + self._validate_non_negative_integer_or_none(value) - self._specifier['width'] = value + self._specifier['width'] = value if value is not None else '' return self def precision(self, value): - if not isinstance(value, int) or value < 0: - raise Exception('expected value to be a non-negative integer or None', str(value)) + self._validate_non_negative_integer_or_none(value) self._specifier['precision'] = '.{0}'.format(value) if value is not None else '' return self def scheme(self, value): - if value not in Scheme: - raise Exception('expected value to be one of', str(list(Scheme))) + self._validate_named(value, Scheme) self._specifier['type'] = value return self def sign(self, value): - if value not in Sign: - raise Exception('expected value to be one of', str(list(Sign))) + self._validate_named(value, Sign) self._specifier['sign'] = value return self def symbol(self, value): - if value not in Symbol: - raise Exception('expected value to be one of', str(list(Symbol))) + self._validate_named(value, Symbol) self._specifier['symbol'] = value return self @@ -188,15 +199,14 @@ def trim(self, value): if isinstance(value, bool): value = Trim.yes if value else Trim.no - if value not in Trim: - raise Exception('expected value to be one of', str(list(Trim))) + self._validate_named(value, Trim) self._specifier['trim'] = value return self + # Locale def symbol_prefix(self, value): - if not is_string(value): - raise Exception('expected value to be a string') + self._validate_string(value) if 'symbol' not in self._locale: self._locale['symbol'] = [value, ''] @@ -205,10 +215,8 @@ def symbol_prefix(self, value): return self - # Locale def symbol_suffix(self, value): - if not is_string(value): - raise Exception('expected value to be a string') + self._validate_string(value) if 'symbol' not in self._locale: self._locale['symbol'] = ['', value] @@ -218,15 +226,13 @@ def symbol_suffix(self, value): return self def decimal_delimitor(self, value): - if not is_string(value): - raise Exception('expected value to be a string') + self._validate_char(value) self._locale['decimal'] = value return self def group_delimitor(self, value): - if not is_string(value): - raise Exception('expected value to be a string') + self._validate_char(value) self._locale['group'] = value return self @@ -234,12 +240,18 @@ def group_delimitor(self, value): def groups(self, groups): groups = groups if isinstance(groups, list) else [groups] if isinstance(groups, int) else None - if not isinstance(groups, list) or len(groups) == 0: - raise Exception('expected groups to be an integer or a list of integers') + if not isinstance(groups, list): + raise TypeError('expected groups to be an integer or a list of integers') + + if len(groups) == 0: + raise ValueError('expected groups to be an integer or a list of one or more integers') for group in groups: - if not isinstance(group, int) or group <= 0: - raise Exception('expected entry to be a non-negative integer') + if not isinstance(group, int): + raise TypeError('expected entry to be an integer') + + if group <= 0: + raise ValueError('expected entry to be a non-negative integer') self._locale['grouping'] = groups return self @@ -251,8 +263,7 @@ def nully(self, value): # Prefix def si_prefix(self, value): - if value not in Prefix: - raise Exception('expected value to be one of', str(list(Prefix))) + self._validate_named(value, Prefix) self._prefix = value return self @@ -262,7 +273,7 @@ def to_plotly_json(self): f['locale'] = self._locale.copy() f['nully'] = self._nully f['prefix'] = self._prefix - f['specifier'] = '{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}'.format( + f['specifier'] = '{}{}{}{}{}{}{}{}{}{}'.format( self._specifier['fill'] if self._specifier['align'] != Align.default else '', self._specifier['align'], self._specifier['sign'], diff --git a/src/dash-table/DataTable.js b/src/dash-table/DataTable.js index c96f40a11..36e085dc2 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/DataTable.js @@ -155,8 +155,9 @@ export const propTypes = { * 'locale': represents localization specific formatting information * When left unspecified, will use the default value provided by d3-format. * - * 'symbol': (default: ['', $']) a list of two strings representing the - * prefix and suffix symbols + * 'symbol': (default: ['$', '']) a list of two strings representing the + * prefix and suffix symbols. Typically used for currency, and implemented using d3's + * currency format, but you can use this for other symbols such as measurement units. * 'decimal': (default: '.') the string used for the decimal separator * 'group': (default: ',') the string used for the groups separator * 'grouping': (default: [3]) a list of integers representing the grouping pattern @@ -315,8 +316,9 @@ export const propTypes = { * * When left unspecified, each individual nested prop will default to a pre-determined value. * - * 'symbol': (default: ['', $']) a list of two strings representing the - * prefix and suffix symbols + * 'symbol': (default: ['$', '']) a list of two strings representing the + * prefix and suffix symbols. Typically used for currency, and implemented using d3's + * currency format, but you can use this for other symbols such as measurement units. * 'decimal': (default: '.') the string used for the decimal separator * 'group': (default: ',') the string used for the groups separator * 'grouping': (default: [3]) a list of integers representing the grouping pattern diff --git a/tests/cypress/tests/unit/formatting_test.ts b/tests/cypress/tests/unit/formatting_test.ts index 5ac54544d..9f243a67c 100644 --- a/tests/cypress/tests/unit/formatting_test.ts +++ b/tests/cypress/tests/unit/formatting_test.ts @@ -32,7 +32,7 @@ describe('formatting', () => { expect(formatter(1766)).to.equal('$1766.00'); expect(formatter(''), 'Empty string case').to.equal(''); expect(formatter('foo'), 'Foo string case').to.equal('foo'); - expect(formatter(true), 'Empty string case').to.equal(true); + expect(formatter(true)).to.equal(true); expect(formatter(NaN), 'NaN case').to.equal(''); expect(formatter(Infinity), 'Infinity case').to.equal(''); expect(formatter(-Infinity), '-Infinity case').to.equal(''); @@ -60,7 +60,7 @@ describe('formatting', () => { expect(formatter(1299431)).to.equal('1,300,000'); expect(formatter(''), 'Empty string case').to.equal(''); expect(formatter('foo'), 'Foo string case').to.equal('foo'); - expect(formatter(true), 'Empty string case').to.equal(true); + expect(formatter(true)).to.equal(true); expect(formatter(NaN), 'NaN case').to.equal('42'); expect(formatter(Infinity), 'Infinity case').to.equal('42'); expect(formatter(-Infinity), '-Infinity case').to.equal('42'); @@ -92,7 +92,7 @@ describe('formatting', () => { expect(formatter(1299431)).to.equal('1y2y99y4y31x00'); expect(formatter(''), 'Empty string case').to.equal(''); expect(formatter('foo'), 'Foo string case').to.equal('foo'); - expect(formatter(true), 'Empty string case').to.equal(true); + expect(formatter(true)).to.equal(true); expect(formatter(NaN), 'NaN case').to.equal('42.4242'); expect(formatter(Infinity), 'Infinity case').to.equal('42.4242'); expect(formatter(-Infinity), '-Infinity case').to.equal('42.4242'); @@ -141,7 +141,7 @@ describe('formatting', () => { expect(formatter(1766)).to.equal('1766000m'); expect(formatter(''), 'Empty string case').to.equal(''); expect(formatter('foo'), 'Foo string case').to.equal('foo'); - expect(formatter(true), 'Empty string case').to.equal(true); + expect(formatter(true)).to.equal(true); expect(formatter(NaN), 'NaN case').to.equal(''); expect(formatter(Infinity), 'Infinity case').to.equal(''); expect(formatter(-Infinity), '-Infinity case').to.equal(''); diff --git a/tests/unit/format_test.py b/tests/unit/format_test.py index d8101fc53..02344f48a 100644 --- a/tests/unit/format_test.py +++ b/tests/unit/format_test.py @@ -58,19 +58,19 @@ def test_valid_align_string(self): Format().align('=') def test_invalid_align_string(self): - self.assertRaises(Exception, Format().align, 'i') + self.assertRaises(TypeError, Format().align, 'i') def test_invalid_align_type(self): - self.assertRaises(Exception, Format().align, 7) + self.assertRaises(TypeError, Format().align, 7) def test_valid_fill(self): Format().fill('.') def test_invalid_fill_length(self): - self.assertRaises(Exception, Format().fill, 'invalid') + self.assertRaises(ValueError, Format().fill, 'invalid') def test_invalid_fill_type(self): - self.assertRaises(Exception, Format().fill, 7) + self.assertRaises(TypeError, Format().fill, 7) def test_valid_group_bool(self): Format().group(True) @@ -82,10 +82,10 @@ def test_valid_group_named(self): Format().group(f.Group.no) def test_invalid_group_type(self): - self.assertRaises(Exception, Format().group, 7) + self.assertRaises(TypeError, Format().group, 7) def test_invalid_group_string(self): - self.assertRaises(Exception, Format().group, 'invalid') + self.assertRaises(TypeError, Format().group, 'invalid') def test_valid_padding_bool(self): Format().padding(False) @@ -97,10 +97,10 @@ def test_valid_padding_named(self): Format().padding(f.Padding.no) def test_invalid_padding_type(self): - self.assertRaises(Exception, Format().padding, 7) + self.assertRaises(TypeError, Format().padding, 7) def test_invalid_padding_string(self): - self.assertRaises(Exception, Format().padding, 'invalid') + self.assertRaises(TypeError, Format().padding, 'invalid') def test_valid_padding_width(self): Format().padding_width(10) @@ -109,10 +109,10 @@ def test_valid_padding_width_0(self): Format().padding_width(0) def test_invalid_padding_width_negative(self): - self.assertRaises(Exception, Format().padding_width, -10) + self.assertRaises(ValueError, Format().padding_width, -10) def test_invalid_padding_width_type(self): - self.assertRaises(Exception, Format().padding_width, 7.7) + self.assertRaises(TypeError, Format().padding_width, 7.7) def test_valid_precision(self): Format().precision(10) @@ -121,10 +121,10 @@ def test_valid_precision_0(self): Format().precision(0) def test_invalid_precision_negative(self): - self.assertRaises(Exception, Format().precision, -10) + self.assertRaises(ValueError, Format().precision, -10) def test_invalid_precision_type(self): - self.assertRaises(Exception, Format().precision, 7.7) + self.assertRaises(TypeError, Format().precision, 7.7) def test_valid_prefix_number(self): Format().si_prefix(10**-24) @@ -133,10 +133,10 @@ def test_valid_prefix_named(self): Format().si_prefix(f.Prefix.micro) def test_invalid_prefix_number(self): - self.assertRaises(Exception, Format().si_prefix, 10**-23) + self.assertRaises(TypeError, Format().si_prefix, 10**-23) def test_invalid_prefix_type(self): - self.assertRaises(Exception, Format().si_prefix, '10**-23') + self.assertRaises(TypeError, Format().si_prefix, '10**-23') def test_valid_scheme_string(self): Format().scheme('s') @@ -145,10 +145,10 @@ def test_valid_scheme_named(self): Format().scheme(f.Scheme.decimal) def test_invalid_scheme_string(self): - self.assertRaises(Exception, Format().scheme, 'invalid') + self.assertRaises(TypeError, Format().scheme, 'invalid') def test_invalid_scheme_type(self): - self.assertRaises(Exception, Format().scheme, 7) + self.assertRaises(TypeError, Format().scheme, 7) def test_valid_sign_string(self): Format().sign('+') @@ -157,10 +157,10 @@ def test_valid_sign_named(self): Format().sign(f.Sign.space) def test_invalid_sign_string(self): - self.assertRaises(Exception, Format().sign, 'invalid') + self.assertRaises(TypeError, Format().sign, 'invalid') def test_invalid_sign_type(self): - self.assertRaises(Exception, Format().sign, 7) + self.assertRaises(TypeError, Format().sign, 7) def test_valid_symbol_string(self): Format().symbol('$') @@ -169,22 +169,22 @@ def test_valid_symbol_named(self): Format().symbol(f.Symbol.hex) def test_invalid_symbol_string(self): - self.assertRaises(Exception, Format().symbol, 'invalid') + self.assertRaises(TypeError, Format().symbol, 'invalid') def test_invalid_symbol_type(self): - self.assertRaises(Exception, Format().symbol, 7) + self.assertRaises(TypeError, Format().symbol, 7) def test_valid_symbol_prefix(self): Format().symbol_prefix('abc+-') def test_invalid_symbol_prefix_type(self): - self.assertRaises(Exception, Format().symbol_prefix, 7) + self.assertRaises(TypeError, Format().symbol_prefix, 7) def test_valid_symbol_suffix(self): Format().symbol_suffix('abc+-') def test_invalid_symbol_suffix(self): - self.assertRaises(Exception, Format().symbol_suffix, 7) + self.assertRaises(TypeError, Format().symbol_suffix, 7) def test_valid_trim_boolean(self): Format().trim(False) @@ -196,22 +196,28 @@ def test_valid_trim_named(self): Format().trim(f.Trim.yes) def test_invalid_trim_string(self): - self.assertRaises(Exception, Format().trim, 'invalid') + self.assertRaises(TypeError, Format().trim, 'invalid') def test_invalid_trim_type(self): - self.assertRaises(Exception, Format().trim, 7) + self.assertRaises(TypeError, Format().trim, 7) def test_valid_decimal_delimitor(self): - Format().decimal_delimitor('xyz') + Format().decimal_delimitor('x') + + def test_valid_decimal_delimitor(self): + self.assertRaises(ValueError, Format().decimal_delimitor, 'xyz') def test_invalid_decimal_delimitor(self): - self.assertRaises(Exception, Format().decimal_delimitor, 7) + self.assertRaises(TypeError, Format().decimal_delimitor, 7) + + def test_valid_group_delimiator(self): + Format().group_delimitor('y') def test_valid_group_delimiator(self): - Format().group_delimitor('xyz') + self.assertRaises(ValueError, Format().group_delimitor, 'xyz') def test_invalid_group_delimitor(self): - self.assertRaises(Exception, Format().group_delimitor, 7) + self.assertRaises(TypeError, Format().group_delimitor, 7) def test_valid_groups(self): Format().groups([3]) @@ -223,22 +229,22 @@ def test_valid_groups_multi(self): Format().groups([2, 2, 3]) def test_invalid_groups_single_0(self): - self.assertRaises(Exception, Format().groups, 0) + self.assertRaises(ValueError, Format().groups, 0) def test_invalid_groups_single_negative(self): - self.assertRaises(Exception, Format().groups, -7) + self.assertRaises(ValueError, Format().groups, -7) def test_invalid_groups_single_type(self): - self.assertRaises(Exception, Format().groups, 7.7) + self.assertRaises(TypeError, Format().groups, 7.7) def test_invalid_groups_empty(self): - self.assertRaises(Exception, Format().groups, []) + self.assertRaises(ValueError, Format().groups, []) def test_invalid_groups_nested_type(self): - self.assertRaises(Exception, Format().groups, [7.7, 7]) + self.assertRaises(TypeError, Format().groups, [7.7, 7]) def test_invalid_groups_nested_0(self): - self.assertRaises(Exception, Format().groups, [3, 3, 0]) + self.assertRaises(ValueError, Format().groups, [3, 3, 0]) def test_invalid_groups_nested_negative(self): - self.assertRaises(Exception, Format().groups, [3, 3, -7]) \ No newline at end of file + self.assertRaises(ValueError, Format().groups, [3, 3, -7]) \ No newline at end of file From fc74d021ca0965641ca0ab23146e0f2cdd7bf55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Wed, 27 Feb 2019 11:20:15 -0500 Subject: [PATCH 50/55] meta --- dash_table/DataTable.py | 5 +++-- dash_table/metadata.json | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dash_table/DataTable.py b/dash_table/DataTable.py index 0a97c692d..7b10c1ece 100644 --- a/dash_table/DataTable.py +++ b/dash_table/DataTable.py @@ -18,8 +18,9 @@ class DataTable(Component): When left unspecified, each individual nested prop will default to a pre-determined value. - 'symbol': (default: ['', $']) a list of two strings representing the - prefix and suffix symbols + 'symbol': (default: ['$', '']) a list of two strings representing the + prefix and suffix symbols. Typically used for currency, and implemented using d3's + currency format, but you can use this for other symbols such as measurement units. 'decimal': (default: '.') the string used for the decimal separator 'group': (default: ',') the string used for the groups separator 'grouping': (default: [3]) a list of integers representing the grouping pattern diff --git a/dash_table/metadata.json b/dash_table/metadata.json index ef69747c2..3ef42fdd7 100644 --- a/dash_table/metadata.json +++ b/dash_table/metadata.json @@ -116,7 +116,7 @@ "required": false } }, - "description": "The formatting applied to the column's data.\n\nThis prop is derived from the [d3-format](https://github.com/d3/d3-format) library specification. Apart from\nbeing structured slightly differently (under a single prop), the usage\nis the same.\n\n'locale': represents localization specific formatting information\n When left unspecified, will use the default value provided by d3-format.\n\n 'symbol': (default: ['', $']) a list of two strings representing the\n prefix and suffix symbols\n 'decimal': (default: '.') the string used for the decimal separator\n 'group': (default: ',') the string used for the groups separator\n 'grouping': (default: [3]) a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': (default: '%') the string used for the percentage symbol\n 'separate_4digits': (default: True) separate integers with 4-digits or less\n\n'nully': a value that will be used in place of the nully value during formatting\n If the value type matches the column type, it will be formatted normally\n'prefix': a number representing the SI unit to use during formatting\n See `dash_table.Format.Prefix` enumeration for the list of valid values\n'specifier': (default: '') represents the rules to apply when formatting the number\n\ndash_table.FormatTemplate contains helper functions to rapidly use certain\ntypical number formats.", + "description": "The formatting applied to the column's data.\n\nThis prop is derived from the [d3-format](https://github.com/d3/d3-format) library specification. Apart from\nbeing structured slightly differently (under a single prop), the usage\nis the same.\n\n'locale': represents localization specific formatting information\n When left unspecified, will use the default value provided by d3-format.\n\n 'symbol': (default: ['$', '']) a list of two strings representing the\n prefix and suffix symbols. Typically used for currency, and implemented using d3's\n currency format, but you can use this for other symbols such as measurement units.\n 'decimal': (default: '.') the string used for the decimal separator\n 'group': (default: ',') the string used for the groups separator\n 'grouping': (default: [3]) a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': (default: '%') the string used for the percentage symbol\n 'separate_4digits': (default: True) separate integers with 4-digits or less\n\n'nully': a value that will be used in place of the nully value during formatting\n If the value type matches the column type, it will be formatted normally\n'prefix': a number representing the SI unit to use during formatting\n See `dash_table.Format.Prefix` enumeration for the list of valid values\n'specifier': (default: '') represents the rules to apply when formatting the number\n\ndash_table.FormatTemplate contains helper functions to rapidly use certain\ntypical number formats.", "required": false }, "hidden": { @@ -333,7 +333,7 @@ } }, "required": false, - "description": "The localization specific formatting information applied to all columns in the table.\n\nThis prop is derived from the [d3.formatLocale](https://github.com/d3/d3-format#formatLocale) data structure specification.\n\nWhen left unspecified, each individual nested prop will default to a pre-determined value.\n\n 'symbol': (default: ['', $']) a list of two strings representing the\n prefix and suffix symbols\n 'decimal': (default: '.') the string used for the decimal separator\n 'group': (default: ',') the string used for the groups separator\n 'grouping': (default: [3]) a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': (default: '%') the string used for the percentage symbol\n 'separate_4digits': (default: True) separate integers with 4-digits or less" + "description": "The localization specific formatting information applied to all columns in the table.\n\nThis prop is derived from the [d3.formatLocale](https://github.com/d3/d3-format#formatLocale) data structure specification.\n\nWhen left unspecified, each individual nested prop will default to a pre-determined value.\n\n 'symbol': (default: ['$', '']) a list of two strings representing the\n prefix and suffix symbols. Typically used for currency, and implemented using d3's\n currency format, but you can use this for other symbols such as measurement units.\n 'decimal': (default: '.') the string used for the decimal separator\n 'group': (default: ',') the string used for the groups separator\n 'grouping': (default: [3]) a list of integers representing the grouping pattern\n 'numerals': a list of ten strings used as replacements for numbers 0-9\n 'percent': (default: '%') the string used for the percentage symbol\n 'separate_4digits': (default: True) separate integers with 4-digits or less" }, "content_style": { "type": { From 7a7c8ff328e9cf39bbbe708db84a9a93d4fdfeec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 Feb 2019 08:27:04 -0500 Subject: [PATCH 51/55] - typo - exception type - datatable location / file rename --- dash_table/Format.py | 2 +- dash_table/FormatTemplate.py | 2 +- dash_table/metadata.json | 2 +- package.json | 2 +- src/dash-table/{ => dash}/DataTable.js | 6 +++--- src/dash-table/{DataTable.sanitize.ts => dash/sanitize.ts} | 4 ++-- src/dash-table/{DataTable.validate.ts => dash/validate.ts} | 0 src/dash-table/index.ts | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) rename src/dash-table/{ => dash}/DataTable.js (99%) rename src/dash-table/{DataTable.sanitize.ts => dash/sanitize.ts} (94%) rename src/dash-table/{DataTable.validate.ts => dash/validate.ts} (100%) diff --git a/dash_table/Format.py b/dash_table/Format.py index b8553600c..5ded9cff2 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -128,7 +128,7 @@ def _validate_non_negative_integer_or_none(self, value): def _validate_named(self, value, named_values): if value not in named_values: - raise TypeError('expceted value to be one of', str(list(named_values))) + raise TypeError('expected value to be one of', str(list(named_values))) def _validate_string(self, value): if not isinstance(value, (str, u''.__class__)): diff --git a/dash_table/FormatTemplate.py b/dash_table/FormatTemplate.py index f8220e6d6..b75ce22aa 100644 --- a/dash_table/FormatTemplate.py +++ b/dash_table/FormatTemplate.py @@ -14,7 +14,7 @@ def money(decimals, sign=Sign.default): def percentage(decimals, rounded=False): if not isinstance(rounded, bool): - raise Exception('expected rounded to be a boolean') + raise TypeError('expected rounded to be a boolean') rounded = Scheme.percentage_rounded if rounded else Scheme.percentage return Format( diff --git a/dash_table/metadata.json b/dash_table/metadata.json index 3ef42fdd7..14d8a704d 100644 --- a/dash_table/metadata.json +++ b/dash_table/metadata.json @@ -1,5 +1,5 @@ { - "src/dash-table/DataTable.js": { + "src/dash-table/dash/DataTable.js": { "description": "", "displayName": "DataTable", "methods": [], diff --git a/package.json b/package.json index 17b910cfa..d75ab077e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "private::build:js-dev": "run-s \"private::build -- --mode development\"", "private::build:js-test": "run-s \"private::build -- --config webpack.test.config.js\"", "private::build:js-test-watch": "run-s \"private::build -- --config webpack.test.config.js --watch\"", - "private::build:extract-meta": "node ./extract-meta src/dash-table/DataTable.js > dash_table/metadata.json", + "private::build:extract-meta": "node ./extract-meta src/dash-table/dash/DataTable.js > dash_table/metadata.json", "private::build:copy-package-info": "cp package.json dash_table/package-info.json", "private::build:generate-classes": "python -c \"import dash; dash.development.component_loader.generate_classes('dash_table', 'dash_table/metadata.json')\"", "private::build:py": "run-s private::build:copy-package-info private::build:extract-meta private::build:generate-classes", diff --git a/src/dash-table/DataTable.js b/src/dash-table/dash/DataTable.js similarity index 99% rename from src/dash-table/DataTable.js rename to src/dash-table/dash/DataTable.js index 36e085dc2..bbed67441 100644 --- a/src/dash-table/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -6,9 +6,9 @@ import RealTable from 'dash-table/components/Table'; import Logger from 'core/Logger'; -import genRandomId from './utils/generate'; -import isValidProps from './DataTable.validate'; -import sanitizeProps from './DataTable.sanitize'; +import genRandomId from 'dash-table/utils/generate'; +import isValidProps from './validate'; +import sanitizeProps from './sanitize'; export default class DataTable extends Component { constructor(props) { diff --git a/src/dash-table/DataTable.sanitize.ts b/src/dash-table/dash/sanitize.ts similarity index 94% rename from src/dash-table/DataTable.sanitize.ts rename to src/dash-table/dash/sanitize.ts index df34e6219..2bdecdf15 100644 --- a/src/dash-table/DataTable.sanitize.ts +++ b/src/dash-table/dash/sanitize.ts @@ -1,8 +1,8 @@ + import * as R from 'ramda'; import { memoizeOne } from 'core/memoizer'; - -import { Columns, ColumnType, INumberLocale } from './components/Table/props'; +import { Columns, ColumnType, INumberLocale } from 'dash-table/components/Table/props'; const D3_DEFAULT_LOCALE: INumberLocale = { symbol: ['$', ''], diff --git a/src/dash-table/DataTable.validate.ts b/src/dash-table/dash/validate.ts similarity index 100% rename from src/dash-table/DataTable.validate.ts rename to src/dash-table/dash/validate.ts diff --git a/src/dash-table/index.ts b/src/dash-table/index.ts index 908127d0b..1fd0452ae 100644 --- a/src/dash-table/index.ts +++ b/src/dash-table/index.ts @@ -1,7 +1,7 @@ import Environment from 'core/environment'; import Logger from 'core/Logger'; -import DataTable from 'dash-table/DataTable'; +import DataTable from 'dash-table/dash/DataTable'; Logger.setDebugLevel(Environment.debugLevel); Logger.setLogLevel(Environment.logLevel); From 7916b29385a149c293778671531aee7b8f91c0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 Feb 2019 08:32:44 -0500 Subject: [PATCH 52/55] - fix tests --- tests/cypress/tests/unit/formatting_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cypress/tests/unit/formatting_test.ts b/tests/cypress/tests/unit/formatting_test.ts index 9f243a67c..3e76d6fc0 100644 --- a/tests/cypress/tests/unit/formatting_test.ts +++ b/tests/cypress/tests/unit/formatting_test.ts @@ -1,5 +1,5 @@ import { getFormatter } from 'dash-table/type/number'; -import { getLocale, getNully, getSpecifier } from 'dash-table/DataTable.sanitize'; +import { getLocale, getNully, getSpecifier } from 'dash-table/dash/sanitize'; describe('formatting', () => { describe('number', () => { From fe73302113b6d3ff52692057480a925ecd5d47c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 Feb 2019 09:18:34 -0500 Subject: [PATCH 53/55] - spellcheck --- .vscode/settings.json | 37 +++++++++++++++++++++++++++++++- dash_table/Format.py | 4 ++-- src/dash-table/dash/DataTable.js | 4 ++-- tests/unit/format_test.py | 26 +++++++++++----------- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 69691038e..67a1ebaf6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,38 @@ { - "tslint.jsEnable": true + "tslint.jsEnable": true, + "cSpell.allowCompoundWords": true, + "cSpell.ignorePaths": [ + "**/package.json", + "**/package-lock.json", + "**/node_modules/**", + "**/vscode-extension/**", + "**/.git/**", + ".vscode", + "typings" + ], + "cSpell.language": "en", + "cSpell.diagnosticLevel": "Error", + "cSpell.languageSettings": [ + { "languageId": "*", "dictionaries": ["fonts", "css", "html", "npm", "typescript", "python"]} + ], + "cSpell.words": [ + "atto", + "deletable", + "femto", + "giga", + "ints", + "milli", + "nully", + "peta", + "pico", + "plotly", + "selectable", + "tera", + "tooltips", + "uneditable", + "yocto", + "yotta", + "zepto", + "zetta" + ] } \ No newline at end of file diff --git a/dash_table/Format.py b/dash_table/Format.py index 5ded9cff2..52234b185 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -225,13 +225,13 @@ def symbol_suffix(self, value): return self - def decimal_delimitor(self, value): + def decimal_delimiter(self, value): self._validate_char(value) self._locale['decimal'] = value return self - def group_delimitor(self, value): + def group_delimiter(self, value): self._validate_char(value) self._locale['group'] = value diff --git a/src/dash-table/dash/DataTable.js b/src/dash-table/dash/DataTable.js index bbed67441..5be2d8c53 100644 --- a/src/dash-table/dash/DataTable.js +++ b/src/dash-table/dash/DataTable.js @@ -265,7 +265,7 @@ export const propTypes = { /** * DEPRECATED * Please use `column_static_dropdown` instead. - * NOTE - Dropdown behaviour will likely change in the future, + * NOTE - Dropdown behavior will likely change in the future, * subscribe to [https://github.com/plotly/dash-table/issues/168](https://github.com/plotly/dash-table/issues/168) * for more information. */ @@ -666,7 +666,7 @@ export const propTypes = { * in the list. Higher priority (more specific) conditional * tooltips should be put at the beginning of the list. * - * The `if` refers to the condtion that needs to be fulfilled + * The `if` refers to the condition that needs to be fulfilled * in order for the associated tooltip configuration to be * used. If multiple conditions are defined, all conditions * must be met for the tooltip to be used by a cell. diff --git a/tests/unit/format_test.py b/tests/unit/format_test.py index 02344f48a..a31b0e78c 100644 --- a/tests/unit/format_test.py +++ b/tests/unit/format_test.py @@ -27,8 +27,8 @@ def test_complex_and_valid_in_ctor(self): symbol=f.Symbol.yes, symbol_prefix='a', symbol_suffix='bc', - decimal_delimitor='x', - group_delimitor='y', + decimal_delimiter='x', + group_delimiter='y', groups=[2, 2, 2, 3], nully='N/A', si_prefix=f.Prefix.none @@ -37,7 +37,7 @@ def test_complex_and_valid_in_ctor(self): self.validate_complex(res.to_plotly_json()) def test_complex_and_valid_in_fluent(self): - res = Format().align(f.Align.center).fill('.').group(f.Group.yes).padding(True).padding_width(10).precision(6).scheme('s').sign(f.Sign.parantheses).symbol(f.Symbol.yes).symbol_prefix('a').symbol_suffix('bc').decimal_delimitor('x').group_delimitor('y').groups([2, 2, 2, 3]).nully('N/A').si_prefix(f.Prefix.none) + res = Format().align(f.Align.center).fill('.').group(f.Group.yes).padding(True).padding_width(10).precision(6).scheme('s').sign(f.Sign.parantheses).symbol(f.Symbol.yes).symbol_prefix('a').symbol_suffix('bc').decimal_delimiter('x').group_delimiter('y').groups([2, 2, 2, 3]).nully('N/A').si_prefix(f.Prefix.none) self.validate_complex(res.to_plotly_json()) @@ -201,23 +201,23 @@ def test_invalid_trim_string(self): def test_invalid_trim_type(self): self.assertRaises(TypeError, Format().trim, 7) - def test_valid_decimal_delimitor(self): - Format().decimal_delimitor('x') + def test_valid_decimal_delimiter(self): + Format().decimal_delimiter('x') - def test_valid_decimal_delimitor(self): - self.assertRaises(ValueError, Format().decimal_delimitor, 'xyz') + def test_valid_decimal_delimiter(self): + self.assertRaises(ValueError, Format().decimal_delimiter, 'xyz') - def test_invalid_decimal_delimitor(self): - self.assertRaises(TypeError, Format().decimal_delimitor, 7) + def test_invalid_decimal_delimiter(self): + self.assertRaises(TypeError, Format().decimal_delimiter, 7) def test_valid_group_delimiator(self): - Format().group_delimitor('y') + Format().group_delimiter('y') def test_valid_group_delimiator(self): - self.assertRaises(ValueError, Format().group_delimitor, 'xyz') + self.assertRaises(ValueError, Format().group_delimiter, 'xyz') - def test_invalid_group_delimitor(self): - self.assertRaises(TypeError, Format().group_delimitor, 7) + def test_invalid_group_delimiter(self): + self.assertRaises(TypeError, Format().group_delimiter, 7) def test_valid_groups(self): Format().groups([3]) From a4c6789978e33d2fa88c9ea6b16c1e4ef392eb09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 Feb 2019 09:24:34 -0500 Subject: [PATCH 54/55] fix tests --- dash_table/DataTable.py | 2 +- dash_table/metadata.json | 4 ++-- tests/visual/percy-storybook/Border.percy.tsx | 2 +- tests/visual/percy-storybook/DashTable.percy.tsx | 2 +- tests/visual/percy-storybook/Filters.percy.tsx | 2 +- tests/visual/percy-storybook/Style.percy.tsx | 2 +- tests/visual/percy-storybook/TriadValidation.percy.tsx | 2 +- tests/visual/percy-storybook/Width.all.percy.tsx | 2 +- tests/visual/percy-storybook/Width.defaults.percy.tsx | 2 +- tests/visual/percy-storybook/Width.max.percy.tsx | 2 +- tests/visual/percy-storybook/Width.min.percy.tsx | 2 +- tests/visual/percy-storybook/Width.percentages.percy.tsx | 2 +- tests/visual/percy-storybook/Width.width.percy.tsx | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/dash_table/DataTable.py b/dash_table/DataTable.py index 7b10c1ece..09d08cfe2 100644 --- a/dash_table/DataTable.py +++ b/dash_table/DataTable.py @@ -231,7 +231,7 @@ class DataTable(Component): in the list. Higher priority (more specific) conditional tooltips should be put at the beginning of the list. -The `if` refers to the condtion that needs to be fulfilled +The `if` refers to the condition that needs to be fulfilled in order for the associated tooltip configuration to be used. If multiple conditions are defined, all conditions must be met for the tooltip to be used by a cell. diff --git a/dash_table/metadata.json b/dash_table/metadata.json index 14d8a704d..76cd3d5a6 100644 --- a/dash_table/metadata.json +++ b/dash_table/metadata.json @@ -253,7 +253,7 @@ } } }, - "description": "DEPRECATED\nPlease use `column_static_dropdown` instead.\nNOTE - Dropdown behaviour will likely change in the future,\nsubscribe to [https://github.com/plotly/dash-table/issues/168](https://github.com/plotly/dash-table/issues/168)\nfor more information.", + "description": "DEPRECATED\nPlease use `column_static_dropdown` instead.\nNOTE - Dropdown behavior will likely change in the future,\nsubscribe to [https://github.com/plotly/dash-table/issues/168](https://github.com/plotly/dash-table/issues/168)\nfor more information.", "required": false }, "type": { @@ -844,7 +844,7 @@ } }, "required": false, - "description": "`column_conditional_tooltips` represents the tooltip shown\nfor different columns and cells.\n\nThis property allows you to specify different tooltips for\ndepending on certain conditions. For example, you may have\ndifferent tooltips in the same column based on the value\nof a certain data property.\n\nPriority is from first to last defined conditional tooltip\nin the list. Higher priority (more specific) conditional\ntooltips should be put at the beginning of the list.\n\nThe `if` refers to the condtion that needs to be fulfilled\nin order for the associated tooltip configuration to be\nused. If multiple conditions are defined, all conditions\nmust be met for the tooltip to be used by a cell.\n\nThe `if` nested property `column_id` refers to the column\nID that must be matched.\nThe `if` nested property `row_index` refers to the index\nof the row in the source `data`.\nThe `if` nested property `filter` refers to the query that\nmust evaluate to True.\n\nThe `type` refers to the type of tooltip syntax used\nfor the tooltip generation. Can either be `markdown`\nor `text`. Defaults to `text`.\nThe `value` refers to the syntax-based content of\nthe tooltip. This value is required.\nThe `delay` represents the delay in milliseconds before\nthe tooltip is shown when hovering a cell. This overrides\nthe table's `tooltip_delay` property. If set to `null`,\nthe tooltip will be shown immediately.\nThe `duration` represents the duration in milliseconds\nduring which the tooltip is shown when hovering a cell.\nThis overrides the table's `tooltip_duration` property.\nIf set to `null`, the tooltip will not disappear.", + "description": "`column_conditional_tooltips` represents the tooltip shown\nfor different columns and cells.\n\nThis property allows you to specify different tooltips for\ndepending on certain conditions. For example, you may have\ndifferent tooltips in the same column based on the value\nof a certain data property.\n\nPriority is from first to last defined conditional tooltip\nin the list. Higher priority (more specific) conditional\ntooltips should be put at the beginning of the list.\n\nThe `if` refers to the condition that needs to be fulfilled\nin order for the associated tooltip configuration to be\nused. If multiple conditions are defined, all conditions\nmust be met for the tooltip to be used by a cell.\n\nThe `if` nested property `column_id` refers to the column\nID that must be matched.\nThe `if` nested property `row_index` refers to the index\nof the row in the source `data`.\nThe `if` nested property `filter` refers to the query that\nmust evaluate to True.\n\nThe `type` refers to the type of tooltip syntax used\nfor the tooltip generation. Can either be `markdown`\nor `text`. Defaults to `text`.\nThe `value` refers to the syntax-based content of\nthe tooltip. This value is required.\nThe `delay` represents the delay in milliseconds before\nthe tooltip is shown when hovering a cell. This overrides\nthe table's `tooltip_delay` property. If set to `null`,\nthe tooltip will be shown immediately.\nThe `duration` represents the duration in milliseconds\nduring which the tooltip is shown when hovering a cell.\nThis overrides the table's `tooltip_duration` property.\nIf set to `null`, the tooltip will not disappear.", "defaultValue": { "value": "[]", "computed": false diff --git a/tests/visual/percy-storybook/Border.percy.tsx b/tests/visual/percy-storybook/Border.percy.tsx index 3fcb60e43..0f60e9732 100644 --- a/tests/visual/percy-storybook/Border.percy.tsx +++ b/tests/visual/percy-storybook/Border.percy.tsx @@ -2,7 +2,7 @@ import * as R from 'ramda'; import React from 'react'; import { storiesOf } from '@storybook/react'; import random from 'core/math/random'; -import DataTable from 'dash-table/DataTable'; +import DataTable from 'dash-table/dash/DataTable'; const setProps = () => { }; diff --git a/tests/visual/percy-storybook/DashTable.percy.tsx b/tests/visual/percy-storybook/DashTable.percy.tsx index 5fd1e9005..c8e6fa184 100644 --- a/tests/visual/percy-storybook/DashTable.percy.tsx +++ b/tests/visual/percy-storybook/DashTable.percy.tsx @@ -2,7 +2,7 @@ import * as R from 'ramda'; import React from 'react'; import { storiesOf } from '@storybook/react'; import random from 'core/math/random'; -import DataTable from 'dash-table/DataTable'; +import DataTable from 'dash-table/dash/DataTable'; import fixtures from './fixtures'; const setProps = () => { }; diff --git a/tests/visual/percy-storybook/Filters.percy.tsx b/tests/visual/percy-storybook/Filters.percy.tsx index 43a6cca14..c08197cf9 100644 --- a/tests/visual/percy-storybook/Filters.percy.tsx +++ b/tests/visual/percy-storybook/Filters.percy.tsx @@ -2,7 +2,7 @@ import * as R from 'ramda'; import React from 'react'; import { storiesOf } from '@storybook/react'; import random from 'core/math/random'; -import DataTable from 'dash-table/DataTable'; +import DataTable from 'dash-table/dash/DataTable'; const setProps = () => { }; diff --git a/tests/visual/percy-storybook/Style.percy.tsx b/tests/visual/percy-storybook/Style.percy.tsx index 67fd6d4a2..b79e9c7cf 100644 --- a/tests/visual/percy-storybook/Style.percy.tsx +++ b/tests/visual/percy-storybook/Style.percy.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; -import DataTable from 'dash-table/DataTable'; +import DataTable from 'dash-table/dash/DataTable'; import fixtures from './fixtures'; import { ColumnType } from 'dash-table/components/Table/props'; diff --git a/tests/visual/percy-storybook/TriadValidation.percy.tsx b/tests/visual/percy-storybook/TriadValidation.percy.tsx index 871ef90b0..4fbfb3e07 100644 --- a/tests/visual/percy-storybook/TriadValidation.percy.tsx +++ b/tests/visual/percy-storybook/TriadValidation.percy.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; -import DataTable from 'dash-table/DataTable'; +import DataTable from 'dash-table/dash/DataTable'; const filteringValues = ['fe', 'be']; const sortingValues = ['fe', 'be']; diff --git a/tests/visual/percy-storybook/Width.all.percy.tsx b/tests/visual/percy-storybook/Width.all.percy.tsx index bf48be2f1..bc90d6c6b 100644 --- a/tests/visual/percy-storybook/Width.all.percy.tsx +++ b/tests/visual/percy-storybook/Width.all.percy.tsx @@ -2,7 +2,7 @@ import * as R from 'ramda'; import React from 'react'; import { storiesOf } from '@storybook/react'; import random from 'core/math/random'; -import DataTable from 'dash-table/DataTable'; +import DataTable from 'dash-table/dash/DataTable'; const setProps = () => { }; diff --git a/tests/visual/percy-storybook/Width.defaults.percy.tsx b/tests/visual/percy-storybook/Width.defaults.percy.tsx index dc7d1a7f9..fe050ddcd 100644 --- a/tests/visual/percy-storybook/Width.defaults.percy.tsx +++ b/tests/visual/percy-storybook/Width.defaults.percy.tsx @@ -2,7 +2,7 @@ import * as R from 'ramda'; import React from 'react'; import { storiesOf } from '@storybook/react'; import random from 'core/math/random'; -import DataTable from 'dash-table/DataTable'; +import DataTable from 'dash-table/dash/DataTable'; const setProps = () => { }; diff --git a/tests/visual/percy-storybook/Width.max.percy.tsx b/tests/visual/percy-storybook/Width.max.percy.tsx index ff850ba20..de4157f70 100644 --- a/tests/visual/percy-storybook/Width.max.percy.tsx +++ b/tests/visual/percy-storybook/Width.max.percy.tsx @@ -2,7 +2,7 @@ import * as R from 'ramda'; import React from 'react'; import { storiesOf } from '@storybook/react'; import random from 'core/math/random'; -import DataTable from 'dash-table/DataTable'; +import DataTable from 'dash-table/dash/DataTable'; const setProps = () => { }; diff --git a/tests/visual/percy-storybook/Width.min.percy.tsx b/tests/visual/percy-storybook/Width.min.percy.tsx index 29d91ddaa..ad9525100 100644 --- a/tests/visual/percy-storybook/Width.min.percy.tsx +++ b/tests/visual/percy-storybook/Width.min.percy.tsx @@ -2,7 +2,7 @@ import * as R from 'ramda'; import React from 'react'; import { storiesOf } from '@storybook/react'; import random from 'core/math/random'; -import DataTable from 'dash-table/DataTable'; +import DataTable from 'dash-table/dash/DataTable'; const setProps = () => { }; diff --git a/tests/visual/percy-storybook/Width.percentages.percy.tsx b/tests/visual/percy-storybook/Width.percentages.percy.tsx index a8bd0f85e..0bdea6f0f 100644 --- a/tests/visual/percy-storybook/Width.percentages.percy.tsx +++ b/tests/visual/percy-storybook/Width.percentages.percy.tsx @@ -2,7 +2,7 @@ import * as R from 'ramda'; import React from 'react'; import { storiesOf } from '@storybook/react'; import random from 'core/math/random'; -import DataTable from 'dash-table/DataTable'; +import DataTable from 'dash-table/dash/DataTable'; const setProps = () => { }; const columns = ['a', 'b', 'c']; diff --git a/tests/visual/percy-storybook/Width.width.percy.tsx b/tests/visual/percy-storybook/Width.width.percy.tsx index ad418fcb1..2957f66cd 100644 --- a/tests/visual/percy-storybook/Width.width.percy.tsx +++ b/tests/visual/percy-storybook/Width.width.percy.tsx @@ -2,7 +2,7 @@ import * as R from 'ramda'; import React from 'react'; import { storiesOf } from '@storybook/react'; import random from 'core/math/random'; -import DataTable from 'dash-table/DataTable'; +import DataTable from 'dash-table/dash/DataTable'; const setProps = () => { }; From 29b285838bf5e9d23fe73ce22ef76fd2320867a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andre=CC=81=20Rivet?= Date: Thu, 28 Feb 2019 10:00:52 -0500 Subject: [PATCH 55/55] expected! --- .vscode/settings.json | 3 +++ dash_table/Format.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 67a1ebaf6..ec772eb96 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,9 @@ ".vscode", "typings" ], + "cSpell.ignoreRegExpList": [ + "'" + ], "cSpell.language": "en", "cSpell.diagnosticLevel": "Error", "cSpell.languageSettings": [ diff --git a/dash_table/Format.py b/dash_table/Format.py index 52234b185..011d50728 100644 --- a/dash_table/Format.py +++ b/dash_table/Format.py @@ -114,7 +114,7 @@ def _validate_char(self, value): self._validate_string(value) if len(value) != 1: - raise ValueError('excepted value to a string of length one') + raise ValueError('expected value to a string of length one') def _validate_non_negative_integer_or_none(self, value): if value is None: