node,
}}
style={{position: 'relative'}}
- value={value}
- marks={truncatedMarks}
+ value={value ? value : calcValue(min, max, value)}
+ marks={sanitizeMarks({min, max, marks, step})}
+ step={calcStep(min, max, step)}
{...omit(
[
'className',
@@ -112,6 +114,7 @@ export default class RangeSlider extends Component {
'marks',
'updatemode',
'verticalHeight',
+ 'step',
],
this.props
)}
diff --git a/components/dash-core-components/src/fragments/Slider.react.js b/components/dash-core-components/src/fragments/Slider.react.js
index 178014e395..8bdf569230 100644
--- a/components/dash-core-components/src/fragments/Slider.react.js
+++ b/components/dash-core-components/src/fragments/Slider.react.js
@@ -1,10 +1,11 @@
import React, {Component} from 'react';
import ReactSlider, {createSliderWithTooltip} from 'rc-slider';
-import {assoc, omit, pickBy} from 'ramda';
+import {assoc, omit} from 'ramda';
import computeSliderStyle from '../utils/computeSliderStyle';
import 'rc-slider/assets/index.css';
+import {sanitizeMarks, calcStep} from '../utils/computeSliderMarkers';
import {propTypes, defaultProps} from '../components/Slider.react';
/**
@@ -47,6 +48,10 @@ export default class Slider extends Component {
setProps,
tooltip,
updatemode,
+ min,
+ max,
+ marks,
+ step,
vertical,
verticalHeight,
} = this.props;
@@ -65,13 +70,6 @@ export default class Slider extends Component {
tipProps = tooltip;
}
- const truncatedMarks = this.props.marks
- ? pickBy(
- (k, mark) => mark >= this.props.min && mark <= this.props.max,
- this.props.marks
- )
- : this.props.marks;
-
return (
+ pickBy((k, mark) => mark >= min && mark <= max, marks);
+
+const truncateNumber = num =>
+ parseInt(num.toString().match(/^-?\d+(?:\.\d{0,0})?/)[0], 10);
+
+const decimalCount = d =>
+ String(d).split('.').length > 1 ? String(d).split('.')[1].length : 0;
+const alignIntValue = (v, d) =>
+ d < 10
+ ? v
+ : parseInt((truncateNumber(v / d) * d).toFixed(decimalCount(d)), 10);
+const alignDecimalValue = (v, d) =>
+ d < 10
+ ? parseFloat(v.toFixed(decimalCount(d)))
+ : parseFloat(((v / d).toFixed(0) * d).toFixed(decimalCount(d)));
+
+const alignValue = (v, d) =>
+ decimalCount(d) < 1 ? alignIntValue(v, d) : alignDecimalValue(v, d);
+
+const log = v => Math.floor(Math.log10(v));
+
+const getNearByStep = v =>
+ v < 10
+ ? [v]
+ : [
+ Math.pow(10, Math.floor(Math.log10(v))),
+ Math.pow(10, Math.ceil(Math.log10(v))) / 2,
+ alignValue(v, Math.pow(10, log(v))),
+ Math.pow(10, Math.ceil(Math.log10(v))),
+ ].sort((a, b) => Math.abs(a - v) - Math.abs(b - v));
+
+const estimateBestSteps = (minValue, maxValue, stepValue) => {
+ const desiredCountMin = 2 + (maxValue / stepValue <= 10 ? 3 : 3); // including start, end
+ const desiredCountMax = 2 + (maxValue / stepValue <= 10 ? 4 : 6);
+
+ const min = minValue / stepValue;
+ const max = maxValue / stepValue;
+
+ const rangeLength = max - min;
+
+ const leastMarksInterval = Math.max(
+ Math.round(rangeLength / (desiredCountMin - 1)),
+ 1
+ );
+ const possibleValues = getNearByStep(leastMarksInterval);
+
+ const finalStep =
+ possibleValues.find(step => {
+ const expectedSteps = Math.ceil(rangeLength / step) + 1;
+ return (
+ expectedSteps >= desiredCountMin - 1 &&
+ expectedSteps <= desiredCountMax + 1
+ );
+ }) || possibleValues[0];
+ return [
+ alignValue(min, finalStep) * stepValue,
+ alignValue(finalStep * stepValue, stepValue),
+ stepValue,
+ ];
+};
+
+/**
+ * Calculate default step if not defined
+ */
+export const calcStep = (min, max, step) => {
+ if (step) {
+ return step;
+ }
+
+ const diff = max > min ? max - min : min - max;
+
+ const v = (Math.abs(diff) + Number.EPSILON) / 100;
+ const N = Math.floor(Math.log10(v));
+ return [
+ Number(Math.pow(10, N)),
+ 2 * Math.pow(10, N),
+ 5 * Math.pow(10, N),
+ ].sort((a, b) => Math.abs(a - v) - Math.abs(b - v))[0];
+};
+
+export const applyD3Format = (mark, min, max) => {
+ const mu_ten_factor = -3;
+ const k_ten_factor = 3;
+
+ const ten_factor = Math.log10(Math.abs(mark));
+ if (ten_factor > mu_ten_factor && ten_factor < k_ten_factor) {
+ return String(mark);
+ }
+ const max_min_mean = (Math.abs(max) + Math.abs(min)) / 2;
+ const si_formatter = formatPrefix(',.0', max_min_mean);
+ return String(si_formatter(mark));
+};
+
+export const autoGenerateMarks = (min, max, step) => {
+ const marks = [];
+ const [start, interval, chosenStep] = step
+ ? [min, step, step]
+ : estimateBestSteps(min, max, calcStep(min, max, step));
+ let cursor = start + interval;
+
+ // make sure we don't step into infinite loop
+ if ((max - cursor) / interval > 0) {
+ do {
+ marks.push(alignValue(cursor, chosenStep));
+ cursor += interval;
+ } while (cursor < max);
+
+ // do some cosmetic
+ const discardThreshold = 1.5;
+ if (
+ marks.length >= 2 &&
+ max - marks[marks.length - 2] <= interval * discardThreshold
+ ) {
+ marks.pop();
+ }
+ }
+
+ const marksObject = {};
+ marks.forEach(mark => {
+ marksObject[mark] = applyD3Format(mark, min, max);
+ });
+ marksObject[min] = applyD3Format(min, min, max);
+ marksObject[max] = applyD3Format(max, min, max);
+ return marksObject;
+};
+
+/**
+ * - Auto generate marks if not given,
+ * - Not generate anything at all when explicit null is given to marks
+ * - Then truncate marks so no out of range marks
+ */
+export const sanitizeMarks = ({min, max, marks, step}) => {
+ if (marks === null) {
+ return undefined;
+ }
+
+ const truncated_marks =
+ marks && isEmpty(marks) === false
+ ? truncateMarks(min, max, marks)
+ : marks;
+
+ if (truncated_marks && isEmpty(truncated_marks) === false) {
+ return truncated_marks;
+ }
+ return autoGenerateMarks(min, max, step);
+};
+
+/**
+ * Calculate default value if not defined
+ */
+export const calcValue = (min, max, value) => {
+ if (value !== undefined) {
+ return value;
+ }
+
+ return [min, max];
+};
diff --git a/components/dash-core-components/src/utils/optionTypes.js b/components/dash-core-components/src/utils/optionTypes.js
new file mode 100644
index 0000000000..4af9208adc
--- /dev/null
+++ b/components/dash-core-components/src/utils/optionTypes.js
@@ -0,0 +1,25 @@
+import {type} from 'ramda';
+
+export const sanitizeOptions = options => {
+ if (type(options) === 'Object') {
+ return Object.entries(options).map(([value, label]) => ({
+ label: String(label),
+ value,
+ }));
+ }
+
+ if (type(options) === 'Array') {
+ if (
+ options.length > 0 &&
+ ['String', 'Number', 'Bool'].includes(type(options[0]))
+ ) {
+ return options.map(option => ({
+ label: String(option),
+ value: option,
+ }));
+ }
+ return options;
+ }
+
+ return options;
+};
diff --git a/components/dash-core-components/tests/integration/dropdown/test_dropdown_radioitems_checklist_shorthands.py b/components/dash-core-components/tests/integration/dropdown/test_dropdown_radioitems_checklist_shorthands.py
new file mode 100644
index 0000000000..12257072fa
--- /dev/null
+++ b/components/dash-core-components/tests/integration/dropdown/test_dropdown_radioitems_checklist_shorthands.py
@@ -0,0 +1,82 @@
+from dash import Dash, dcc, html
+
+
+def test_ddsh001_test_dropdown_radioitems_checklist_shorthands(dash_dcc):
+ app = Dash(__name__)
+
+ TEST_OPTIONS_N_VALUES = [
+ [["a", "b", "c"]],
+ [["a", "b", "c"], "b"],
+ [["a", 3, "c"]],
+ [["a", 3, "c"], 3],
+ [["a", True, "c"]],
+ [["a", True, "c"], True],
+ [["a", 3, "c", True, False]],
+ [["a", 3, "c", True, False], False],
+ # {`value1`: `label1`, `value2`, `label2`, ...}
+ [{"one": "One", "two": "Two", "three": "Three"}],
+ [{"one": "One", "two": "Two", "three": "Three"}, "two"],
+ [{"one": 1, "two": 2, "three": False}],
+ [{"one": 1, "two": 2, "three": False}, "three"],
+ [{"one": 1, "two": True, "three": 3}],
+ [{"one": 1, "two": True, "three": 3}, "two"],
+ # original options format
+ [
+ [
+ {"label": "one", "value": 1},
+ {"label": "two", "value": True},
+ {"label": "three", "value": 3},
+ ]
+ ],
+ [
+ [
+ {"label": "one", "value": 1},
+ {"label": "two", "value": True},
+ {"label": "three", "value": 3},
+ ],
+ True,
+ ],
+ ]
+
+ layout = []
+ for definition in TEST_OPTIONS_N_VALUES:
+ (option, value) = definition if len(definition) > 1 else [definition[0], None]
+ layout.extend(
+ [
+ html.Div(
+ [
+ html.Div(
+ f"Options={option}, Value={value}",
+ style={"marginBottom": 15, "marginTop": 25},
+ ),
+ dcc.Dropdown(option, value),
+ ]
+ ),
+ html.Div(
+ [
+ html.Div(
+ f"Options={option}, Value={value}",
+ style={"marginBottom": 15, "marginTop": 25},
+ ),
+ dcc.RadioItems(option, value=value),
+ ]
+ ),
+ html.Div(
+ [
+ html.Div(
+ f"Options={option}, Value={value}",
+ style={"marginBottom": 15, "marginTop": 25},
+ ),
+ dcc.Checklist(option, value=[value]),
+ ]
+ ),
+ ]
+ )
+
+ app.layout = html.Div(layout)
+
+ dash_dcc.start_server(app)
+ dash_dcc.wait_for_element(".dash-dropdown")
+ dash_dcc.percy_snapshot(
+ "ddsh001 - test_ddsh001_test_dropdown_radioitems_checklist_shorthands"
+ )
diff --git a/components/dash-core-components/tests/integration/dropdown/test_dynamic_options.py b/components/dash-core-components/tests/integration/dropdown/test_dynamic_options.py
index fd4d00560e..85d9985579 100644
--- a/components/dash-core-components/tests/integration/dropdown/test_dynamic_options.py
+++ b/components/dash-core-components/tests/integration/dropdown/test_dynamic_options.py
@@ -1,4 +1,4 @@
-from dash import Dash, Input, Output, dcc
+from dash import Dash, Input, Output, dcc, html
from dash.exceptions import PreventUpdate
@@ -51,3 +51,26 @@ def update_options(search_value):
assert options[0].text == "Montreal"
assert dash_dcc.get_logs() == []
+
+
+def test_dddo002_array_value(dash_dcc):
+ dropdown_options = [
+ {"label": "New York City", "value": "New,York,City"},
+ {"label": "Montreal", "value": "Montreal"},
+ {"label": "San Francisco", "value": "San,Francisco"},
+ ]
+
+ app = Dash(__name__)
+ arrayValue = ["San", "Francisco"]
+
+ dropdown = dcc.Dropdown(
+ options=dropdown_options,
+ value=arrayValue,
+ )
+ app.layout = html.Div([dropdown])
+
+ dash_dcc.start_server(app)
+
+ dash_dcc.wait_for_text_to_equal("#react-select-2--value-item", "San Francisco")
+
+ assert dash_dcc.get_logs() == []
diff --git a/components/dash-core-components/tests/integration/misc/test_bcdp_auto_id.py b/components/dash-core-components/tests/integration/misc/test_bcdp_auto_id.py
new file mode 100644
index 0000000000..6fe4e441eb
--- /dev/null
+++ b/components/dash-core-components/tests/integration/misc/test_bcdp_auto_id.py
@@ -0,0 +1,46 @@
+from dash import Dash, Input, Output, dcc, html
+
+
+def test_msps002_auto_id_assert(dash_dcc):
+ app = Dash(__name__)
+
+ input1 = dcc.Input(value="Hello")
+ input2 = dcc.Input(value="Hello")
+ input3 = dcc.Input(value=3)
+ output1 = html.Div()
+ output2 = html.Div()
+ output3 = html.Div(id="output-3")
+ slider = dcc.Slider(0, 10, value=9)
+
+ app.layout = html.Div([input1, input2, output1, output2, output3, input3, slider])
+
+ @app.callback(Output(output1, "children"), Input(input1, "value"))
+ def update(v):
+ return f"Output1: Input1={v}"
+
+ @app.callback(Output(output2, "children"), Input(input2, "value"))
+ def update(v):
+ return f"Output2: Input2={v}"
+
+ @app.callback(
+ Output("output-3", "children"), Input(input1, "value"), Input(input2, "value")
+ )
+ def update(v1, v2):
+ return f"Output3: Input1={v1}, Input2={v2}"
+
+ @app.callback(Output(slider, "value"), Input(input3, "value"))
+ def update(v):
+ return v
+
+ # Verify the auto-generated IDs are stable
+ assert output1.id == "e3e70682-c209-4cac-629f-6fbed82c07cd"
+ assert input1.id == "82e2e662-f728-b4fa-4248-5e3a0a5d2f34"
+ assert output2.id == "d4713d60-c8a7-0639-eb11-67b367a9c378"
+ assert input2.id == "23a7711a-8133-2876-37eb-dcd9e87a1613"
+ # we make sure that the if the id is set explicitly, then it is not replaced by random id
+ assert output3.id == "output-3"
+
+ dash_dcc.start_server(app)
+
+ dash_dcc.wait_for_element(".rc-slider")
+ dash_dcc.percy_snapshot("component_auto_id - test_msps002_auto_id_assert", True)
diff --git a/components/dash-core-components/tests/integration/misc/test_persistence.py b/components/dash-core-components/tests/integration/misc/test_persistence.py
index 8435a6a6b8..1559d57815 100644
--- a/components/dash-core-components/tests/integration/misc/test_persistence.py
+++ b/components/dash-core-components/tests/integration/misc/test_persistence.py
@@ -65,9 +65,9 @@ def test_msps001_basic_persistence(dash_dcc):
persistence=True,
),
dcc.RangeSlider(
- id="rangeslider", min=0, max=10, value=[3, 7], persistence=True
+ id="rangeslider", min=0, max=10, step=1, value=[3, 7], persistence=True
),
- dcc.Slider(id="slider", min=20, max=30, value=25, persistence=True),
+ dcc.Slider(id="slider", min=20, max=30, step=1, value=25, persistence=True),
dcc.Tabs(
id="tabs",
children=[
diff --git a/components/dash-core-components/tests/integration/sliders/test_sliders_shorthands.py b/components/dash-core-components/tests/integration/sliders/test_sliders_shorthands.py
new file mode 100644
index 0000000000..79bad9df0e
--- /dev/null
+++ b/components/dash-core-components/tests/integration/sliders/test_sliders_shorthands.py
@@ -0,0 +1,180 @@
+from dash import Dash, dcc, html
+import numpy as np
+import math
+
+
+def test_slsh001_rangeslider_shorthand_props(dash_dcc):
+ NUMBERS = [10 * N for N in np.arange(1, 2, 0.5)]
+ # TEST_RANGES = []
+ LAYOUT = []
+ TEST_CASES = []
+
+ for n in NUMBERS:
+ TEST_CASES.extend(
+ [
+ [n, n * 1.5, abs(n * 1.5 - n) / 5],
+ [-n, 0, n / 10],
+ [-n, n, n / 10],
+ [-1.5 * n, -1 * n, n / 7],
+ ]
+ )
+
+ for t in TEST_CASES:
+ min, max, steps = t
+ marks = {
+ i: "Label {}".format(i) if i == 1 else str(i)
+ for i in range(math.ceil(min), math.floor(max))
+ }
+
+ LAYOUT.extend(
+ [
+ html.Div(
+ [
+ html.Div(
+ f"{min} - {max}",
+ style={"marginBottom": 15, "marginTop": 25},
+ ),
+ dcc.Slider(min, max),
+ ]
+ ),
+ html.Div(
+ [
+ html.Div(
+ f"{min} - {max}",
+ style={"marginBottom": 15, "marginTop": 25},
+ ),
+ dcc.RangeSlider(min, max),
+ ]
+ ),
+ html.Div(
+ [
+ html.Div(
+ f"{min} - {max}, {steps}",
+ style={"marginBottom": 15, "marginTop": 25},
+ ),
+ dcc.Slider(min, max, steps),
+ ]
+ ),
+ html.Div(
+ [
+ html.Div(
+ f"{min} - {max}, {steps}",
+ style={"marginBottom": 15, "marginTop": 25},
+ ),
+ dcc.RangeSlider(min, max, steps),
+ ]
+ ),
+ html.Div(
+ [
+ html.Div(
+ f"{min} - {max}, {steps}, value={min + steps}",
+ style={"marginBottom": 15, "marginTop": 25},
+ ),
+ dcc.Slider(min, max, steps, value=min + steps),
+ ]
+ ),
+ html.Div(
+ [
+ html.Div(
+ f"{min} - {max}, {steps}, value=[{min + steps},{min + steps * 3}]",
+ style={"marginBottom": 15, "marginTop": 25},
+ ),
+ dcc.RangeSlider(
+ min, max, steps, value=[min + steps, min + steps * 3]
+ ),
+ ]
+ ),
+ html.Div(
+ [
+ html.Div(
+ f"{min} - {max}, {steps}, value={min + steps}, marks={marks}",
+ style={"marginBottom": 15, "marginTop": 25},
+ ),
+ dcc.Slider(
+ min,
+ max,
+ steps,
+ value=min + steps,
+ marks=marks,
+ ),
+ ]
+ ),
+ html.Div(
+ [
+ html.Div(
+ f"{min} - {max}, {steps},value=[{min + steps},{min + steps * 3}], marks={marks}",
+ style={"marginBottom": 15, "marginTop": 25},
+ ),
+ dcc.RangeSlider(
+ min,
+ max,
+ steps,
+ value=[min + steps, min + steps * 3],
+ marks=marks,
+ ),
+ ]
+ ),
+ html.Div(
+ [
+ html.Div(
+ f"{min} - {max}, {steps},value=[{min + steps},{min + steps * 3}], marks=None",
+ style={"marginBottom": 15, "marginTop": 25},
+ ),
+ dcc.RangeSlider(
+ min,
+ max,
+ steps,
+ value=[min + steps, min + steps * 3],
+ marks=None,
+ ),
+ ]
+ ),
+ ]
+ )
+
+ app = Dash(__name__)
+ app.layout = html.Div(LAYOUT)
+
+ dash_dcc.start_server(app)
+ dash_dcc.wait_for_element(".rc-slider")
+ dash_dcc.percy_snapshot("slsh001 - test_slsh001_rangeslider_shorthand_props", True)
+
+
+def test_slsh002_sliders_marks_si_unit_format(dash_dcc):
+
+ LAYOUT = []
+
+ # Showing SI Units
+ LAYOUT.extend(
+ [
+ html.Div(
+ "Testing SI units",
+ style={"marginBottom": 10, "marginTop": 30},
+ ),
+ ]
+ )
+
+ for n in range(-20, 20):
+ min = 0
+ max = pow(10, n)
+ LAYOUT.extend(
+ [
+ html.Div(
+ [
+ html.Div(
+ f"min={min}, max={max}(=10^{n})",
+ style={"marginBottom": 15, "marginTop": 25},
+ ),
+ dcc.Slider(min, max),
+ dcc.RangeSlider(min, max),
+ ]
+ )
+ ]
+ )
+
+ app = Dash(__name__)
+ app.layout = html.Div(LAYOUT)
+
+ dash_dcc.start_server(app)
+ dash_dcc.wait_for_element(".rc-slider")
+ dash_dcc.percy_snapshot("slsh002 - test_slsh002_sliders_marks_si_unit_format", True)
diff --git a/components/dash-table/src/dash-table/components/Table/props.ts b/components/dash-table/src/dash-table/components/Table/props.ts
index 71aef18941..93e48b6821 100644
--- a/components/dash-table/src/dash-table/components/Table/props.ts
+++ b/components/dash-table/src/dash-table/components/Table/props.ts
@@ -97,6 +97,26 @@ export interface ICellCoordinates {
column_id: ColumnId;
}
+export class Column implements IBaseColumn {
+ clearable?: boolean | boolean[] | 'first' | 'last' | undefined;
+ deletable?: boolean | boolean[] | 'first' | 'last' | undefined;
+ editable = false;
+ filter_options!: IFilterOptions;
+ hideable?: boolean | boolean[] | 'first' | 'last' | undefined;
+ renamable?: boolean | boolean[] | 'first' | 'last' | undefined;
+ selectable?: boolean | boolean[] | 'first' | 'last' | undefined;
+ sort_as_null: SortAsNull = [];
+ id!: string;
+ name: string | string[] = [];
+
+ constructor(initialValues: any) {
+ if (Object.keys(initialValues).includes('name'))
+ this.name = initialValues.name;
+ if (Object.keys(initialValues).includes('id'))
+ this.id = initialValues.id;
+ }
+}
+
export type ColumnId = string;
export type Columns = IColumn[];
export type Data = Datum[];
diff --git a/components/dash-table/src/dash-table/dash/DataTable.js b/components/dash-table/src/dash-table/dash/DataTable.js
index 0132fb62bc..61cc68656e 100644
--- a/components/dash-table/src/dash-table/dash/DataTable.js
+++ b/components/dash-table/src/dash-table/dash/DataTable.js
@@ -110,15 +110,26 @@ export const defaultProps = {
export const propTypes = {
/**
- * The row and column indices and IDs of the currently active cell.
- * `row_id` is only returned if the data rows have an `id` key.
+ * The contents of the table.
+ * The keys of each item in data should match the column IDs.
+ * Each item can also have an 'id' key, whose value is its row ID. If there
+ * is a column with ID='id' this will display the row ID, otherwise it is
+ * just used to reference the row for selections, filtering, etc.
+ * Example:
+ * [
+ * {'column-1': 4.5, 'column-2': 'montreal', 'column-3': 'canada'},
+ * {'column-1': 8, 'column-2': 'boston', 'column-3': 'america'}
+ * ]
*/
- active_cell: PropTypes.exact({
- row: PropTypes.number,
- column: PropTypes.number,
- row_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- column_id: PropTypes.string
- }),
+ data: PropTypes.arrayOf(
+ PropTypes.objectOf(
+ PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ PropTypes.bool
+ ])
+ )
+ ),
/**
* Columns describes various aspects about each individual column.
@@ -427,6 +438,17 @@ export const propTypes = {
})
),
+ /**
+ * The row and column indices and IDs of the currently active cell.
+ * `row_id` is only returned if the data rows have an `id` key.
+ */
+ active_cell: PropTypes.exact({
+ row: PropTypes.number,
+ column: PropTypes.number,
+ row_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ column_id: PropTypes.string
+ }),
+
/**
* If true, headers are included when copying from the table to different
* tabs and elsewhere. Note that headers are ignored when copying from the table onto itself and
@@ -510,20 +532,6 @@ export const propTypes = {
})
),
- /**
- * The contents of the table.
- * The keys of each item in data should match the column IDs.
- * Each item can also have an 'id' key, whose value is its row ID. If there
- * is a column with ID='id' this will display the row ID, otherwise it is
- * just used to reference the row for selections, filtering, etc.
- * Example:
- * [
- * {'column-1': 4.5, 'column-2': 'montreal', 'column-3': 'canada'},
- * {'column-1': 8, 'column-2': 'boston', 'column-3': 'america'}
- * ]
- */
- data: PropTypes.arrayOf(PropTypes.object),
-
/**
* The previous state of `data`. `data_previous`
* has the same structure as `data` and it will be updated
diff --git a/components/dash-table/src/dash-table/dash/Sanitizer.ts b/components/dash-table/src/dash-table/dash/Sanitizer.ts
index c3d4e8f1a2..7198e4bced 100644
--- a/components/dash-table/src/dash-table/dash/Sanitizer.ts
+++ b/components/dash-table/src/dash-table/dash/Sanitizer.ts
@@ -2,7 +2,6 @@ import * as R from 'ramda';
import {memoizeOne} from 'core/memoizer';
import {
- Columns,
ColumnType,
Fixed,
IColumn,
@@ -18,11 +17,13 @@ import {
FilterLogicalOperator,
SelectedCells,
FilterCase,
- IFilterOptions
+ IFilterOptions,
+ Data
} from 'dash-table/components/Table/props';
import headerRows from 'dash-table/derived/header/headerRows';
import resolveFlag from 'dash-table/derived/cell/resolveFlag';
import dataLoading from 'dash-table/derived/table/data_loading';
+import {Column, Columns} from '../components/Table/props';
const D3_DEFAULT_LOCALE: INumberLocale = {
symbol: ['$', ''],
@@ -65,6 +66,11 @@ const getFixedRows = (
(filter_action !== TableAction.None ? 1 : 0) +
data2number(fixed.data);
+const populateColumnsFromData = (data: Data) =>
+ data.length > 0
+ ? Object.keys(data[0]).map(key => new Column({name: key, id: key}))
+ : [];
+
const applyDefaultsToColumns = (
defaultLocale: INumberLocale,
defaultSort: SortAsNull,
@@ -112,6 +118,7 @@ const getVisibleColumns = (
export default class Sanitizer {
sanitize(props: PropsWithDefaults): SanitizedProps {
const locale_format = this.applyDefaultToLocale(props.locale_format);
+ const data = props.data ?? [];
const columns = props.columns
? this.applyDefaultsToColumns(
locale_format,
@@ -120,8 +127,7 @@ export default class Sanitizer {
props.editable,
props.filter_options
)
- : [];
- const data = props.data ?? [];
+ : this.populateColumnsFrom(data);
const visibleColumns = this.getVisibleColumns(
columns,
props.hidden_columns
@@ -171,6 +177,8 @@ export default class Sanitizer {
});
}
+ private readonly populateColumnsFrom = memoizeOne(populateColumnsFromData);
+
private readonly applyDefaultToLocale = memoizeOne(applyDefaultToLocale);
private readonly applyDefaultsToColumns = memoizeOne(
diff --git a/dash/dependencies.py b/dash/dependencies.py
index 1b2cc6f7f9..12cd4a59be 100644
--- a/dash/dependencies.py
+++ b/dash/dependencies.py
@@ -1,4 +1,5 @@
import json
+from dash.development.base_component import Component
from ._validate import validate_callback
from ._grouping import flatten_grouping, make_grouping_by_index
@@ -27,7 +28,12 @@ def to_json(self):
class DashDependency: # pylint: disable=too-few-public-methods
def __init__(self, component_id, component_property):
- self.component_id = component_id
+
+ if isinstance(component_id, Component):
+ self.component_id = component_id.set_random_id()
+ else:
+ self.component_id = component_id
+
self.component_property = component_property
def __str__(self):
diff --git a/dash/development/base_component.py b/dash/development/base_component.py
index 7b63dccc5f..4cde39805f 100644
--- a/dash/development/base_component.py
+++ b/dash/development/base_component.py
@@ -1,11 +1,15 @@
import abc
import inspect
import sys
+import uuid
+import random
from .._utils import patch_collections_abc, stringify_id
MutableSequence = patch_collections_abc("MutableSequence")
+rd = random.Random(0)
+
# pylint: disable=no-init,too-few-public-methods
class ComponentRegistry:
@@ -163,6 +167,12 @@ def __init__(self, **kwargs):
setattr(self, k, v)
+ def set_random_id(self):
+ if not hasattr(self, "id"):
+ v = str(uuid.UUID(int=rd.randint(0, 2 ** 128)))
+ setattr(self, "id", v)
+ return getattr(self, "id")
+
def to_plotly_json(self):
# Add normal properties
props = {
diff --git a/dash/development/component_generator.py b/dash/development/component_generator.py
index 3d8a8632a8..79b95315aa 100644
--- a/dash/development/component_generator.py
+++ b/dash/development/component_generator.py
@@ -25,6 +25,7 @@
"UNDEFINED",
"REQUIRED",
"to_plotly_json",
+ "set_random_id",
"available_properties",
"available_wildcard_properties",
"_.*",
diff --git a/tests/integration/devtools/test_props_check.py b/tests/integration/devtools/test_props_check.py
index 603909158e..23211ed3dd 100644
--- a/tests/integration/devtools/test_props_check.py
+++ b/tests/integration/devtools/test_props_check.py
@@ -15,12 +15,6 @@
"component": dcc.Checklist,
"props": {"options": [{"label": "hello"}], "value": ["test"]},
},
- "invalid-nested-prop": {
- "fail": True,
- "name": "invalid nested prop",
- "component": dcc.Checklist,
- "props": {"options": [{"label": "hello", "value": True}], "value": ["test"]},
- },
"invalid-arrayOf": {
"fail": True,
"name": "invalid arrayOf",
@@ -113,6 +107,12 @@
"columns": [{"id": "id", "name": "name", "format": {"prefix": "asdf"}}]
},
},
+ "allow-nested-prop": {
+ "fail": False,
+ "name": "allow nested prop",
+ "component": dcc.Checklist,
+ "props": {"options": [{"label": "hello", "value": True}], "value": ["test"]},
+ },
"allow-null": {
"fail": False,
"name": "nested null",
diff --git a/tests/unit/development/test_base_component.py b/tests/unit/development/test_base_component.py
index 7b1ed62f9a..4a3a8fd446 100644
--- a/tests/unit/development/test_base_component.py
+++ b/tests/unit/development/test_base_component.py
@@ -3,9 +3,10 @@
import plotly
import pytest
-from dash import __version__
+from dash import __version__, Dash
from dash import html
from dash.development.base_component import Component
+from dash import dcc, Input, Output
Component._prop_names = ("id", "a", "children", "style")
Component._type = "TestComponent"
@@ -473,3 +474,37 @@ def test_debc027_component_error_message():
+ "keyword argument: `asdf`\n"
+ "Allowed arguments: {}".format(", ".join(sorted(html.Div()._prop_names)))
)
+
+
+def test_set_random_id():
+ app = Dash(__name__)
+
+ input1 = dcc.Input(value="Hello")
+ input2 = dcc.Input(value="Hello")
+ output1 = html.Div()
+ output2 = html.Div()
+ output3 = html.Div(id="output-3")
+
+ app.layout = html.Div([input1, input2, output1, output2, output3])
+
+ @app.callback(Output(output1, "children"), Input(input1, "value"))
+ def update(v):
+ return f"Input 1 {v}"
+
+ @app.callback(Output(output2, "children"), Input(input2, "value"))
+ def update(v):
+ return f"Input 2 {v}"
+
+ @app.callback(
+ Output("output-3", "children"), Input(input1, "value"), Input(input2, "value")
+ )
+ def update(v1, v2):
+ return f"Output 3 - Input 1: {v1}, Input 2: {v2}"
+
+ # Verify the auto-generated IDs are stable
+ assert output1.id == "e3e70682-c209-4cac-629f-6fbed82c07cd"
+ assert input1.id == "82e2e662-f728-b4fa-4248-5e3a0a5d2f34"
+ assert output2.id == "d4713d60-c8a7-0639-eb11-67b367a9c378"
+ assert input2.id == "23a7711a-8133-2876-37eb-dcd9e87a1613"
+ # we make sure that the if the id is set explicitly, then it is not replaced by random id
+ assert output3.id == "output-3"