CoolFace
Datasetpublic

gradio/frontend

sourceHugging Faceupdated 17h agoView on Hugging Face
1likes442kdownloads
Index.svelte844 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(24		gradio.props.color &&25			gradio.props.value &&26			gradio.props.value.datatypes[gradio.props.color] === "nominal"27			? Array.from(new Set(_data.map((d) => d[gradio.props.color!])))28			: []29	);30 31	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 needed32	let y_lim = $derived(gradio.props.y_lim || null);33	let x_start = $derived(x_lim?.[0] !== null ? x_lim?.[0] : undefined);34	let x_end = $derived(x_lim?.[1] !== null ? x_lim?.[1] : undefined);35	let y_start = $derived(y_lim?.[0] !== null ? y_lim?.[0] : undefined);36	let y_end = $derived(y_lim?.[1] !== null ? y_lim?.[1] : undefined);37 38	let fullscreen = $state(false);39 40	function reformat_sort(41		_sort: NativePlotProps["sort"]42	):43		| string44		| "ascending"45		| "descending"46		| { field: string; order: "ascending" | "descending" }47		| string[]48		| null49		| undefined {50		if (_sort === "x") {51			return "ascending";52		} else if (_sort === "-x") {53			return "descending";54		} else if (_sort === "y") {55			return { field: gradio.props.y, order: "ascending" };56		} else if (_sort === "-y") {57			return { field: gradio.props.y, order: "descending" };58		} else if (_sort === null) {59			return null;60		} else if (Array.isArray(_sort)) {61			return _sort;62		}63	}64	let _sort = $derived(reformat_sort(gradio.props.sort));65 66	let _data: {67		[x: string]: string | number;68	}[] = $state([]);69 70	function escape_field_name(fieldName: string): string {71		// Escape special characters in field names according to Vega-Lite spec:72		// https://vega.github.io/vega-lite/docs/field.html73		return fieldName74			.replace(/\./g, "\\.")75			.replace(/\[/g, "\\[")76			.replace(/\]/g, "\\]");77	}78 79	let x_temporal = $derived(80		gradio.props.value &&81			gradio.props.value.datatypes[gradio.props.x] === "temporal"82	);83	let _x_lim = $derived(84		x_temporal85			? [86					x_start !== undefined ? x_start * 1000 : null,87					x_end !== undefined ? x_end * 1000 : null88				]89			: x_lim90	);91	let mouse_down_on_chart = $state(false);92	const SUFFIX_DURATION: Record<string, number> = {93		s: 1,94		m: 60,95		h: 60 * 60,96		d: 24 * 60 * 6097	};98	let _x_bin = $derived(99		gradio.props.x_bin100			? typeof gradio.props.x_bin === "string"101				? 1000 *102					parseInt(103						gradio.props.x_bin.substring(0, gradio.props.x_bin.length - 1)104					) *105					SUFFIX_DURATION[gradio.props.x_bin[gradio.props.x_bin.length - 1]]106				: gradio.props.x_bin107			: undefined108	);109 110	let _y_aggregate = $derived.by(() => {111		if (gradio.props.value) {112			if (gradio.props.value.mark === "point") {113				const aggregating = _x_bin !== undefined;114				return gradio.props.y_aggregate || aggregating ? "sum" : undefined;115			} else {116				return gradio.props.y_aggregate ? gradio.props.y_aggregate : "sum";117			}118		}119		return undefined;120	});121 122	let aggregating = $derived.by(() => {123		if (gradio.props.value) {124			if (gradio.props.value.mark === "point") {125				return _x_bin !== undefined;126			} else {127				return (128					_x_bin !== undefined ||129					gradio.props.value.datatypes[gradio.props.x] === "nominal"130				);131			}132		}133		return false;134	});135 136	function downsample(137		data: PlotData["data"],138		x_index: number,139		y_index: number,140		color_index: number | null,141		x_start: number | undefined,142		x_end: number | undefined143	): PlotData["data"] {144		if (145			data.length < 1000 ||146			gradio.props.x_bin !== null ||147			gradio.props.value?.mark !== "line" ||148			gradio.props.value?.datatypes[gradio.props.x] === "nominal"149		) {150			return data;151		}152		const bin_count = 250;153		let min_max_bins_per_color: Record<154			string,155			[number | null, number, number | null, number][]156		> = {};157		if (x_start === undefined || x_end === undefined) {158			data.forEach((row) => {159				let x_value = row[x_index] as number;160				if (x_start === undefined || x_value < x_start) {161					x_start = x_value;162				}163				if (x_end === undefined || x_value > x_end) {164					x_end = x_value;165				}166			});167		}168		if (x_start === undefined || x_end === undefined) {169			return data;170		}171		const x_range = x_end - x_start;172		const bin_size = x_range / bin_count;173		data.forEach((row, i) => {174			const x_value = row[x_index] as number;175			const y_value = row[y_index] as number;176			const color_value =177				color_index !== null ? (row[color_index] as string) : "any";178			const bin_index = Math.floor((x_value - (x_start as number)) / bin_size);179			if (min_max_bins_per_color[color_value] === undefined) {180				min_max_bins_per_color[color_value] = [];181			}182			min_max_bins_per_color[color_value][bin_index] = min_max_bins_per_color[183				color_value184			][bin_index] || [185				null,186				Number.POSITIVE_INFINITY,187				null,188				Number.NEGATIVE_INFINITY189			];190			if (y_value < min_max_bins_per_color[color_value][bin_index][1]) {191				min_max_bins_per_color[color_value][bin_index][0] = i;192				min_max_bins_per_color[color_value][bin_index][1] = y_value;193			}194			if (y_value > min_max_bins_per_color[color_value][bin_index][3]) {195				min_max_bins_per_color[color_value][bin_index][2] = i;196				min_max_bins_per_color[color_value][bin_index][3] = y_value;197			}198		});199		const downsampled_data: PlotData["data"] = [];200		Object.values(min_max_bins_per_color).forEach((bins) => {201			bins.forEach(([min_index, _, max_index, __]) => {202				let indices: number[] = [];203				if (min_index !== null && max_index !== null) {204					indices = [205						Math.min(min_index, max_index),206						Math.max(min_index, max_index)207					];208				} else if (min_index !== null) {209					indices = [min_index];210				} else if (max_index !== null) {211					indices = [max_index];212				}213				indices.forEach((index) => {214					downsampled_data.push(data[index]);215				});216			});217		});218		return downsampled_data;219	}220	function reformat_data(221		data: PlotData,222		x_start: number | undefined,223		x_end: number | undefined224	): {225		[x: string]: string | number;226	}[] {227		let x_index = data.columns.indexOf(gradio.props.x);228		let y_index = data.columns.indexOf(gradio.props.y);229		let color_index = gradio.props.color230			? data.columns.indexOf(gradio.props.color)231			: null;232		let datatable = data.data;233 234		if (x_start !== undefined && x_end !== undefined) {235			const time_factor =236				data.datatypes[gradio.props.x] === "temporal" ? 1000 : 1;237			const _x_start = x_start * time_factor;238			const _x_end = x_end * time_factor;239			let largest_before_start: Record<string, [number, number]> = {};240			let smallest_after_end: Record<string, [number, number]> = {};241			const _datatable = datatable.filter((row, i) => {242				const x_value = row[x_index] as number;243				const color_value =244					color_index !== null ? (row[color_index] as string) : "any";245				if (246					x_value < _x_start &&247					(largest_before_start[color_value] === undefined ||248						x_value > largest_before_start[color_value][1])249				) {250					largest_before_start[color_value] = [i, x_value];251				}252				if (253					x_value > _x_end &&254					(smallest_after_end[color_value] === undefined ||255						x_value < smallest_after_end[color_value][1])256				) {257					smallest_after_end[color_value] = [i, x_value];258				}259				return x_value >= _x_start && x_value <= _x_end;260			});261			datatable = [262				...Object.values(largest_before_start).map(([i, _]) => datatable[i]),263				...downsample(264					_datatable,265					x_index,266					y_index,267					color_index,268					_x_start,269					_x_end270				),271				...Object.values(smallest_after_end).map(([i, _]) => datatable[i])272			];273		} else {274			datatable = downsample(275				datatable,276				x_index,277				y_index,278				color_index,279				undefined,280				undefined281			);282		}283 284		if (gradio.props.tooltip == "all" || Array.isArray(gradio.props.tooltip)) {285			return datatable.map((row) => {286				const obj: { [x: string]: string | number } = {};287				data.columns.forEach((col, i) => {288					obj[col] = row[i];289				});290				return obj;291			});292		}293		return datatable.map((row) => {294			const obj = {295				[gradio.props.x]: row[x_index],296				[gradio.props.y]: row[y_index]297			};298			if (gradio.props.color && color_index !== null) {299				obj[gradio.props.color] = row[color_index];300			}301			return obj;302		});303	}304 305	$effect(() => {306		_data = gradio.props.value307			? reformat_data(gradio.props.value, x_start, x_end)308			: [];309	});310 311	let old_value = $state<PlotData | null>(gradio.props.value);312	$effect(() => {313		if (old_value !== gradio.props.value && view) {314			old_value = gradio.props.value;315			view.data("data", _data).runAsync();316		}317	});318 319	const is_browser = typeof window !== "undefined";320	let chart_element = $state<HTMLDivElement>();321	let computed_style = $derived(322		chart_element ? window.getComputedStyle(chart_element) : null323	);324	let view = $state<View>();325	let mounted = $state(false);326	let old_width = $state<number>(0);327	let old_height = $state<number>(0);328	let resizeObserver = $state<ResizeObserver>();329 330	let vegaEmbed: typeof import("vega-embed").default;331 332	async function load_chart(): Promise<void> {333		if (mouse_down_on_chart) {334			refresh_pending = true;335			return;336		}337		if (view) {338			view.finalize();339		}340		if (!gradio.props.value || !chart_element) return;341		old_width = chart_element.offsetWidth;342		old_height = chart_element.offsetHeight;343		const spec = create_vega_lite_spec();344		if (!spec) return;345		resizeObserver = new ResizeObserver((el) => {346			if (!el[0].target || !(el[0].target instanceof HTMLElement)) return;347			if (348				old_width === 0 &&349				chart_element!.offsetWidth !== 0 &&350				gradio.props.value!.datatypes[gradio.props.x] === "nominal"351			) {352				// a bug where when a nominal chart is first loaded, the width is 0, it doesn't resize353				load_chart();354			} else {355				const width_change = Math.abs(old_width - el[0].target.offsetWidth);356				const height_change = Math.abs(old_height - el[0].target.offsetHeight);357				if (width_change > 100 || height_change > 100) {358					old_width = el[0].target.offsetWidth;359					old_height = el[0].target.offsetHeight;360					load_chart();361				} else {362					view.signal("width", el[0].target.offsetWidth).run();363					if (fullscreen) {364						view.signal("height", el[0].target.offsetHeight).run();365					}366				}367			}368		});369 370		if (!vegaEmbed) {371			vegaEmbed = (await import("vega-embed")).default;372		}373		vegaEmbed(chart_element, spec, { actions: false }).then(function (result) {374			view = result.view;375			resizeObserver!.observe(chart_element!);376			var debounceTimeout: NodeJS.Timeout;377			var lastSelectTime = 0;378			view.addEventListener("dblclick", () => {379				gradio.dispatch("double_click");380			});381			// prevent double-clicks from highlighting text382			chart_element!.addEventListener(383				"mousedown",384				function (e) {385					if (e.detail > 1) {386						e.preventDefault();387					}388				},389				false390			);391			if (gradio.props._selectable) {392				view.addSignalListener("brush", function (_, value) {393					if (Date.now() - lastSelectTime < 1000) return;394					mouse_down_on_chart = true;395					if (Object.keys(value).length === 0) return;396					clearTimeout(debounceTimeout);397					let range: [number, number] = value[Object.keys(value)[0]];398					if (x_temporal) {399						range = [range[0] / 1000, range[1] / 1000];400					}401					debounceTimeout = setTimeout(function () {402						mouse_down_on_chart = false;403						lastSelectTime = Date.now();404						gradio.dispatch("select", {405							value: range,406							index: range,407							selected: true408						});409						if (refresh_pending) {410							refresh_pending = false;411							load_chart();412						}413					}, 250);414				});415			}416		});417	}418 419	let refresh_pending = $state(false);420 421	onMount(() => {422		mounted = true;423		return () => {424			mounted = false;425			if (view) {426				view.finalize();427			}428			if (resizeObserver) {429				resizeObserver.disconnect();430			}431		};432	});433 434	function export_chart(): void {435		if (!view || !computed_style) return;436 437		const block_background = computed_style.getPropertyValue(438			"--block-background-fill"439		);440		const export_background = block_background || "white";441 442		view.background(export_background).run();443 444		view445			.toImageURL("png", 2)446			.then(function (url) {447				view.background("transparent").run();448 449				const link = document.createElement("a");450				link.setAttribute("href", url);451				link.setAttribute("download", "chart.png");452				link.style.display = "none";453				document.body.appendChild(link);454				link.click();455				document.body.removeChild(link);456			})457			.catch(function (err) {458				console.error("Export failed:", err);459				view.background("transparent").run();460			});461	}462 463	let _color_map = $derived(JSON.stringify(gradio.props.color_map));464 465	$effect(() => {466		// Track dependencies to trigger chart reload467		void gradio.props.title;468		void gradio.props.x_title;469		void gradio.props.y_title;470		void gradio.props.color_title;471		void gradio.props.x;472		void gradio.props.y;473		void gradio.props.color;474		void gradio.props.x_bin;475		void _y_aggregate;476		void _color_map;477		void gradio.props.colors_in_legend;478		void x_start;479		void x_end;480		void y_start;481		void y_end;482		void gradio.props.caption;483		void gradio.props.sort;484		void mounted;485		void chart_element;486		void fullscreen;487		void computed_style;488 489		if (mounted && chart_element) {490			untrack(() => {491				load_chart();492			});493		}494	});495 496	function create_vega_lite_spec(): Spec | null {497		if (!gradio.props.value || !computed_style) return null;498		let accent_color = computed_style.getPropertyValue("--color-accent");499		let body_text_color = computed_style.getPropertyValue("--body-text-color");500		let borderColorPrimary = computed_style.getPropertyValue(501			"--border-color-primary"502		);503		let font_family = computed_style.fontFamily;504		let title_weight = computed_style.getPropertyValue(505			"--block-title-text-weight"506		) as507			| "bold"508			| "normal"509			| 100510			| 200511			| 300512			| 400513			| 500514			| 600515			| 700516			| 800517			| 900;518		const font_to_px_val = (font: string): number => {519			return font.endsWith("px") ? parseFloat(font.slice(0, -2)) : 12;520		};521		let text_size_md = font_to_px_val(522			computed_style.getPropertyValue("--text-md")523		);524		let text_size_sm = font_to_px_val(525			computed_style.getPropertyValue("--text-sm")526		);527 528		/* eslint-disable complexity */529		return {530			$schema: "https://vega.github.io/schema/vega-lite/v5.17.0.json",531			background: "transparent",532			config: {533				autosize: { type: "fit", contains: "padding" },534				axis: {535					labelFont: font_family,536					labelColor: body_text_color,537					titleFont: font_family,538					titleColor: body_text_color,539					titlePadding: 8,540					tickColor: borderColorPrimary,541					labelFontSize: text_size_sm,542					gridColor: borderColorPrimary,543					titleFontWeight: "normal",544					titleFontSize: text_size_sm,545					labelFontWeight: "normal",546					domain: false,547					labelAngle: 0,548					titleLimit: chart_element.offsetHeight * 0.8549				},550				legend: {551					labelColor: body_text_color,552					labelFont: font_family,553					titleColor: body_text_color,554					titleFont: font_family,555					titleFontWeight: "normal",556					titleFontSize: text_size_sm,557					labelFontWeight: "normal",558					offset: 2559				},560				title: {561					color: body_text_color,562					font: font_family,563					fontSize: text_size_md,564					fontWeight: title_weight,565					anchor: "middle"566				},567				view: { stroke: borderColorPrimary },568				mark: {569					stroke: gradio.props.value.mark !== "bar" ? accent_color : undefined,570					fill: gradio.props.value.mark === "bar" ? accent_color : undefined,571					cursor: "crosshair"572				}573			},574			data: { name: "data" },575			datasets: {576				data: _data577			},578			layer: [579				"plot",580				...(gradio.props.value.mark === "line" ? ["hover"] : [])581			].map((mode) => {582				return {583					encoding: {584						size:585							gradio.props.value!.mark === "line"586								? mode == "plot"587									? {588											condition: {589												empty: false,590												param: "hoverPlot",591												value: 3592											},593											value: 2594										}595									: {596											condition: { empty: false, param: "hover", value: 100 },597											value: 0598										}599								: undefined,600						opacity:601							mode === "plot"602								? undefined603								: {604										condition: { empty: false, param: "hover", value: 1 },605										value: 0606									},607						x: {608							axis: {609								...(gradio.props.x_label_angle !== null && {610									labelAngle: gradio.props.x_label_angle611								}),612								labels: gradio.props.x_axis_labels_visible,613								ticks: gradio.props.x_axis_labels_visible614							},615							field: escape_field_name(gradio.props.x),616							title: gradio.props.x_title || gradio.props.x,617							type: gradio.props.value!.datatypes[gradio.props.x],618							scale: {619								zero: false,620								domainMin: _x_lim?.[0] !== null ? _x_lim?.[0] : undefined,621								domainMax: _x_lim?.[1] !== null ? _x_lim?.[1] : undefined622							},623							bin: _x_bin ? { step: _x_bin } : undefined,624							sort: _sort625						},626						y: {627							axis: gradio.props.y_label_angle628								? { labelAngle: gradio.props.y_label_angle }629								: {},630							field: escape_field_name(gradio.props.y),631							title: gradio.props.y_title || gradio.props.y,632							type: gradio.props.value!.datatypes[gradio.props.y],633							scale: {634								zero: false,635								domainMin: y_start ?? undefined,636								domainMax: y_end ?? undefined637							},638							aggregate: aggregating ? _y_aggregate : undefined639						},640						color: gradio.props.color641							? {642									field: escape_field_name(gradio.props.color),643									legend: {644										orient: "bottom",645										title: gradio.props.color_title,646										values: [...gradio.props.colors_in_legend] || undefined647									},648									scale:649										gradio.props.value!.datatypes[gradio.props.color] ===650										"nominal"651											? {652													domain: unique_colors,653													range: gradio.props.color_map654														? unique_colors.map(655																(c) => gradio.props.color_map![c]656															)657														: undefined658												}659											: {660													range: [661														100, 200, 300, 400, 500, 600, 700, 800, 900662													].map((n) =>663														computed_style!.getPropertyValue("--primary-" + n)664													),665													interpolate: "hsl"666												},667									type: gradio.props.value!.datatypes[gradio.props.color]668								}669							: undefined,670						tooltip:671							gradio.props.tooltip == "none"672								? undefined673								: [674										{675											field: escape_field_name(gradio.props.y),676											type: gradio.props.value!.datatypes[gradio.props.y],677											aggregate: aggregating ? _y_aggregate : undefined,678											title: gradio.props.y_title || gradio.props.y679										},680										{681											field: escape_field_name(gradio.props.x),682											type: gradio.props.value!.datatypes[gradio.props.x],683											title: gradio.props.x_title || gradio.props.x,684											format: x_temporal ? "%Y-%m-%d %H:%M:%S" : undefined,685											bin: _x_bin ? { step: _x_bin } : undefined686										},687										...(gradio.props.color688											? [689													{690														field: gradio.props.color,691														type: gradio.props.value!.datatypes[692															gradio.props.color693														]694													}695												]696											: []),697										...(gradio.props.tooltip === "axis"698											? []699											: gradio.props.value?.columns700													.filter(701														(col) =>702															col !== gradio.props.x &&703															col !== gradio.props.y &&704															col !== gradio.props.color &&705															(gradio.props.tooltip === "all" ||706																gradio.props.tooltip.includes(col))707													)708													.map((column) => ({709														field: column,710														type: gradio.props.value!.datatypes[column]711													})))712									]713					},714					strokeDash: {},715					mark: {716						clip: true,717						type: mode === "hover" ? "point" : gradio.props.value.mark718					},719					name: mode720				};721			}),722			// @ts-ignore723			params: [724				...(gradio.props.value!.mark === "line"725					? [726							{727								name: "hoverPlot",728								select: {729									clear: "mouseout",730									fields: gradio.props.color ? [gradio.props.color] : [],731									nearest: true,732									on: "mouseover",733									type: "point" as "point"734								},735								views: ["hover"]736							},737							{738								name: "hover",739								select: {740									clear: "mouseout",741									nearest: true,742									on: "mouseover",743									type: "point" as "point"744								},745								views: ["hover"]746							}747						]748					: []),749				...(gradio.props._selectable750					? [751							{752								name: "brush",753								select: {754									encodings: ["x"],755									mark: { fill: "gray", fillOpacity: 0.3, stroke: "none" },756									type: "interval" as "interval"757								},758								views: ["plot"]759							}760						]761					: [])762			],763			width: chart_element!.offsetWidth,764			height: gradio.props.height || fullscreen ? "container" : undefined,765			title: gradio.props.title || undefined766		} as Spec;767	}768 769	/* eslint-enable complexity */770</script>771 772<Block773	visible={gradio.shared.visible}774	elem_id={gradio.shared.elem_id}775	elem_classes={gradio.shared.elem_classes}776	scale={gradio.shared.scale}777	min_width={gradio.shared.min_width}778	allow_overflow={false}779	padding={true}780	height={gradio.props.height}781	bind:fullscreen782>783	{#if gradio.shared.loading_status}784		<StatusTracker785			autoscroll={gradio.shared.autoscroll}786			i18n={gradio.i18n}787			{...gradio.shared.loading_status}788			on_clear_status={() =>789				gradio.dispatch("clear_status", gradio.shared.loading_status)}790		/>791	{/if}792	{#if gradio.props.buttons?.length}793		<IconButtonWrapper>794			{#if gradio.props.buttons?.includes("export")}795				<IconButton Icon={Download} label="Export" on:click={export_chart} />796			{/if}797			{#if gradio.props.buttons?.includes("fullscreen")}798				<FullscreenButton799					{fullscreen}800					on:fullscreen={({ detail }) => {801						fullscreen = detail;802					}}803				/>804			{/if}805		</IconButtonWrapper>806	{/if}807	<BlockTitle show_label={gradio.shared.show_label} info={undefined}808		>{gradio.shared.label}</BlockTitle809	>810 811	{#if gradio.props.value && is_browser}812		<div bind:this={chart_element}></div>813 814		{#if gradio.props.caption}815			<p class="caption">{gradio.props.caption}</p>816		{/if}817	{:else}818		<Empty unpadded_box={true}><LabelIcon /></Empty>819	{/if}820</Block>821 822<style>823	div {824		width: 100%;825		height: 100%;826	}827	:global(#vg-tooltip-element) {828		font-family: var(--font) !important;829		font-size: var(--text-xs) !important;830		box-shadow: none !important;831		background-color: var(--block-background-fill) !important;832		border: 1px solid var(--border-color-primary) !important;833		color: var(--body-text-color) !important;834	}835	:global(#vg-tooltip-element .key) {836		color: var(--body-text-color-subdued) !important;837	}838	.caption {839		padding: 0 4px;840		margin: 0;841		text-align: center;842	}843</style>844 
gradio/frontend · CoolFace