CoolFace
Datasetpublic

gradio/frontend

sourceHugging Faceupdated 13h agoView on Hugging Face
1likes442kdownloads
Index.svelte866 linesDownload Raw Back to nativeplot
1<script lang="ts">2	import { Gradio } from "@gradio/utils";3	import { BlockTitle } from "@gradio/atoms";4	import { Block } from "@gradio/atoms";5	import {6		FullscreenButton,7		IconButtonWrapper,8		IconButton9	} from "@gradio/atoms";10	import { StatusTracker } from "@gradio/statustracker";11	import { onMount, untrack } from "svelte";12	import { Download } from "@gradio/icons";13 14	import type { TopLevelSpec as Spec } from "vega-lite";15	import type { View } from "vega";16	import { LineChart as LabelIcon } from "@gradio/icons";17	import { Empty } from "@gradio/atoms";18	import type { NativePlotProps, NativePlotEvents, PlotData } from "./types";19 20	let props = $props();21	const gradio = new Gradio<NativePlotEvents, NativePlotProps>(props);22 23	let unique_colors = $derived.by(() => {24		if (25			!gradio.props.color ||26			!gradio.props.value ||27			gradio.props.value.datatypes[gradio.props.color] !== "nominal"28		) {29			return [];30		}31		const color_index = gradio.props.value.columns.indexOf(gradio.props.color);32		if (color_index === -1) return [];33		return Array.from(34			new Set(gradio.props.value.data.map((row) => row[color_index]))35		);36	});37 38	let x_lim = $derived(gradio.props.x_lim || null); // for some unknown reason, x_lim was getting set to undefined when used in re-render, so this line is needed39	let y_lim = $derived(gradio.props.y_lim || null);40	let x_start = $derived(x_lim?.[0] !== null ? x_lim?.[0] : undefined);41	let x_end = $derived(x_lim?.[1] !== null ? x_lim?.[1] : undefined);42	let y_start = $derived(y_lim?.[0] !== null ? y_lim?.[0] : undefined);43	let y_end = $derived(y_lim?.[1] !== null ? y_lim?.[1] : undefined);44 45	let fullscreen = $state(false);46 47	function reformat_sort(48		_sort: NativePlotProps["sort"]49	):50		| string51		| "ascending"52		| "descending"53		| { field: string; order: "ascending" | "descending" }54		| string[]55		| null56		| undefined {57		if (_sort === "x") {58			return "ascending";59		} else if (_sort === "-x") {60			return "descending";61		} else if (_sort === "y") {62			return { field: gradio.props.y, order: "ascending" };63		} else if (_sort === "-y") {64			return { field: gradio.props.y, order: "descending" };65		} else if (_sort === null) {66			return null;67		} else if (Array.isArray(_sort)) {68			return _sort;69		}70	}71	let _sort = $derived(reformat_sort(gradio.props.sort));72 73	let _data: {74		[x: string]: string | number;75	}[] = $state([]);76 77	function escape_field_name(fieldName: string): string {78		// Escape special characters in field names according to Vega-Lite spec:79		// https://vega.github.io/vega-lite/docs/field.html80		return fieldName81			.replace(/\./g, "\\.")82			.replace(/\[/g, "\\[")83			.replace(/\]/g, "\\]");84	}85 86	let x_temporal = $derived(87		gradio.props.value &&88			gradio.props.value.datatypes[gradio.props.x] === "temporal"89	);90	let _x_lim = $derived(91		x_temporal92			? [93					x_start !== undefined ? x_start * 1000 : null,94					x_end !== undefined ? x_end * 1000 : null95				]96			: x_lim97	);98	let mouse_down_on_chart = $state(false);99	const SUFFIX_DURATION: Record<string, number> = {100		s: 1,101		m: 60,102		h: 60 * 60,103		d: 24 * 60 * 60104	};105	let _x_bin = $derived(106		gradio.props.x_bin107			? typeof gradio.props.x_bin === "string"108				? 1000 *109					parseInt(110						gradio.props.x_bin.substring(0, gradio.props.x_bin.length - 1)111					) *112					SUFFIX_DURATION[gradio.props.x_bin[gradio.props.x_bin.length - 1]]113				: gradio.props.x_bin114			: undefined115	);116 117	let _y_aggregate = $derived.by(() => {118		if (gradio.props.value) {119			if (gradio.props.value.mark === "point") {120				const aggregating = _x_bin !== undefined;121				return gradio.props.y_aggregate || aggregating ? "sum" : undefined;122			} else {123				return gradio.props.y_aggregate ? gradio.props.y_aggregate : "sum";124			}125		}126		return undefined;127	});128 129	let aggregating = $derived.by(() => {130		if (gradio.props.value) {131			if (gradio.props.value.mark === "point") {132				return _x_bin !== undefined;133			} else {134				return (135					_x_bin !== undefined ||136					gradio.props.value.datatypes[gradio.props.x] === "nominal"137				);138			}139		}140		return false;141	});142 143	function downsample(144		data: PlotData["data"],145		x_index: number,146		y_index: number,147		color_index: number | null,148		x_start: number | undefined,149		x_end: number | undefined150	): PlotData["data"] {151		if (152			data.length < 1000 ||153			gradio.props.x_bin !== null ||154			gradio.props.value?.mark !== "line" ||155			gradio.props.value?.datatypes[gradio.props.x] === "nominal"156		) {157			return data;158		}159		const bin_count = 250;160		let min_max_bins_per_color: Record<161			string,162			[number | null, number, number | null, number][]163		> = {};164		if (x_start === undefined || x_end === undefined) {165			data.forEach((row) => {166				let x_value = row[x_index] as number;167				if (x_start === undefined || x_value < x_start) {168					x_start = x_value;169				}170				if (x_end === undefined || x_value > x_end) {171					x_end = x_value;172				}173			});174		}175		if (x_start === undefined || x_end === undefined) {176			return data;177		}178		const x_range = x_end - x_start;179		const bin_size = x_range / bin_count;180		data.forEach((row, i) => {181			const x_value = row[x_index] as number;182			const y_value = row[y_index] as number;183			const color_value =184				color_index !== null ? (row[color_index] as string) : "any";185			const bin_index = Math.floor((x_value - (x_start as number)) / bin_size);186			if (min_max_bins_per_color[color_value] === undefined) {187				min_max_bins_per_color[color_value] = [];188			}189			min_max_bins_per_color[color_value][bin_index] = min_max_bins_per_color[190				color_value191			][bin_index] || [192				null,193				Number.POSITIVE_INFINITY,194				null,195				Number.NEGATIVE_INFINITY196			];197			if (y_value < min_max_bins_per_color[color_value][bin_index][1]) {198				min_max_bins_per_color[color_value][bin_index][0] = i;199				min_max_bins_per_color[color_value][bin_index][1] = y_value;200			}201			if (y_value > min_max_bins_per_color[color_value][bin_index][3]) {202				min_max_bins_per_color[color_value][bin_index][2] = i;203				min_max_bins_per_color[color_value][bin_index][3] = y_value;204			}205		});206		const downsampled_data: PlotData["data"] = [];207		Object.values(min_max_bins_per_color).forEach((bins) => {208			bins.forEach(([min_index, _, max_index, __]) => {209				let indices: number[] = [];210				if (min_index !== null && max_index !== null) {211					indices = [212						Math.min(min_index, max_index),213						Math.max(min_index, max_index)214					];215				} else if (min_index !== null) {216					indices = [min_index];217				} else if (max_index !== null) {218					indices = [max_index];219				}220				indices.forEach((index) => {221					downsampled_data.push(data[index]);222				});223			});224		});225		return downsampled_data;226	}227	function reformat_data(228		data: PlotData,229		x_start: number | undefined,230		x_end: number | undefined231	): {232		[x: string]: string | number;233	}[] {234		let x_index = data.columns.indexOf(gradio.props.x);235		let y_index = data.columns.indexOf(gradio.props.y);236		let color_index = gradio.props.color237			? data.columns.indexOf(gradio.props.color)238			: null;239		let datatable = data.data;240 241		if (x_start !== undefined && x_end !== undefined) {242			const time_factor =243				data.datatypes[gradio.props.x] === "temporal" ? 1000 : 1;244			const _x_start = x_start * time_factor;245			const _x_end = x_end * time_factor;246			let largest_before_start: Record<string, [number, number]> = {};247			let smallest_after_end: Record<string, [number, number]> = {};248			const _datatable = datatable.filter((row, i) => {249				const x_value = row[x_index] as number;250				const color_value =251					color_index !== null ? (row[color_index] as string) : "any";252				if (253					x_value < _x_start &&254					(largest_before_start[color_value] === undefined ||255						x_value > largest_before_start[color_value][1])256				) {257					largest_before_start[color_value] = [i, x_value];258				}259				if (260					x_value > _x_end &&261					(smallest_after_end[color_value] === undefined ||262						x_value < smallest_after_end[color_value][1])263				) {264					smallest_after_end[color_value] = [i, x_value];265				}266				return x_value >= _x_start && x_value <= _x_end;267			});268			datatable = [269				...Object.values(largest_before_start).map(([i, _]) => datatable[i]),270				...downsample(271					_datatable,272					x_index,273					y_index,274					color_index,275					_x_start,276					_x_end277				),278				...Object.values(smallest_after_end).map(([i, _]) => datatable[i])279			];280		} else {281			datatable = downsample(282				datatable,283				x_index,284				y_index,285				color_index,286				undefined,287				undefined288			);289		}290 291		if (gradio.props.tooltip == "all" || Array.isArray(gradio.props.tooltip)) {292			return datatable.map((row) => {293				const obj: { [x: string]: string | number } = {};294				data.columns.forEach((col, i) => {295					obj[col] = row[i];296				});297				return obj;298			});299		}300		return datatable.map((row) => {301			const obj = {302				[gradio.props.x]: row[x_index],303				[gradio.props.y]: row[y_index]304			};305			if (gradio.props.color && color_index !== null) {306				obj[gradio.props.color] = row[color_index];307			}308			return obj;309		});310	}311 312	$effect(() => {313		_data = gradio.props.value314			? reformat_data(gradio.props.value, x_start, x_end)315			: [];316	});317 318	let old_value = $state<PlotData | null>(gradio.props.value);319	$effect(() => {320		if (old_value !== gradio.props.value && view) {321			old_value = gradio.props.value;322			view.data("data", _data).runAsync();323		}324	});325 326	const is_browser = typeof window !== "undefined";327	let chart_element = $state<HTMLDivElement>();328	let computed_style = $derived(329		chart_element ? window.getComputedStyle(chart_element) : null330	);331	let view = $state<View>();332	let mounted = $state(false);333	let old_width = $state<number>(0);334	let old_height = $state<number>(0);335	let resizeObserver = $state<ResizeObserver>();336 337	let vegaEmbed: typeof import("vega-embed").default;338 339	async function load_chart(): Promise<void> {340		if (mouse_down_on_chart) {341			refresh_pending = true;342			return;343		}344		if (view) {345			view.finalize();346		}347		if (!gradio.props.value || !chart_element) return;348		old_width = chart_element.offsetWidth;349		old_height = chart_element.offsetHeight;350		const spec = create_vega_lite_spec();351		if (!spec) return;352		resizeObserver = new ResizeObserver((el) => {353			if (!el[0].target || !(el[0].target instanceof HTMLElement)) return;354			if (355				old_width === 0 &&356				chart_element!.offsetWidth !== 0 &&357				gradio.props.value!.datatypes[gradio.props.x] === "nominal"358			) {359				// a bug where when a nominal chart is first loaded, the width is 0, it doesn't resize360				load_chart();361			} else {362				const width_change = Math.abs(old_width - el[0].target.offsetWidth);363				const height_change = Math.abs(old_height - el[0].target.offsetHeight);364				if (width_change > 100 || height_change > 100) {365					old_width = el[0].target.offsetWidth;366					old_height = el[0].target.offsetHeight;367					load_chart();368				} else {369					view.signal("width", el[0].target.offsetWidth).run();370					if (fullscreen) {371						view.signal("height", el[0].target.offsetHeight).run();372					}373				}374			}375		});376 377		if (!vegaEmbed) {378			vegaEmbed = (await import("vega-embed")).default;379		}380		vegaEmbed(chart_element, spec, { actions: false }).then(function (result) {381			view = result.view;382			resizeObserver!.observe(chart_element!);383			var debounceTimeout: NodeJS.Timeout;384			var lastSelectTime = 0;385			view.addEventListener("dblclick", () => {386				gradio.dispatch("double_click");387			});388			// prevent double-clicks from highlighting text389			chart_element!.addEventListener(390				"mousedown",391				function (e) {392					if (e.detail > 1) {393						e.preventDefault();394					}395				},396				false397			);398			if (gradio.props._selectable) {399				view.addSignalListener("brush", function (_, value) {400					if (Date.now() - lastSelectTime < 1000) return;401					mouse_down_on_chart = true;402					if (Object.keys(value).length === 0) return;403					clearTimeout(debounceTimeout);404					let range: [number, number] = value[Object.keys(value)[0]];405					if (x_temporal) {406						range = [range[0] / 1000, range[1] / 1000];407					}408					debounceTimeout = setTimeout(function () {409						mouse_down_on_chart = false;410						lastSelectTime = Date.now();411						gradio.dispatch("select", {412							value: range,413							index: range,414							selected: true415						});416						if (refresh_pending) {417							refresh_pending = false;418							load_chart();419						}420					}, 250);421				});422			}423		});424	}425 426	let refresh_pending = $state(false);427 428	onMount(() => {429		mounted = true;430		return () => {431			mounted = false;432			if (view) {433				view.finalize();434			}435			if (resizeObserver) {436				resizeObserver.disconnect();437			}438		};439	});440 441	function export_chart(): void {442		if (!view || !computed_style) return;443 444		const block_background = computed_style.getPropertyValue(445			"--block-background-fill"446		);447		const export_background = block_background || "white";448 449		view.background(export_background).run();450 451		view452			.toImageURL("png", 2)453			.then(function (url) {454				view.background("transparent").run();455 456				const link = document.createElement("a");457				link.setAttribute("href", url);458				link.setAttribute("download", "chart.png");459				link.style.display = "none";460				document.body.appendChild(link);461				link.click();462				document.body.removeChild(link);463			})464			.catch(function (err) {465				console.error("Export failed:", err);466				view.background("transparent").run();467			});468	}469 470	let _color_map = $derived(JSON.stringify(gradio.props.color_map));471 472	$effect(() => {473		// Track dependencies to trigger chart reload474		void gradio.props.title;475		void gradio.props.x_title;476		void gradio.props.y_title;477		void gradio.props.color_title;478		void gradio.props.x;479		void gradio.props.y;480		void gradio.props.color;481		void gradio.props.x_bin;482		void _y_aggregate;483		void _color_map;484		void gradio.props.colors_in_legend;485		void x_start;486		void x_end;487		void y_start;488		void y_end;489		void gradio.props.caption;490		void gradio.props.sort;491		void mounted;492		void chart_element;493		void fullscreen;494		void computed_style;495 496		if (mounted && chart_element) {497			untrack(() => {498				load_chart();499			});500		}501	});502 503	function create_vega_lite_spec(): Spec | null {504		if (!gradio.props.value || !computed_style) return null;505		let accent_color = computed_style.getPropertyValue("--color-accent");506		let body_text_color = computed_style.getPropertyValue("--body-text-color");507		let borderColorPrimary = computed_style.getPropertyValue(508			"--border-color-primary"509		);510		let font_family = computed_style.fontFamily;511		let title_weight = computed_style.getPropertyValue(512			"--block-title-text-weight"513		) as514			| "bold"515			| "normal"516			| 100517			| 200518			| 300519			| 400520			| 500521			| 600522			| 700523			| 800524			| 900;525		const font_to_px_val = (font: string): number => {526			return font.endsWith("px") ? parseFloat(font.slice(0, -2)) : 12;527		};528		let text_size_md = font_to_px_val(529			computed_style.getPropertyValue("--text-md")530		);531		let text_size_sm = font_to_px_val(532			computed_style.getPropertyValue("--text-sm")533		);534 535		/* eslint-disable complexity */536		return {537			$schema: "https://vega.github.io/schema/vega-lite/v5.17.0.json",538			background: "transparent",539			config: {540				autosize: { type: "fit", contains: "padding" },541				axis: {542					labelFont: font_family,543					labelColor: body_text_color,544					titleFont: font_family,545					titleColor: body_text_color,546					titlePadding: 8,547					tickColor: borderColorPrimary,548					labelFontSize: text_size_sm,549					gridColor: borderColorPrimary,550					titleFontWeight: "normal",551					titleFontSize: text_size_sm,552					labelFontWeight: "normal",553					domain: false,554					labelAngle: 0,555					titleLimit: chart_element.offsetHeight * 0.8556				},557				legend: {558					labelColor: body_text_color,559					labelFont: font_family,560					titleColor: body_text_color,561					titleFont: font_family,562					titleFontWeight: "normal",563					titleFontSize: text_size_sm,564					labelFontWeight: "normal",565					offset: 2566				},567				title: {568					color: body_text_color,569					font: font_family,570					fontSize: text_size_md,571					fontWeight: title_weight,572					anchor: "middle"573				},574				view: { stroke: borderColorPrimary },575				mark: {576					stroke: gradio.props.value.mark !== "bar" ? accent_color : undefined,577					fill: gradio.props.value.mark === "bar" ? accent_color : undefined,578					cursor: "crosshair"579				}580			},581			data: { name: "data" },582			datasets: {583				data: _data584			},585			layer: [586				"plot",587				...(gradio.props.value.mark === "line" ? ["hover"] : [])588			].map((mode) => {589				return {590					encoding: {591						size:592							gradio.props.value!.mark === "line"593								? mode == "plot"594									? {595											condition: {596												empty: false,597												param: "hoverPlot",598												value: 3599											},600											value: 2601										}602									: {603											condition: { empty: false, param: "hover", value: 100 },604											value: 0605										}606								: undefined,607						opacity:608							mode === "plot"609								? undefined610								: {611										condition: { empty: false, param: "hover", value: 1 },612										value: 0613									},614						x: {615							axis: {616								...(gradio.props.x_label_angle !== null && {617									labelAngle: gradio.props.x_label_angle618								}),619								labels: gradio.props.x_axis_labels_visible,620								ticks: gradio.props.x_axis_labels_visible,621								...(gradio.props.x_axis_format !== null && {622									format: gradio.props.x_axis_format623								})624							},625							field: escape_field_name(gradio.props.x),626							title: gradio.props.x_title || gradio.props.x,627							type: gradio.props.value!.datatypes[gradio.props.x],628							scale: {629								zero: false,630								domainMin: _x_lim?.[0] !== null ? _x_lim?.[0] : undefined,631								domainMax: _x_lim?.[1] !== null ? _x_lim?.[1] : undefined632							},633							bin: _x_bin ? { step: _x_bin } : undefined,634							sort: _sort635						},636						y: {637							axis: {638								...(gradio.props.y_label_angle && {639									labelAngle: gradio.props.y_label_angle640								}),641								...(gradio.props.y_axis_format !== null && {642									format: gradio.props.y_axis_format643								})644							},645							field: escape_field_name(gradio.props.y),646							title: gradio.props.y_title || gradio.props.y,647							type: gradio.props.value!.datatypes[gradio.props.y],648							scale: {649								zero: false,650								domainMin: y_start ?? undefined,651								domainMax: y_end ?? undefined652							},653							aggregate: aggregating ? _y_aggregate : undefined654						},655						color: gradio.props.color656							? {657									field: escape_field_name(gradio.props.color),658									legend: {659										orient: "bottom",660										title: gradio.props.color_title,661										values: gradio.props.colors_in_legend?.length662											? [...gradio.props.colors_in_legend]663											: undefined664									},665									scale:666										gradio.props.value!.datatypes[gradio.props.color] ===667										"nominal"668											? {669													domain: unique_colors,670													range: gradio.props.color_map671														? unique_colors.map(672																(c) => gradio.props.color_map![c]673															)674														: undefined675												}676											: {677													range: [678														100, 200, 300, 400, 500, 600, 700, 800, 900679													].map((n) =>680														computed_style!.getPropertyValue("--primary-" + n)681													),682													interpolate: "hsl"683												},684									type: gradio.props.value!.datatypes[gradio.props.color]685								}686							: undefined,687						tooltip:688							gradio.props.tooltip == "none"689								? undefined690								: [691										{692											field: escape_field_name(gradio.props.y),693											type: gradio.props.value!.datatypes[gradio.props.y],694											aggregate: aggregating ? _y_aggregate : undefined,695											title: gradio.props.y_title || gradio.props.y696										},697										{698											field: escape_field_name(gradio.props.x),699											type: gradio.props.value!.datatypes[gradio.props.x],700											title: gradio.props.x_title || gradio.props.x,701											format: x_temporal ? "%Y-%m-%d %H:%M:%S" : undefined,702											bin: _x_bin ? { step: _x_bin } : undefined703										},704										...(gradio.props.color705											? [706													{707														field: gradio.props.color,708														type: gradio.props.value!.datatypes[709															gradio.props.color710														]711													}712												]713											: []),714										...(gradio.props.tooltip === "axis"715											? []716											: gradio.props.value?.columns717													.filter(718														(col) =>719															col !== gradio.props.x &&720															col !== gradio.props.y &&721															col !== gradio.props.color &&722															(gradio.props.tooltip === "all" ||723																gradio.props.tooltip.includes(col))724													)725													.map((column) => ({726														field: column,727														type: gradio.props.value!.datatypes[column]728													})))729									]730					},731					strokeDash: {},732					mark: {733						clip: true,734						type: mode === "hover" ? "point" : gradio.props.value.mark735					},736					name: mode737				};738			}),739			// @ts-ignore740			params: [741				...(gradio.props.value!.mark === "line"742					? [743							{744								name: "hoverPlot",745								select: {746									clear: "mouseout",747									fields: gradio.props.color ? [gradio.props.color] : [],748									nearest: true,749									on: "mouseover",750									type: "point" as "point"751								},752								views: ["hover"]753							},754							{755								name: "hover",756								select: {757									clear: "mouseout",758									nearest: true,759									on: "mouseover",760									type: "point" as "point"761								},762								views: ["hover"]763							}764						]765					: []),766				...(gradio.props._selectable767					? [768							{769								name: "brush",770								select: {771									encodings: ["x"],772									mark: { fill: "gray", fillOpacity: 0.3, stroke: "none" },773									type: "interval" as "interval"774								},775								views: ["plot"]776							}777						]778					: [])779			],780			width: chart_element!.offsetWidth,781			height: gradio.props.height || fullscreen ? "container" : undefined,782			title: gradio.props.title || undefined783		} as Spec;784	}785	/* eslint-enable complexity */786	$inspect("visible", gradio.shared.visible);787</script>788 789<Block790	visible={gradio.shared.visible}791	elem_id={gradio.shared.elem_id}792	elem_classes={gradio.shared.elem_classes}793	scale={gradio.shared.scale}794	min_width={gradio.shared.min_width}795	allow_overflow={false}796	padding={true}797	height={gradio.props.height}798	bind:fullscreen799>800	{#if gradio.shared.loading_status}801		<StatusTracker802			autoscroll={gradio.shared.autoscroll}803			i18n={gradio.i18n}804			{...gradio.shared.loading_status}805			on_clear_status={() =>806				gradio.dispatch("clear_status", gradio.shared.loading_status)}807		/>808	{/if}809	{#if gradio.props.buttons?.length}810		<IconButtonWrapper811			buttons={gradio.props.buttons}812			on_custom_button_click={(id) => {813				gradio.dispatch("custom_button_click", { id });814			}}815		>816			{#if gradio.props.buttons?.some((btn) => typeof btn === "string" && btn === "export")}817				<IconButton Icon={Download} label="Export" onclick={export_chart} />818			{/if}819			{#if gradio.props.buttons?.some((btn) => typeof btn === "string" && btn === "fullscreen")}820				<FullscreenButton821					{fullscreen}822					on:fullscreen={({ detail }) => {823						fullscreen = detail;824					}}825				/>826			{/if}827		</IconButtonWrapper>828	{/if}829	<BlockTitle show_label={gradio.shared.show_label} info={undefined}830		>{gradio.shared.label}</BlockTitle831	>832 833	{#if gradio.props.value && is_browser}834		<div bind:this={chart_element}></div>835 836		{#if gradio.props.caption}837			<p class="caption">{gradio.props.caption}</p>838		{/if}839	{:else}840		<Empty unpadded_box={true}><LabelIcon /></Empty>841	{/if}842</Block>843 844<style>845	div {846		width: 100%;847		height: 100%;848	}849	:global(#vg-tooltip-element) {850		font-family: var(--font) !important;851		font-size: var(--text-xs) !important;852		box-shadow: none !important;853		background-color: var(--block-background-fill) !important;854		border: 1px solid var(--border-color-primary) !important;855		color: var(--body-text-color) !important;856	}857	:global(#vg-tooltip-element .key) {858		color: var(--body-text-color-subdued) !important;859	}860	.caption {861		padding: 0 4px;862		margin: 0;863		text-align: center;864	}865</style>866 
gradio/frontend · CoolFace