diff --git a/tests/dash/app_dataframe_updating_graph_fe.py b/tests/dash/app_dataframe_updating_graph_fe.py new file mode 100644 index 000000000..5b06043a2 --- /dev/null +++ b/tests/dash/app_dataframe_updating_graph_fe.py @@ -0,0 +1,137 @@ +from dash.dependencies import Input, Output +import dash_core_components as dcc +import dash_html_components as html +import pandas as pd +from textwrap import dedent + +import dash_table +from index import app + +ID_PREFIX = "app_dataframe_updating_graph" +IDS = {"table": ID_PREFIX, "container": "{}-container".format(ID_PREFIX)} +df = pd.read_csv("./datasets/gapminder.csv") +df = df[df["year"] == 2007] + + +def layout(): + return html.Div( + [ + html.Div( + dash_table.Table( + id=IDS["table"], + columns=[ + {"name": i, "id": i, "deletable": True} for i in df.columns + ], + dataframe=df.to_dict("rows"), + editable=True, + filtering=True, + sorting=True, + sorting_type="multi", + row_selectable="multi", + row_deletable=True, + selected_rows=[], + derived_viewport_indices=[], + n_fixed_rows=1, + ), + style={"height": 300, "overflowY": "scroll"}, + ), + html.Div(id=IDS["container"]), + dcc.Markdown( + dedent( + """ + *** + + `Table` includes several features for modifying and transforming the + view of the data. These include: + + - Sorting by column (`sorting=True`) + - Filtering by column (`filtering=True`) + - Editing the cells (`editable=True`) + - Deleting rows (`row_deletable=True`) + - Deleting columns (`columns[i].deletable=True`) + - Selecting rows (`row_selectable='single' | 'multi'`) + + > A quick note on filtering. We have defined our own + > syntax for performing filtering operations. Here are some + > examples for this particular dataset: + > - `lt num(50)` in the `lifeExp` column + > - `eq "Canada"` in the `country` column + + By default, these transformations are done clientside. + Your Dash callbacks can respond to these modifications + by listening to the `dataframe` property as an `Input`. + + Note that if `dataframe` is an `Input` then the entire + `dataframe` will be passed over the network: if your dataframe is + large, then this will become slow. For large dataframes, you have + two options: + - Use `dataframe_indicies` instead + - Perform the sorting or filtering in Python instead + + Issues with this example: + - Row selection callbacks don't work yet: `derived_viewport_indices` + isn't getting updated on row selection and `selected_rows` doesn't + track the underlying data (e.g. it will always be [1, 3] even after sorting or filtering) + """ + ) + ), + ] + ) + + +@app.callback( + Output(IDS["container"], "children"), + [ + Input(IDS["table"], "derived_virtual_dataframe"), + Input(IDS["table"], "selected_rows"), + ], +) +def update_graph(rows, selected_rows): + # When the table is first rendered, `derived_virtual_dataframe` + # will be `None`. This is due to an idiosyncracy in Dash + # (unsupplied properties are always None and Dash calls the dependent + # callbacks when the component is first rendered). + # So, if `selected_rows` is `None`, then the component was just rendered + # and its value will be the same as the component's dataframe. + # Instead of setting `None` in here, you could also set + # `derived_virtual_dataframe=df.to_rows('dict')` when you initialize + # the component. + if rows is None: + dff = df + else: + dff = pd.DataFrame(rows) + + colors = [] + for i in range(len(dff)): + if i in selected_rows: + colors.append("#7FDBFF") + else: + colors.append("#0074D9") + + return html.Div( + [ + dcc.Graph( + id=column, + figure={ + "data": [ + { + "x": dff["country"], + # check if column exists - user may have deleted it + # If `column.deletable=False`, then you don't + # need to do this check. + "y": dff[column] if column in dff else [], + "type": "bar", + "marker": {"color": colors}, + } + ], + "layout": { + "xaxis": {"automargin": True}, + "yaxis": {"automargin": True}, + "height": 250, + "margin": {"t": 10, "l": 10, "r": 10}, + }, + }, + ) + for column in ["pop", "lifeExp", "gdpPercap"] + ] + ) diff --git a/tests/dash/app_sizing.py b/tests/dash/app_sizing.py new file mode 100644 index 000000000..8732b7538 --- /dev/null +++ b/tests/dash/app_sizing.py @@ -0,0 +1,275 @@ +from collections import OrderedDict +from dash.dependencies import Input, Output +import dash_core_components as dcc +import dash_html_components as html +import pandas as pd +from textwrap import dedent + +import dash_table +from index import app +from .utils import section_title, html_table + + +def layout(): + data = OrderedDict( + [ + ( + "Date", + [ + "July 12th, 2013 - July 25th, 2013", + "July 12th, 2013 - August 25th, 2013", + "July 12th, 2014 - August 25th, 2014", + ], + ), + ( + "Election Polling Organization", + ["The New York Times", "Pew Research", "The Washington Post"], + ), + ("Rep", [1, -20, 3.512]), + ("Dem", [10, 20, 30]), + ("Ind", [2, 10924, 3912]), + ( + "Region", + [ + "Northern New York State to the Southern Appalachian Mountains", + "Canada", + "Southern Vermont", + ], + ), + ] + ) + + df = pd.DataFrame(data) + df_long = pd.DataFrame( + OrderedDict([(name, col_data * 10) for (name, col_data) in data.items()]) + ) + + return [ + html.H1("Sizing Guide"), + html.Div( + style={ + "marginLeft": "auto", + "marginRight": "auto", + "width": "80%", + "borderLeft": "thin hotpink solid", + "borderRight": "thin hotpink solid", + }, + children=[ + html.H1("Background - HTML Tables"), + section_title("HTML Table - Default Styles"), + html.Div("By default, HTML tables expand to their contents"), + html_table(df, table_style={}, base_column_style={}), + section_title("HTML Table - Padding"), + html.Div( + """ + Since the table content is packed so tightly, + it's usually a good idea to place some left + on the columns. + """ + ), + html_table(df, table_style={}, cell_style={"paddingLeft": 10}), + section_title("HTML Table - Responsive Table"), + html.Div( + """ + With 100% width, the tables will expand to their + container. When the table gets small, the text will break into + multiple lines. + """ + ), + html_table(df, table_style={"width": "100%"}, base_column_style={}), + section_title("HTML Table - Columns with min-width"), + html.Div( + "Here, the min-width for the first column is 130px, or about the width of this line: " + ), + html.Div( + style={"width": 130, "height": 10, "backgroundColor": "hotpink"} + ), + html_table( + df, + table_style={"width": "100%"}, + column_style={"Date": {"minWidth": "130"}}, + ), + section_title("HTML Table - Underspecified Widths"), + html.Div( + """ + The widths can be under-specified. Here, we're only setting the width for the three + columns in the middle, the rest of the columns are automatically sized to fit the rest of the container. + The columns have a width of 50px, or the width of this line: + """ + ), + html.Div( + style={"width": 50, "height": 10, "backgroundColor": "hotpink"} + ), + html_table( + df, + table_style={"width": "100%"}, + column_style={ + "Dem": {"width": 50}, + "Rep": {"width": 50}, + "Ind": {"width": 50}, + }, + ), + section_title("HTML Table - Widths that are smaller than the content"), + html.Div( + """ + In this case, we're setting the width to 20px, which is smaller + than the "10924" number in the "Ind" column. + The table does not allow it. + """ + ), + html.Div( + style={"width": 20, "height": 10, "backgroundColor": "hotpink"} + ), + html_table( + df, + table_style={"width": "100%"}, + column_style={ + "Dem": {"width": 20}, + "Rep": {"width": 20}, + "Ind": {"width": 20}, + }, + ), + section_title("HTML Table - Content with Ellipses"), + html.Div( + """ + With `max-width`, the content can collapse into + ellipses once the content doesn't fit. + + Here, `max-width` is set to 0. It could be any number, the only + important thing is that it is supplied. The behaviour will be + the same whether it is 0 or 50. + """ + ), + html_table( + df, + table_style={"width": "100%"}, + cell_style={ + "whiteSpace": "nowrap", + "overflow": "hidden", + "textOverflow": "ellipsis", + "maxWidth": 0, + }, + ), + section_title("HTML Table - Vertical Scrolling"), + html.Div( + """ + By supplying a max-height of the Table container and supplying + `overflow-y: scroll`, the table will become scrollable if the + table's contents are larger than the container. + """ + ), + html.Div( + style={"maxHeight": 300, "overflowY": "scroll"}, + children=html_table(df_long, table_style={"width": "100%"}), + ), + section_title("HTML Table - Vertical Scrolling with Max Height"), + html.Div( + """ + With `max-height`, if the table's contents are shorter than the + `max-height`, then the container will be shorter. + If you want a container with a constant height no matter the + contents, then use `height`. + + Here, we're setting max-height to 300, or the height of this line: + """ + ), + html.Div( + style={"width": 5, "height": 300, "backgroundColor": "hotpink"} + ), + html.Div( + style={"maxHeight": 300, "overflowY": "scroll"}, + children=html_table(df, table_style={"width": "100%"}), + ), + section_title("HTML Table - Vertical Scrolling with Height"), + html.Div("and here is `height` with the same content"), + html.Div( + style={"height": 300, "overflowY": "scroll"}, + children=html_table(df, table_style={"width": "100%"}), + ), + section_title("HTML Table - Horizontal Scrolling"), + html.Div( + """ + With HTML tables, we can set `min-width` to be 100%. + If the content is small, then the columns will have some extra + space. + But if the content of any of the cells is really large, then the + cells will expand beyond the container and a scrollbar will appear. + + In this way, `min-width` and `overflow-x: scroll` is an alternative + to `text-overflow: ellipses`. With scroll, the content that can't + fit in the container will get pushed out into a scrollable zone. + With text-overflow: ellipses, the content will get truncated by + ellipses. Both strategies work with or without line breaks on the + white spaces (`white-space: normal` or `white-space: nowrap`). + + These next two examples have the same styles applied: + - `min-width: 100%` + - `white-space: nowrap` (to keep the content on a single line) + - A parent with `overflow-x: scroll` + + """ + ), + section_title("HTML Table - Two Columns, 100% Min-Width"), + html.Div( + html_table( + pd.DataFrame({"Column 1": [1, 2], "Column 2": [3, 3]}), + table_style={"minWidth": "100%"}, + cell_style={"whiteSpace": "nowrap"}, + ), + style={"overflowX": "scroll"}, + ), + section_title("HTML Table - Long Columns, 100% Min-Width"), + html.Div( + """ + Here is a table with several columns with long titles, + 100% min-width, and `'white-space': 'nowrap'` + (to keep the text on a single line) + """ + ), + html.Div( + html_table( + pd.DataFrame( + { + "This is Column {} Data".format(i): [1, 2] + for i in range(10) + } + ), + table_style={"minWidth": "100%", "overflowX": "scroll"}, + cell_style={"whiteSpace": "nowrap"}, + ), + style={"overflowX": "scroll"}, + ), + html.Hr(), + html.H3("Dash Interactive Table"), + html.Div("These same styles can be applied to the dash table"), + section_title("Dash Table - Default Styles"), + dash_table.Table( + id="sizing-1", + dataframe=df.to_dict("rows"), + columns=[{"name": i, "id": i} for i in df.columns], + ), + section_title("Dash Table - Padding"), + # ... + section_title("Dash Table - Underspecified Widths"), + # ... + section_title("Dash Table - Widths that are smaller than the content"), + # ... + section_title("Dash Table - Content with Ellipses"), + # ... + section_title("Dash Table - Vertical Scrolling"), + # ... + section_title("Dash Table - Vertical Scrolling with Max Height"), + # ... + section_title("Dash Table - Vertical Scrolling with Height"), + # ... + section_title("Dash Table - Horizontal Scrolling"), + # ... + section_title("Dash Table - Two Columns, 100% Min-Width"), + # ... + section_title("Dash Table - Long Columns, 100% Min-Width"), + # ... + section_title("Dash Table - Alignment"), + # ... + ], + ), + ] diff --git a/tests/dash/app_styling.py b/tests/dash/app_styling.py new file mode 100644 index 000000000..71fba8961 --- /dev/null +++ b/tests/dash/app_styling.py @@ -0,0 +1,57 @@ +from collections import OrderedDict +from dash.dependencies import Input, Output +import dash_core_components as dcc +import dash_html_components as html +import pandas as pd +from textwrap import dedent + +import dash_table +from index import app +from .utils import html_table, section_title + + +def layout(): + data = OrderedDict( + [ + ("Date", ["2015-01-01", "2015-10-24", "2016-05-10"]), + ("Region", ["Montreal", "Vermont", "New York City"]), + ("Temperature", [1, -20, 3.512]), + ("Humidity", [10, 20, 30]), + ("Pressure", [2, 10924, 3912]), + ] + ) + + df = pd.DataFrame(data) + + return html.Div( + style={"marginLeft": "auto", "marginRight": "auto", "width": "80%"}, + children=[ + html.H1("[WIP] - Styling the Table"), + section_title("HTML Table - Alignment"), + html.Div( + """ + When displaying numerical data, it's a good practice to use + monospaced fonts, to right-align the data, and to provide the same + number of decimals throughout the column. + + Note that it's not possible to modify the number of decimal places + in css. `dash-table` will provide formatting options in the future, + until then you'll have to modify your data before displaying it. + + For textual data, left-aligning the data is usually easier to read. + + In both cases, the column headers should have the same alignment + as the cell content. + """ + ), + html_table( + df, + cell_style={"paddingLeft": 5, "paddingRight": 5}, + cell_style_by_column={ + "Temperature": {"textAlign": "right", "fontFamily": "monospaced"}, + "Humidity": {"textAlign": "right", "fontFamily": "monospaced"}, + "Pressure": {"textAlign": "right", "fontFamily": "monospaced"}, + }, + ), + ], + ) diff --git a/tests/dash/utils.py b/tests/dash/utils.py new file mode 100644 index 000000000..4deb688bd --- /dev/null +++ b/tests/dash/utils.py @@ -0,0 +1,53 @@ +import dash_html_components as html + + +def merge(*args): + merged = {} + for arg in args: + for key in arg: + merged[key] = arg[key] + return merged + + +def section_title(title): + return html.H4(title, style={"marginTop": "20px"}) + + +def html_table( + df, + base_column_style={}, + table_style={}, + column_style={}, + cell_style={}, + cell_style_by_column={}, +): + header = [] + for column in df.columns: + header.append( + html.Th( + column, + style=merge( + base_column_style, + cell_style, + column_style.get(column, {}), + cell_style_by_column.get(column, {}), + ), + ) + ) + + rows = [] + for i in range(len(df)): + row = [] + for column in df.columns: + row.append( + html.Td( + df.iloc[i][column], + style=merge(cell_style, cell_style_by_column.get(column, {})), + ) + ) + rows.append(html.Tr(row)) + + return html.Table( + [html.Thead(header), html.Tbody(rows)], + style=merge(table_style, {"marginTop": "20px", "marginBottom": "20px"}), + )