diff --git a/demo/filter_records.py b/demo/filter_records.py index a947952fc1..a2e4ed2fdc 100644 --- a/demo/filter_records.py +++ b/demo/filter_records.py @@ -1,8 +1,6 @@ # Demo: (Dataframe, Dropdown) -> (Dataframe) import gradio as gr -import numpy as np -import random def filter_records(records, gender): return records[records['gender'] == gender] diff --git a/frontend/src/interfaces/output/dataframe.jsx b/frontend/src/interfaces/output/dataframe.jsx index fd367c3ebb..82ef0d7072 100644 --- a/frontend/src/interfaces/output/dataframe.jsx +++ b/frontend/src/interfaces/output/dataframe.jsx @@ -1,45 +1,129 @@ import React from 'react'; import ComponentExample from '../component_example'; -import jspreadsheet from "jspreadsheet-ce"; -import "../../../node_modules/jspreadsheet-ce/dist/jspreadsheet.css" import classNames from 'classnames'; +import { array_compare } from "../../utils"; class DataframeOutput extends React.Component { constructor(props) { super(props); - this.wrapper = React.createRef(); + this.state = this.constructor.get_default_state() } - componentDidMount = function() { - this.el = jspreadsheet(this.wrapper.current, {minDimensions: [1, 1]}); + static get_default_state() { + return { + "page": 0, + "sort_by": null, + "sort_descending": false + } } - resetData(new_data) { - let [new_rows, new_cols] = [new_data.length, new_data[0].length]; - let current_data = this.el.getData(); - let [cur_rows, cur_cols] = [current_data.length, current_data[0].length]; - if (cur_rows < new_rows) { - this.el.insertRow(new_rows - cur_rows); - } else if (cur_rows > new_rows) { - this.el.deleteRow(0, cur_rows - new_rows); + static getDerivedStateFromProps(nextProps, prevState) { + if (prevState.data === undefined || + !array_compare(nextProps.value.data, prevState.data)) { + let new_state = DataframeOutput.get_default_state(); + new_state["data"] = nextProps.value.data; + return new_state; } - if (cur_cols < new_cols) { - this.el.insertColumn(new_cols - cur_cols); - } else if (cur_cols > new_cols) { - this.el.deleteColumn(0, cur_cols - new_cols); + return null; + } + set_page(page) { + this.setState({ "page": page }); + } + sort_table(col_index) { + if (this.state.sort_by === col_index) { + this.setState({ "sort_descending": !this.state.sort_descending, "page": 0 }); + } else { + this.setState({ "sort_by": col_index, "sort_descending": false, "page": 0 }); } - this.el.setData(new_data); } render() { - if (this.props.value && this.props.value.headers && this.el) { - for (let [i, header] of this.props.value.headers.entries()) - this.el.setHeader(i, header); + if (this.props.value.data.length === 0) { + return null; } - if (this.props.value && this.el) { - this.resetData(this.props.value.data) + let headers = this.props.headers || this.props.value.headers; + let row_count = this.props.value.data.length; + let col_count = this.props.value.data[0].length; + let selected_data = this.props.value.data.slice(); + if (this.state.sort_by !== null) { + selected_data.sort((function (index) { + return function (a, b) { + return (a[index] === b[index] ? 0 : (a[index] < b[index] ? -1 : 1)); + }; + })(this.state.sort_by)); + if (this.state.sort_descending) { + selected_data.reverse(); + } } - return ( -
-
-
) + let visible_pages = null; + if (this.props.max_rows !== null && row_count > this.props.max_rows) { + if (this.props.overflow_row_behaviour === "paginate") { + selected_data = selected_data.slice( + this.state.page * this.props.max_rows, + (this.state.page + 1) * this.props.max_rows + ); + let page_count = Math.ceil(row_count / this.props.max_rows); + visible_pages = []; + [0, this.state.page, page_count - 1].forEach(anchor => { + for (let i = anchor - 2; i <= anchor + 2; i++) { + if (i >= 0 && i < page_count && !visible_pages.includes(i)) { + if (visible_pages.length > 0 && i - visible_pages[visible_pages.length - 1] > 1) { + visible_pages.push(null); + } + visible_pages.push(i); + } + } + }) + } else { + selected_data = selected_data.slice( + 0, + Math.ceil(this.props.max_rows / 2) + ).concat( + [Array(col_count).fill("...")], + selected_data.slice( + row_count - Math.floor(this.props.max_rows / 2) + )); + } + } + if (this.props.max_cols !== null && col_count > this.props.max_cols) { + let [hidden_col_start, hidden_col_end] = [ + Math.ceil(this.props.max_cols / 2), + col_count - Math.floor(this.props.max_cols / 2) - 1 + ]; + headers = headers.slice(0, hidden_col_start) + ["..."] + headers.slice(hidden_col_end); + selected_data = selected_data.map(row => + row.slice(0, hidden_col_start) + ["..."] + row.slice(hidden_col_end) + ); + } + return
+ + {headers ? + + {headers.map((header, i) => + )} + + : false} + + {selected_data.map(row => { + return {row.map(cell => )} + })} + +
+ {header} + {this.state.sort_by === i ? ( + this.state.sort_descending ? "⇧" : "⇩") + : false} +
{cell}
+ {visible_pages !== null ? +
+ Pages: {visible_pages.map(page => page === null ? +
...
: + + )} +
+ : false} +
} } @@ -71,4 +155,4 @@ class DataframeOutputExample extends ComponentExample { } } -export {DataframeOutput, DataframeOutputExample}; +export { DataframeOutput, DataframeOutputExample }; diff --git a/frontend/src/themes/defaults.scss b/frontend/src/themes/defaults.scss index c9c93d9b2f..dffd287d51 100644 --- a/frontend/src/themes/defaults.scss +++ b/frontend/src/themes/defaults.scss @@ -472,6 +472,31 @@ @apply text-2xl p-2; } } + .output_dataframe { + table { + thead { + @apply font-bold border-gray-200 border-b-2; + } + th { + @apply transition cursor-pointer; + } + th:hover { + @apply bg-gray-200; + } + td, th { + @apply px-4; + } + } + .pages { + @apply flex gap-1 items-center; + } + .page { + @apply px-2 py-1 bg-gray-200; + } + .page.selected { + @apply bg-gray-300; + } + } .output_carousel { @apply flex flex-col gap-2; .carousel_control { diff --git a/frontend/src/utils.jsx b/frontend/src/utils.jsx index f60933d218..1d8c52c34b 100644 --- a/frontend/src/utils.jsx +++ b/frontend/src/utils.jsx @@ -88,4 +88,22 @@ export function saveAs(uri, filename) { } else { window.open(uri); } - } \ No newline at end of file +} + +export function array_compare(a1, a2) { + if (a1.length != a2.length) { + return false; + } + for (var i in a1) { + // Don't forget to check for arrays in our arrays. + if (a1[i] instanceof Array && a2[i] instanceof Array) { + if (!array_compare(a1[i], a2[i])) { + return false; + } + } + else if (a1[i] != a2[i]) { + return false; + } + } + return true; +} diff --git a/gradio.egg-info/requires.txt b/gradio.egg-info/requires.txt index 8e565209f9..b6135b3e41 100644 --- a/gradio.egg-info/requires.txt +++ b/gradio.egg-info/requires.txt @@ -1,15 +1,15 @@ -numpy -scipy -matplotlib -pandas -pillow +Flask-Cors>=3.0.8 +Flask-Login +Flask>=1.1.1 +analytics-python ffmpy +flask-cachebuster markdown2 +matplotlib +numpy +pandas +paramiko +pillow pycryptodome requests -paramiko -analytics-python -Flask>=1.1.1 -Flask-Cors>=3.0.8 -flask-cachebuster -Flask-Login +scipy diff --git a/gradio/outputs.py b/gradio/outputs.py index 74a9298622..1ac2379102 100644 --- a/gradio/outputs.py +++ b/gradio/outputs.py @@ -441,21 +441,30 @@ class Dataframe(OutputComponent): Output type: Union[pandas.DataFrame, numpy.array, List[Union[str, float]], List[List[Union[str, float]]]] """ - def __init__(self, headers=None, type="auto", label=None): + def __init__(self, headers=None, max_rows=20, max_cols=None, overflow_row_behaviour="paginate", type="auto", label=None): ''' Parameters: - headers (List[str]): Header names to dataframe. + headers (List[str]): Header names to dataframe. Only applicable if type is "numpy" or "array". + max_rows (int): Maximum number of rows to display at once. Set to None for infinite. + max_cols (int): Maximum number of columns to display at once. Set to None for infinite. + overflow_row_behaviour (str): If set to "paginate", will create pages for overflow rows. If set to "show_ends", will show initial and final rows and truncate middle rows. type (str): Type of value to be passed to component. "pandas" for pandas dataframe, "numpy" for numpy array, or "array" for Python array, "auto" detects return type. label (str): component name in interface. ''' - self.type = type self.headers = headers + self.max_rows = max_rows + self.max_cols = max_cols + self.overflow_row_behaviour = overflow_row_behaviour + self.type = type super().__init__(label) def get_template_context(self): return { "headers": self.headers, + "max_rows": self.max_rows, + "max_cols": self.max_cols, + "overflow_row_behaviour": self.overflow_row_behaviour, **super().get_template_context() }