Add styling (e.g. font colors and background colors) support to gr.DataFrame through the pd.Styler object (#5877)

* colors

* add changeset

* revert

* revert

* colors

* move box shadow

* done

* lint

* lint

* fixes

* lint

* added focused false

* revert focus

* revert focus

* revert focus

* revert focus

* revert focus

* apply style to td instead

* dataframe

* lint

* test

* Update js/dataframe/shared/Table.svelte

Co-authored-by: aliabid94 <aabid94@gmail.com>

* notebooks

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
Co-authored-by: aliabid94 <aabid94@gmail.com>
This commit is contained in:
Abubakar Abid 2023-10-12 15:45:55 -07:00 committed by GitHub
parent e70805d54c
commit a55b809423
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 175 additions and 41 deletions

View File

@ -0,0 +1,6 @@
---
"@gradio/dataframe": minor
"gradio": minor
---
feat:Add styling (e.g. font colors and background colors) support to `gr.DataFrame` through the `pd.Styler` object

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"cells": [{"cell_type": "markdown", "id": 302934307671667531413257853548643485645, "metadata": {}, "source": ["# Gradio Demo: dataframe_colorful"]}, {"cell_type": "code", "execution_count": null, "id": 272996653310673477252411125948039410165, "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": 288918539441861185822528903084949547379, "metadata": {}, "outputs": [], "source": ["import pandas as pd \n", "import gradio as gr\n", "\n", "df = pd.DataFrame({\"A\" : [14, 4, 5, 4, 1], \n", "\t\t\t\t\"B\" : [5, 2, 54, 3, 2], \n", "\t\t\t\t\"C\" : [20, 20, 7, 3, 8], \n", "\t\t\t\t\"D\" : [14, 3, 6, 2, 6], \n", "\t\t\t\t\"E\" : [23, 45, 64, 32, 23]}) \n", "\n", "t = df.style.highlight_max(color = 'lightgreen', axis = 0)\n", "\n", "with gr.Blocks() as demo:\n", " gr.Dataframe(t)\n", " \n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

View File

@ -0,0 +1,16 @@
import pandas as pd
import gradio as gr
df = pd.DataFrame({"A" : [14, 4, 5, 4, 1],
"B" : [5, 2, 54, 3, 2],
"C" : [20, 20, 7, 3, 8],
"D" : [14, 3, 6, 2, 6],
"E" : [23, 45, 64, 32, 23]})
t = df.style.highlight_max(color = 'lightgreen', axis = 0)
with gr.Blocks() as demo:
gr.Dataframe(t)
if __name__ == "__main__":
demo.launch()

View File

@ -300,18 +300,32 @@ class Dataframe(Changeable, Inputable, Selectable, IOComponent, JSONSerializable
raise ValueError(f"Cannot process value as a Dataframe: {y}")
return asdict(value)
@staticmethod
def __get_cell_style(cell_id: str, cell_styles: list[dict]) -> str:
styles_for_cell = []
for style in cell_styles:
if cell_id in style.get("selectors", []):
styles_for_cell.extend(style.get("props", []))
styles_str = "; ".join([f"{prop}: {value}" for prop, value in styles_for_cell])
return styles_str
@staticmethod
def __extract_metadata(df: Styler) -> dict[str, list[list]]:
metadata = {"display_value": []}
metadata = {"display_value": [], "styling": []}
style_data = df._compute()._translate(None, None) # type: ignore
cell_styles = style_data.get("cellstyle", [])
for i in range(len(style_data["body"])):
metadata["display_value"].append([])
metadata["styling"].append([])
for j in range(len(style_data["body"][i])):
cell_type = style_data["body"][i][j]["type"]
if cell_type != "td":
continue
display_value = style_data["body"][i][j]["display_value"]
cell_id = style_data["body"][i][j]["id"]
styles_str = Dataframe.__get_cell_style(cell_id, cell_styles)
metadata["display_value"][i].append(display_value)
metadata["styling"][i].append(styles_str)
return metadata
@staticmethod

View File

@ -23,12 +23,15 @@
<Story
name="Interactive dataframe"
args={{
values: [
value: {
data: [
["Cat", 5],
["Horse", 3],
["Snake", 1]
],
headers: ["Animal", "Votes"],
metadata: null
},
label: "Animals",
col_count: [2, "dynamic"],
row_count: [3, "dynamic"]
@ -38,12 +41,15 @@
<Story
name="Static dataframe"
args={{
values: [
value: {
data: [
["Cat", 5],
["Horse", 3],
["Snake", 1]
],
headers: ["Animal", "Votes"],
metadata: null
},
label: "Animals",
col_count: [2, "dynamic"],
row_count: [3, "dynamic"],
@ -54,11 +60,12 @@
<Story
name="Dataframe with different precisions"
args={{
values: {
value: {
data: [
[1.24, 1.24, 1.24],
[1.21, 1.21, 1.21]
],
headers: ["Precision=0", "Precision=1", "Precision=2"],
metadata: {
display_value: [
["1", "1.2", "1.24"],
@ -66,8 +73,7 @@
]
}
},
headers: ["Precision=0", "Precision=1", "Precision=2"],
label: "Animals",
label: "Numbers",
col_count: [3, "dynamic"],
row_count: [2, "dynamic"],
editable: false
@ -77,12 +83,15 @@
<Story
name="Dataframe with markdown and math"
args={{
values: [
value: {
data: [
["Linear", "$y=x$", "Has a *maximum* of 1 root"],
["Quadratic", "$y=x^2$", "Has a *maximum* of 2 roots"],
["Cubic", "$y=x^3$", "Has a *maximum* of 3 roots"]
],
headers: ["Type", "Example", "Roots"],
metadata: null
},
datatype: ["str", "markdown", "markdown"],
latex_delimiters: [{ left: "$", right: "$", display: false }],
label: "Math",
@ -93,12 +102,28 @@
/>
<Story
name="Empty dataframe"
name="Dataframe with different colors"
args={{
values: [[]],
headers: ["Animal", "Votes"],
label: "Animals",
col_count: [2, "dynamic"],
row_count: [0, "dynamic"]
value: {
data: [
[800, 100, 800],
[200, 800, 700]
],
headers: ["Math", "Reading", "Writing"],
metadata: {
styling: [
[
"background-color:teal; color: white",
"1.2",
"background-color:teal; color: white"
],
["1", "background-color:teal; color: white", "1.21"]
]
}
},
label: "Test scores",
col_count: [3, "dynamic"],
row_count: [2, "dynamic"],
editable: false
}}
/>

View File

@ -6,6 +6,7 @@
export let edit: boolean;
export let value: string | number = "";
export let display_value: string | null = null;
export let styling = "";
export let header = false;
export let datatype:
| "str"
@ -64,6 +65,7 @@
role="button"
class:edit
on:focus|preventDefault
style={styling}
>
{#if datatype === "html"}
{@html value}

View File

@ -36,12 +36,14 @@
export let line_breaks = true;
let selected: false | [number, number] = false;
let display_value: string[][] | null = value?.metadata?.display_value ?? null;
let styling: string[][] | null = value?.metadata?.styling ?? null;
$: {
if (value) {
headers = value.headers;
values = value.data;
display_value = value?.metadata?.display_value ?? null;
styling = value?.metadata?.styling ?? null;
} else if (values === null) {
values = [];
}
@ -162,7 +164,9 @@
dispatch("change", {
data: data.map((r) => r.map(({ value }) => value)),
headers: _headers.map((h) => h.value),
metadata: editable ? null : { display_value: display_value }
metadata: editable
? null
: { display_value: display_value, styling: styling }
});
function get_sort_status(
@ -556,13 +560,14 @@
function sort_data(
_data: typeof data,
_display_value: string[][] | null,
_styling: string[][] | null,
col?: number,
dir?: SortDirection
): void {
let id = null
let id = null;
//Checks if the selected cell is still in the data
if (selected && selected[0] in data && selected[1] in data[selected[0]]) {
id = data[selected[0]][selected[1]].id
id = data[selected[0]][selected[1]].id;
}
if (typeof col !== "number" || !dir) {
return;
@ -581,13 +586,16 @@
return;
}
// sort both data and display_value in place based on the values in data
const tempData = [..._data];
const tempData2 = _display_value ? [..._display_value] : null;
// sort all the data and metadata based on the values in the data
const temp_data = [..._data];
const temp_display_value = _display_value ? [..._display_value] : null;
const temp_styling = _styling ? [..._styling] : null;
indices.forEach((originalIndex, sortedIndex) => {
_data[sortedIndex] = tempData[originalIndex];
if (_display_value && tempData2)
_display_value[sortedIndex] = tempData2[originalIndex];
_data[sortedIndex] = temp_data[originalIndex];
if (_display_value && temp_display_value)
_display_value[sortedIndex] = temp_display_value[originalIndex];
if (_styling && temp_styling)
_styling[sortedIndex] = temp_styling[originalIndex];
});
data = data;
@ -598,7 +606,7 @@
}
}
$: sort_data(data, display_value, sort_by, sort_direction);
$: sort_data(data, display_value, styling, sort_by, sort_direction);
$: selected_index = !!selected && selected[0];
@ -773,7 +781,8 @@
on:touchstart={() => start_edit(index, j)}
on:click={() => handle_cell_click(index, j)}
on:dblclick={() => start_edit(index, j)}
style="width: var(--cell-width-{j});"
style:width="var(--cell-width-{j})"
style={styling?.[index]?.[j] || ""}
class:focus={dequal(selected, [index, j])}
>
<div class="cell-wrap">

View File

@ -1417,7 +1417,68 @@ class TestDataframe:
["Mike", "1,1", "800"],
["Adam", "1,1", "800"],
["Mike", "1,1", "800"],
]
],
"styling": [
["", "", ""],
["", "", ""],
["", "", ""],
["", "", ""],
["", "", ""],
["", "", ""],
["", "", ""],
["", "", ""],
],
},
}
df = pd.DataFrame(
{
"A": [14, 4, 5, 4, 1],
"B": [5, 2, 54, 3, 2],
"C": [20, 20, 7, 3, 8],
"D": [14, 3, 6, 2, 6],
"E": [23, 45, 64, 32, 23],
}
)
t = df.style.highlight_max(color="lightgreen", axis=0)
output = component.postprocess(t)
assert output == {
"data": [
[14, 5, 20, 14, 23],
[4, 2, 20, 3, 45],
[5, 54, 7, 6, 64],
[4, 3, 3, 2, 32],
[1, 2, 8, 6, 23],
],
"headers": ["A", "B", "C", "D", "E"],
"metadata": {
"display_value": [
["14", "5", "20", "14", "23"],
["4", "2", "20", "3", "45"],
["5", "54", "7", "6", "64"],
["4", "3", "3", "2", "32"],
["1", "2", "8", "6", "23"],
],
"styling": [
[
"background-color: lightgreen",
"",
"background-color: lightgreen",
"background-color: lightgreen",
"",
],
["", "", "background-color: lightgreen", "", ""],
[
"",
"background-color: lightgreen",
"",
"",
"background-color: lightgreen",
],
["", "", "", "", ""],
["", "", "", "", ""],
],
},
}