mirror of
https://github.com/gradio-app/gradio.git
synced 2024-11-21 01:01:05 +08:00
d1cfe1e9de
* changes * add changeset * changes * changes * chanes * changes --------- Co-authored-by: Ali Abid <aliabid94@gmail.com> Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com> Co-authored-by: Abubakar Abid <abubakar@huggingface.co>
580 lines
15 KiB
Svelte
580 lines
15 KiB
Svelte
<script lang="ts">
|
|
import type { Gradio, SelectData } from "@gradio/utils";
|
|
import { BlockTitle } from "@gradio/atoms";
|
|
import { Block } from "@gradio/atoms";
|
|
import { StatusTracker } from "@gradio/statustracker";
|
|
import type { LoadingStatus } from "@gradio/statustracker";
|
|
import { onMount } from "svelte";
|
|
|
|
import type { TopLevelSpec as Spec } from "vega-lite";
|
|
import type { View } from "vega";
|
|
import { LineChart as LabelIcon } from "@gradio/icons";
|
|
import { Empty } from "@gradio/atoms";
|
|
|
|
interface PlotData {
|
|
columns: string[];
|
|
data: [string | number][];
|
|
datatypes: Record<string, "quantitative" | "temporal" | "nominal">;
|
|
mark: "line" | "point" | "bar";
|
|
}
|
|
export let value: PlotData | null;
|
|
export let x: string;
|
|
export let y: string;
|
|
export let color: string | null = null;
|
|
export let root: string;
|
|
$: unique_colors =
|
|
color && value && value.datatypes[color] === "nominal"
|
|
? Array.from(new Set(_data.map((d) => d[color])))
|
|
: [];
|
|
|
|
export let title: string | null = null;
|
|
export let x_title: string | null = null;
|
|
export let y_title: string | null = null;
|
|
export let color_title: string | null = null;
|
|
export let x_bin: string | number | null = null;
|
|
export let y_aggregate:
|
|
| "sum"
|
|
| "mean"
|
|
| "median"
|
|
| "min"
|
|
| "max"
|
|
| undefined = undefined;
|
|
export let color_map: Record<string, string> | null = null;
|
|
export let x_lim: [number, number] | null = null;
|
|
export let y_lim: [number, number] | null = null;
|
|
export let x_label_angle: number | null = null;
|
|
export let y_label_angle: number | null = null;
|
|
export let x_axis_labels_visible = true;
|
|
export let caption: string | null = null;
|
|
export let sort: "x" | "y" | "-x" | "-y" | string[] | null = null;
|
|
export let tooltip: "axis" | "none" | "all" | string[] = "axis";
|
|
function reformat_sort(
|
|
_sort: typeof sort
|
|
):
|
|
| string
|
|
| "ascending"
|
|
| "descending"
|
|
| { field: string; order: "ascending" | "descending" }
|
|
| string[]
|
|
| undefined {
|
|
if (_sort === "x") {
|
|
return "ascending";
|
|
} else if (_sort === "-x") {
|
|
return "descending";
|
|
} else if (_sort === "y") {
|
|
return { field: y, order: "ascending" };
|
|
} else if (_sort === "-y") {
|
|
return { field: y, order: "descending" };
|
|
} else if (_sort === null) {
|
|
return undefined;
|
|
} else if (Array.isArray(_sort)) {
|
|
return _sort;
|
|
}
|
|
}
|
|
$: _sort = reformat_sort(sort);
|
|
export let _selectable = false;
|
|
let _data: {
|
|
[x: string]: string | number;
|
|
}[];
|
|
export let gradio: Gradio<{
|
|
select: SelectData;
|
|
double_click: undefined;
|
|
clear_status: LoadingStatus;
|
|
}>;
|
|
|
|
$: x_temporal = value && value.datatypes[x] === "temporal";
|
|
$: _x_lim = x_lim && x_temporal ? [x_lim[0] * 1000, x_lim[1] * 1000] : x_lim;
|
|
let _x_bin: number | undefined;
|
|
let mouse_down_on_chart = false;
|
|
const SUFFIX_DURATION: Record<string, number> = {
|
|
s: 1,
|
|
m: 60,
|
|
h: 60 * 60,
|
|
d: 24 * 60 * 60
|
|
};
|
|
$: _x_bin = x_bin
|
|
? typeof x_bin === "string"
|
|
? 1000 *
|
|
parseInt(x_bin.substring(0, x_bin.length - 1)) *
|
|
SUFFIX_DURATION[x_bin[x_bin.length - 1]]
|
|
: x_bin
|
|
: undefined;
|
|
let _y_aggregate: typeof y_aggregate;
|
|
let aggregating: boolean;
|
|
$: {
|
|
if (value) {
|
|
if (value.mark === "point") {
|
|
aggregating = _x_bin !== undefined;
|
|
_y_aggregate = y_aggregate || aggregating ? "sum" : undefined;
|
|
} else {
|
|
aggregating = _x_bin !== undefined || value.datatypes[x] === "nominal";
|
|
_y_aggregate = y_aggregate ? y_aggregate : "sum";
|
|
}
|
|
}
|
|
}
|
|
function reformat_data(data: PlotData): {
|
|
[x: string]: string | number;
|
|
}[] {
|
|
if (tooltip == "all" || Array.isArray(tooltip)) {
|
|
return data.data.map((row) => {
|
|
const obj: { [x: string]: string | number } = {};
|
|
data.columns.forEach((col, i) => {
|
|
obj[col] = row[i];
|
|
});
|
|
return obj;
|
|
});
|
|
}
|
|
let x_index = data.columns.indexOf(x);
|
|
let y_index = data.columns.indexOf(y);
|
|
let color_index = color ? data.columns.indexOf(color) : null;
|
|
return data.data.map((row) => {
|
|
const obj = {
|
|
[x]: row[x_index],
|
|
[y]: row[y_index]
|
|
};
|
|
if (color && color_index !== null) {
|
|
obj[color] = row[color_index];
|
|
}
|
|
return obj;
|
|
});
|
|
}
|
|
$: _data = value ? reformat_data(value) : [];
|
|
|
|
const is_browser = typeof window !== "undefined";
|
|
let chart_element: HTMLDivElement;
|
|
$: computed_style = chart_element
|
|
? window.getComputedStyle(chart_element)
|
|
: null;
|
|
let view: View;
|
|
let mounted = false;
|
|
let old_width: number;
|
|
let resizeObserver: ResizeObserver;
|
|
|
|
let vegaEmbed: typeof import("vega-embed").default;
|
|
async function load_chart(): Promise<void> {
|
|
if (view) {
|
|
view.finalize();
|
|
}
|
|
if (!value || !chart_element) return;
|
|
old_width = chart_element.offsetWidth;
|
|
const spec = create_vega_lite_spec();
|
|
if (!spec) return;
|
|
resizeObserver = new ResizeObserver((el) => {
|
|
if (!el[0].target || !(el[0].target instanceof HTMLElement)) return;
|
|
if (
|
|
old_width === 0 &&
|
|
chart_element.offsetWidth !== 0 &&
|
|
value.datatypes[x] === "nominal"
|
|
) {
|
|
// a bug where when a nominal chart is first loaded, the width is 0, it doesn't resize
|
|
load_chart();
|
|
} else {
|
|
view.signal("width", el[0].target.offsetWidth).run();
|
|
}
|
|
});
|
|
|
|
if (!vegaEmbed) {
|
|
vegaEmbed = (await import("vega-embed")).default;
|
|
}
|
|
vegaEmbed(chart_element, spec, { actions: false }).then(function (result) {
|
|
view = result.view;
|
|
|
|
resizeObserver.observe(chart_element);
|
|
var debounceTimeout: NodeJS.Timeout;
|
|
view.addEventListener("dblclick", () => {
|
|
gradio.dispatch("double_click");
|
|
});
|
|
// prevent double-clicks from highlighting text
|
|
chart_element.addEventListener(
|
|
"mousedown",
|
|
function (e) {
|
|
if (e.detail > 1) {
|
|
e.preventDefault();
|
|
}
|
|
},
|
|
false
|
|
);
|
|
if (_selectable) {
|
|
view.addSignalListener("brush", function (_, value) {
|
|
if (Object.keys(value).length === 0) return;
|
|
clearTimeout(debounceTimeout);
|
|
let range: [number, number] = value[Object.keys(value)[0]];
|
|
if (x_temporal) {
|
|
range = [range[0] / 1000, range[1] / 1000];
|
|
}
|
|
let callback = (): void => {
|
|
gradio.dispatch("select", {
|
|
value: range,
|
|
index: range,
|
|
selected: true
|
|
});
|
|
};
|
|
if (mouse_down_on_chart) {
|
|
release_callback = callback;
|
|
} else {
|
|
debounceTimeout = setTimeout(function () {
|
|
gradio.dispatch("select", {
|
|
value: range,
|
|
index: range,
|
|
selected: true
|
|
});
|
|
}, 250);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
let release_callback: (() => void) | null = null;
|
|
onMount(() => {
|
|
mounted = true;
|
|
chart_element.addEventListener("mousedown", () => {
|
|
mouse_down_on_chart = true;
|
|
});
|
|
chart_element.addEventListener("mouseup", () => {
|
|
mouse_down_on_chart = false;
|
|
if (release_callback) {
|
|
release_callback();
|
|
release_callback = null;
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
mounted = false;
|
|
if (view) {
|
|
view.finalize();
|
|
}
|
|
if (resizeObserver) {
|
|
resizeObserver.disconnect();
|
|
}
|
|
};
|
|
});
|
|
|
|
$: title,
|
|
x_title,
|
|
y_title,
|
|
color_title,
|
|
x,
|
|
y,
|
|
color,
|
|
x_bin,
|
|
_y_aggregate,
|
|
color_map,
|
|
x_lim,
|
|
y_lim,
|
|
caption,
|
|
sort,
|
|
value,
|
|
mounted,
|
|
chart_element,
|
|
computed_style && requestAnimationFrame(load_chart);
|
|
|
|
function create_vega_lite_spec(): Spec | null {
|
|
if (!value || !computed_style) return null;
|
|
let accent_color = computed_style.getPropertyValue("--color-accent");
|
|
let body_text_color = computed_style.getPropertyValue("--body-text-color");
|
|
let borderColorPrimary = computed_style.getPropertyValue(
|
|
"--border-color-primary"
|
|
);
|
|
let font_family = computed_style.fontFamily;
|
|
let title_weight = computed_style.getPropertyValue(
|
|
"--block-title-text-weight"
|
|
) as
|
|
| "bold"
|
|
| "normal"
|
|
| 100
|
|
| 200
|
|
| 300
|
|
| 400
|
|
| 500
|
|
| 600
|
|
| 700
|
|
| 800
|
|
| 900;
|
|
const font_to_px_val = (font: string): number => {
|
|
return font.endsWith("px") ? parseFloat(font.slice(0, -2)) : 12;
|
|
};
|
|
let text_size_md = font_to_px_val(
|
|
computed_style.getPropertyValue("--text-md")
|
|
);
|
|
let text_size_sm = font_to_px_val(
|
|
computed_style.getPropertyValue("--text-sm")
|
|
);
|
|
|
|
/* eslint-disable complexity */
|
|
return {
|
|
$schema: "https://vega.github.io/schema/vega-lite/v5.17.0.json",
|
|
background: "transparent",
|
|
config: {
|
|
autosize: { type: "fit", contains: "padding" },
|
|
axis: {
|
|
labelFont: font_family,
|
|
labelColor: body_text_color,
|
|
titleFont: font_family,
|
|
titleColor: body_text_color,
|
|
titlePadding: 8,
|
|
tickColor: borderColorPrimary,
|
|
labelFontSize: text_size_sm,
|
|
gridColor: borderColorPrimary,
|
|
titleFontWeight: "normal",
|
|
titleFontSize: text_size_sm,
|
|
labelFontWeight: "normal",
|
|
domain: false,
|
|
labelAngle: 0
|
|
},
|
|
legend: {
|
|
labelColor: body_text_color,
|
|
labelFont: font_family,
|
|
titleColor: body_text_color,
|
|
titleFont: font_family,
|
|
titleFontWeight: "normal",
|
|
titleFontSize: text_size_sm,
|
|
labelFontWeight: "normal",
|
|
offset: 2
|
|
},
|
|
title: {
|
|
color: body_text_color,
|
|
font: font_family,
|
|
fontSize: text_size_md,
|
|
fontWeight: title_weight,
|
|
anchor: "middle"
|
|
},
|
|
view: { stroke: borderColorPrimary },
|
|
mark: {
|
|
stroke: value.mark !== "bar" ? accent_color : undefined,
|
|
fill: value.mark === "bar" ? accent_color : undefined,
|
|
cursor: "crosshair"
|
|
}
|
|
},
|
|
data: { name: "data" },
|
|
datasets: {
|
|
data: _data
|
|
},
|
|
layer: ["plot", ...(value.mark === "line" ? ["hover"] : [])].map(
|
|
(mode) => {
|
|
return {
|
|
encoding: {
|
|
size:
|
|
value.mark === "line"
|
|
? mode == "plot"
|
|
? {
|
|
condition: {
|
|
empty: false,
|
|
param: "hoverPlot",
|
|
value: 3
|
|
},
|
|
value: 2
|
|
}
|
|
: {
|
|
condition: { empty: false, param: "hover", value: 100 },
|
|
value: 0
|
|
}
|
|
: undefined,
|
|
opacity:
|
|
mode === "plot"
|
|
? undefined
|
|
: {
|
|
condition: { empty: false, param: "hover", value: 1 },
|
|
value: 0
|
|
},
|
|
x: {
|
|
axis: {
|
|
...(x_label_angle !== null && { labelAngle: x_label_angle }),
|
|
labels: x_axis_labels_visible,
|
|
ticks: x_axis_labels_visible
|
|
},
|
|
field: x,
|
|
title: x_title || x,
|
|
type: value.datatypes[x],
|
|
scale: _x_lim ? { domain: _x_lim } : undefined,
|
|
bin: _x_bin ? { step: _x_bin } : undefined,
|
|
sort: _sort
|
|
},
|
|
y: {
|
|
axis: y_label_angle ? { labelAngle: y_label_angle } : {},
|
|
field: y,
|
|
title: y_title || y,
|
|
type: value.datatypes[y],
|
|
scale: y_lim ? { domain: y_lim } : undefined,
|
|
aggregate: aggregating ? _y_aggregate : undefined
|
|
},
|
|
color: color
|
|
? {
|
|
field: color,
|
|
legend: { orient: "bottom", title: color_title },
|
|
scale:
|
|
value.datatypes[color] === "nominal"
|
|
? {
|
|
domain: unique_colors,
|
|
range: color_map
|
|
? unique_colors.map((c) => color_map[c])
|
|
: undefined
|
|
}
|
|
: {
|
|
range: [
|
|
100, 200, 300, 400, 500, 600, 700, 800, 900
|
|
].map((n) =>
|
|
computed_style.getPropertyValue("--primary-" + n)
|
|
),
|
|
interpolate: "hsl"
|
|
},
|
|
type: value.datatypes[color]
|
|
}
|
|
: undefined,
|
|
tooltip:
|
|
tooltip == "none"
|
|
? undefined
|
|
: [
|
|
{
|
|
field: y,
|
|
type: value.datatypes[y],
|
|
aggregate: aggregating ? _y_aggregate : undefined,
|
|
title: y_title || y
|
|
},
|
|
{
|
|
field: x,
|
|
type: value.datatypes[x],
|
|
title: x_title || x,
|
|
format: x_temporal ? "%Y-%m-%d %H:%M:%S" : undefined,
|
|
bin: _x_bin ? { step: _x_bin } : undefined
|
|
},
|
|
...(color
|
|
? [
|
|
{
|
|
field: color,
|
|
type: value.datatypes[color]
|
|
}
|
|
]
|
|
: []),
|
|
...(tooltip === "axis"
|
|
? []
|
|
: value?.columns
|
|
.filter(
|
|
(col) =>
|
|
col !== x &&
|
|
col !== y &&
|
|
col !== color &&
|
|
(tooltip === "all" || tooltip.includes(col))
|
|
)
|
|
.map((column) => ({
|
|
field: column,
|
|
type: value.datatypes[column]
|
|
})))
|
|
]
|
|
},
|
|
strokeDash: {},
|
|
mark: { clip: true, type: mode === "hover" ? "point" : value.mark },
|
|
name: mode
|
|
};
|
|
}
|
|
),
|
|
// @ts-ignore
|
|
params: [
|
|
...(value.mark === "line"
|
|
? [
|
|
{
|
|
name: "hoverPlot",
|
|
select: {
|
|
clear: "mouseout",
|
|
fields: color ? [color] : [],
|
|
nearest: true,
|
|
on: "mouseover",
|
|
type: "point" as "point"
|
|
},
|
|
views: ["hover"]
|
|
},
|
|
{
|
|
name: "hover",
|
|
select: {
|
|
clear: "mouseout",
|
|
nearest: true,
|
|
on: "mouseover",
|
|
type: "point" as "point"
|
|
},
|
|
views: ["hover"]
|
|
}
|
|
]
|
|
: []),
|
|
...(_selectable
|
|
? [
|
|
{
|
|
name: "brush",
|
|
select: {
|
|
encodings: ["x"],
|
|
mark: { fill: "gray", fillOpacity: 0.3, stroke: "none" },
|
|
type: "interval" as "interval"
|
|
},
|
|
views: ["plot"]
|
|
}
|
|
]
|
|
: [])
|
|
],
|
|
width: chart_element.offsetWidth,
|
|
title: title || undefined
|
|
};
|
|
/* eslint-enable complexity */
|
|
}
|
|
|
|
export let label = "Textbox";
|
|
export let elem_id = "";
|
|
export let elem_classes: string[] = [];
|
|
export let visible = true;
|
|
export let show_label: boolean;
|
|
export let scale: number | null = null;
|
|
export let min_width: number | undefined = undefined;
|
|
export let loading_status: LoadingStatus | undefined = undefined;
|
|
export let height: number | undefined = undefined;
|
|
</script>
|
|
|
|
<Block
|
|
{visible}
|
|
{elem_id}
|
|
{elem_classes}
|
|
{scale}
|
|
{min_width}
|
|
allow_overflow={false}
|
|
padding={true}
|
|
{height}
|
|
>
|
|
{#if loading_status}
|
|
<StatusTracker
|
|
autoscroll={gradio.autoscroll}
|
|
i18n={gradio.i18n}
|
|
{...loading_status}
|
|
on:clear_status={() => gradio.dispatch("clear_status", loading_status)}
|
|
/>
|
|
{/if}
|
|
<BlockTitle {root} {show_label} info={undefined}>{label}</BlockTitle>
|
|
{#if value && is_browser}
|
|
<div bind:this={chart_element}></div>
|
|
|
|
{#if caption}
|
|
<p class="caption">{caption}</p>
|
|
{/if}
|
|
{:else}
|
|
<Empty unpadded_box={true}><LabelIcon /></Empty>
|
|
{/if}
|
|
</Block>
|
|
|
|
<style>
|
|
div {
|
|
width: 100%;
|
|
}
|
|
:global(#vg-tooltip-element) {
|
|
font-family: var(--font) !important;
|
|
font-size: var(--text-xs) !important;
|
|
box-shadow: none !important;
|
|
background-color: var(--block-background-fill) !important;
|
|
border: 1px solid var(--border-color-primary) !important;
|
|
color: var(--body-text-color) !important;
|
|
}
|
|
:global(#vg-tooltip-element .key) {
|
|
color: var(--body-text-color-subdued) !important;
|
|
}
|
|
.caption {
|
|
padding: 0 4px;
|
|
margin: 0;
|
|
text-align: center;
|
|
}
|
|
</style>
|