gr.DateTime component (#8713)

* changes

* changes

* add changeset

* changes

* changes

* changes

* changes

* changes

* changes

* changes

* changes

* changes

* changes

* changes

* changes

* changes

* changes

* changes

* changes

* changes

* changes

* Update gradio/components/datetime.py

Co-authored-by: Abubakar Abid <abubakar@huggingface.co>

* changes

* 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>
This commit is contained in:
aliabid94 2024-07-10 20:55:45 -07:00 committed by GitHub
parent b736c8db34
commit e3c7079e38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 866 additions and 58 deletions

View File

@ -0,0 +1,8 @@
---
"@gradio/app": minor
"@gradio/icons": minor
"@gradio/plot": minor
"gradio": minor
---
feat:Time range component

View File

@ -0,0 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: datetime_component"]}, {"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 gradio as gr\n", "\n", "with gr.Blocks() as demo:\n", " gr.DateTime()\n", "\n", "demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

View File

@ -0,0 +1,6 @@
import gradio as gr
with gr.Blocks() as demo:
gr.DateTime()
demo.launch()

1
demo/datetimes/run.ipynb Normal file
View File

@ -0,0 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: datetimes"]}, {"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 gradio as gr\n", "\n", "with gr.Blocks() as demo:\n", " date1 = gr.DateTime(include_time=True, label=\"Date and Time\", type=\"datetime\", elem_id=\"date1\")\n", " date2 = gr.DateTime(include_time=False, label=\"Date Only\", type=\"string\", elem_id=\"date2\")\n", " date3 = gr.DateTime(elem_id=\"date3\", timezone=\"Europe/Paris\")\n", "\n", " with gr.Row():\n", " btn1 = gr.Button(\"Load Date 1\")\n", " btn2 = gr.Button(\"Load Date 2\")\n", " btn3 = gr.Button(\"Load Date 3\")\n", "\n", " click_output = gr.Textbox(label=\"Last Load\")\n", " change_output = gr.Textbox(label=\"Last Change\")\n", " submit_output = gr.Textbox(label=\"Last Submit\")\n", "\n", " btn1.click(lambda x:x, date1, click_output)\n", " btn2.click(lambda x:x, date2, click_output)\n", " btn3.click(lambda x:x, date3, click_output)\n", "\n", " for item in [date1, date2, date3]:\n", " item.change(lambda x:x, item, change_output)\n", " item.submit(lambda x:x, item, submit_output)\n", " \n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

27
demo/datetimes/run.py Normal file
View File

@ -0,0 +1,27 @@
import gradio as gr
with gr.Blocks() as demo:
date1 = gr.DateTime(include_time=True, label="Date and Time", type="datetime", elem_id="date1")
date2 = gr.DateTime(include_time=False, label="Date Only", type="string", elem_id="date2")
date3 = gr.DateTime(elem_id="date3", timezone="Europe/Paris")
with gr.Row():
btn1 = gr.Button("Load Date 1")
btn2 = gr.Button("Load Date 2")
btn3 = gr.Button("Load Date 3")
click_output = gr.Textbox(label="Last Load")
change_output = gr.Textbox(label="Last Change")
submit_output = gr.Textbox(label="Last Submit")
btn1.click(lambda x:x, date1, click_output)
btn2.click(lambda x:x, date2, click_output)
btn3.click(lambda x:x, date3, click_output)
for item in [date1, date2, date3]:
item.change(lambda x:x, item, change_output)
item.submit(lambda x:x, item, submit_output)
if __name__ == "__main__":
demo.launch()

View File

@ -65,7 +65,6 @@ def line_plot_fn(dataset):
tooltip=['country', 'life_expect'],
overlay_point=False,
title="Life expectancy for countries",
stroke_dash_legend_title="Country Cluster",
)

View File

@ -26,6 +26,7 @@ from gradio.components import (
DataFrame,
Dataframe,
Dataset,
DateTime,
DownloadButton,
Dropdown,
DuplicateButton,

View File

@ -19,6 +19,7 @@ from gradio.components.code import Code
from gradio.components.color_picker import ColorPicker
from gradio.components.dataframe import Dataframe
from gradio.components.dataset import Dataset
from gradio.components.datetime import DateTime
from gradio.components.download_button import DownloadButton
from gradio.components.dropdown import Dropdown
from gradio.components.duplicate_button import DuplicateButton
@ -95,6 +96,7 @@ __all__ = [
"Markdown",
"MessageDict",
"Textbox",
"DateTime",
"Dropdown",
"Model3D",
"File",

View File

@ -0,0 +1,155 @@
"""gr.DateTime() component."""
from __future__ import annotations
import re
from datetime import datetime, timedelta
from typing import Any, Literal
import pytz
from gradio_client.documentation import document
from gradio.components.base import FormComponent
from gradio.events import Events
@document()
class DateTime(FormComponent):
"""
Component to select a date and (optionally) a time.
"""
EVENTS = [
Events.change,
Events.submit,
]
def __init__(
self,
value: float | str | datetime | None = None,
*,
include_time: bool = True,
type: Literal["timestamp", "datetime", "string"] = "timestamp",
timezone: str | None = None,
label: str | None = None,
show_label: bool | None = None,
info: str | None = None,
every: float | None = None,
scale: int | None = None,
min_width: int = 160,
visible: bool = True,
elem_id: str | None = None,
elem_classes: list[str] | str | None = None,
render: bool = True,
key: int | str | None = None,
):
"""
Parameters:
value: default value for datetime.
label: The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.
show_label: if True, will display label.
include_time: If True, the component will include time selection. If False, only date selection will be available.
type: The type of the value. Can be "timestamp", "datetime", or "string". If "timestamp", the value will be a number representing the start and end date in seconds since epoch. If "datetime", the value will be a datetime object. If "string", the value will be the date entered by the user.
timezone: The timezone to use for timestamps, such as "US/Pacific" or "Europe/Paris". If None, the timezone will be the local timezone.
info: additional component description.
every: If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise. The event can be accessed (e.g. to cancel it) via this component's .load_event attribute.
scale: relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.
min_width: minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.
visible: If False, component will be hidden.
elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.
key: if assigned, will be used to assume identity across a re-render. Components that have the same key across a re-render will have their value preserved.
"""
super().__init__(
every=every,
scale=scale,
min_width=min_width,
visible=visible,
label=label,
show_label=show_label,
info=info,
elem_id=elem_id,
elem_classes=elem_classes,
render=render,
key=key,
value=value,
)
self.type = type
self.include_time = include_time
self.time_format = "%Y-%m-%d %H:%M:%S" if include_time else "%Y-%m-%d"
self.timezone = timezone
def preprocess(self, payload: str | None) -> str | float | datetime | None:
"""
Parameters:
payload: the text entered in the textarea.
Returns:
Passes text value as a {str} into the function.
"""
if payload is None or payload == "":
return None
if self.type == "string" and "now" not in payload:
return payload
datetime = self.get_datetime_from_str(payload)
if self.type == "string":
return datetime.strftime(self.time_format)
if self.type == "datetime":
return datetime
elif self.type == "timestamp":
return datetime.timestamp()
def postprocess(self, value: float | datetime | str | None) -> str | None:
"""
Parameters:
value: Expects a tuple pair of datetimes.
Returns:
A tuple pair of timestamps.
"""
if value is None:
return None
if isinstance(value, datetime):
return datetime.strftime(value, self.time_format)
elif isinstance(value, str):
return value
else:
return datetime.fromtimestamp(
value, tz=pytz.timezone(self.timezone) if self.timezone else None
).strftime(self.time_format)
def api_info(self) -> dict[str, Any]:
return {
"type": "string",
"description": f"Formatted as YYYY-MM-DD{' HH:MM:SS' if self.include_time else ''}",
}
def example_payload(self) -> str:
return "2020-10-01 05:20:15"
def example_value(self) -> str:
return "2020-10-01 05:20:15"
def get_datetime_from_str(self, date: str) -> datetime:
now_regex = r"^(?:\s*now\s*(?:-\s*(\d+)\s*([dmhs]))?)?\s*$"
if "now" in date:
match = re.match(now_regex, date)
if match:
num = int(match.group(1) or 0)
unit = match.group(2) or "s"
if unit == "d":
delta = timedelta(days=num)
elif unit == "h":
delta = timedelta(hours=num)
elif unit == "m":
delta = timedelta(minutes=num)
else:
delta = timedelta(seconds=num)
return datetime.now() - delta
else:
raise ValueError("Invalid 'now' time format")
else:
dt = datetime.strptime(date, self.time_format)
if self.timezone:
dt = pytz.timezone(self.timezone).localize(dt)
return dt

View File

@ -9,6 +9,7 @@ from gradio_client.documentation import document
from gradio.components.base import Component
from gradio.components.plot import AltairPlot, AltairPlotData, Plot
from gradio.events import Events
if TYPE_CHECKING:
import pandas as pd
@ -27,6 +28,8 @@ class LinePlot(Plot):
data_model = AltairPlotData
EVENTS = [Events.select]
def __init__(
self,
value: pd.DataFrame | Callable | None = None,
@ -73,7 +76,6 @@ class LinePlot(Plot):
x_lim: list[int] | None = None,
y_lim: list[int] | None = None,
caption: str | None = None,
interactive: bool | None = True,
label: str | None = None,
show_label: bool | None = None,
container: bool = True,
@ -87,17 +89,18 @@ class LinePlot(Plot):
render: bool = True,
key: int | str | None = None,
show_actions_button: bool = False,
interactive: bool | None = None,
):
"""
Parameters:
value: The pandas dataframe containing the data to display in a scatter plot.
x: Column corresponding to the x axis.
y: Column corresponding to the y axis.
x: Column corresponding to the x axis. Can be grouped if datetime, e.g. 'yearmonth(date)' or 'minuteseconds(date)' with a column name 'date'. Any time unit supported by altair can be used.
y: Column corresponding to the y axis. Can be aggregated, e.g. 'sum(price)' or 'count(price)' with a column name 'price'. Any aggregation function supported by altair can be used.
color: The column to determine the point color. If the column contains numeric data, gradio will interpolate the column data so that small values correspond to light colors and large values correspond to dark values.
stroke_dash: The column to determine the symbol used to draw the line, e.g. dashed lines, dashed lines with points.
overlay_point: Whether to draw a point on the line for each (x, y) coordinate pair.
title: The title to display on top of the chart.
tooltip: The column (or list of columns) to display on the tooltip when a user hovers a point on the plot.
tooltip: The column (or list of columns) to display on the tooltip when a user hovers a point on the plot. Set to [] to disable tooltips.
x_title: The title given to the x axis. By default, uses the value of the x parameter.
y_title: The title given to the y axis. By default, uses the value of the y parameter.
x_label_angle: The angle for the x axis labels. Positive values are clockwise, and negative values are counter-clockwise.
@ -111,7 +114,7 @@ class LinePlot(Plot):
x_lim: A tuple or list containing the limits for the x-axis, specified as [x_min, x_max].
y_lim: A tuple of list containing the limits for the y-axis, specified as [y_min, y_max].
caption: The (optional) caption to display below the plot.
interactive: Whether users should be able to interact with the plot by panning or zooming with their mouse or trackpad.
interactive: Deprecated.
label: The (optional) label to display on the top left corner of the plot.
show_label: Whether the label should be displayed.
every: Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.
@ -127,7 +130,9 @@ class LinePlot(Plot):
self.y = y
self.color = color
self.stroke_dash = stroke_dash
self.tooltip = tooltip
self.tooltip = (
tooltip if tooltip is not None else [elem for elem in [x, y, color] if elem]
)
self.title = title
self.x_title = x_title
self.y_title = y_title
@ -141,7 +146,6 @@ class LinePlot(Plot):
self.x_lim = x_lim
self.y_lim = y_lim
self.caption = caption
self.interactive_chart = interactive
if isinstance(width, str):
width = None
warnings.warn(
@ -172,6 +176,11 @@ class LinePlot(Plot):
every=every,
inputs=inputs,
)
if interactive is not None:
warnings.warn(
"The `interactive` parameter is deprecated and will be removed in a future version. "
"The LinePlot component is always interactive."
)
def get_block_name(self) -> str:
return "plot"
@ -220,25 +229,23 @@ class LinePlot(Plot):
width: int | None = None,
x_lim: list[int] | None = None,
y_lim: list[int] | None = None,
interactive: bool | None = None,
):
"""Helper for creating the scatter plot."""
import altair as alt
interactive = True if interactive is None else interactive
encodings = {
"x": alt.X(
x, # type: ignore
title=x_title or x, # type: ignore
scale=AltairPlot.create_scale(x_lim), # type: ignore
x,
title=x_title or x,
scale=AltairPlot.create_scale(x_lim),
axis=alt.Axis(labelAngle=x_label_angle)
if x_label_angle is not None
else alt.Axis(),
),
"y": alt.Y(
y, # type: ignore
title=y_title or y, # type: ignore
scale=AltairPlot.create_scale(y_lim), # type: ignore
y,
title=y_title or y,
scale=AltairPlot.create_scale(y_lim),
axis=alt.Axis(labelAngle=y_label_angle)
if y_label_angle is not None
else alt.Axis(),
@ -265,46 +272,60 @@ class LinePlot(Plot):
),
}
highlight = None
if interactive and any([color, stroke_dash]):
highlight = alt.selection(
type="single", # type: ignore
on="mouseover",
fields=[c for c in [color, stroke_dash] if c],
nearest=True,
)
if stroke_dash:
stroke_dash = {
"field": stroke_dash, # type: ignore
"legend": AltairPlot.create_legend( # type: ignore
position=stroke_dash_legend_position, # type: ignore
title=stroke_dash_legend_title or stroke_dash, # type: ignore
), # type: ignore
} # type: ignore
stroke_dash_encoding = {
"field": stroke_dash,
"legend": AltairPlot.create_legend(
position=stroke_dash_legend_position or "bottom",
title=stroke_dash_legend_title,
),
}
else:
stroke_dash = alt.value(alt.Undefined) # type: ignore
stroke_dash_encoding = alt.value(alt.Undefined)
if tooltip:
encodings["tooltip"] = tooltip
chart = alt.Chart(value).encode(**encodings) # type: ignore
chart = alt.Chart(value).encode(**encodings)
points = chart.mark_point(clip=True).encode(
opacity=alt.value(alt.Undefined) if overlay_point else alt.value(0),
)
lines = chart.mark_line(clip=True).encode(strokeDash=stroke_dash)
lines = chart.mark_line(clip=True).encode(strokeDash=stroke_dash_encoding)
if highlight:
points = points.add_selection(highlight)
highlight = alt.selection_point(
on="mouseover",
fields=[c for c in [color, stroke_dash] if c],
nearest=True,
clear="mouseout",
empty=False,
)
points = points.add_params(highlight)
lines = lines.encode(
size=alt.condition(highlight, alt.value(4), alt.value(2)),
)
if not overlay_point:
highlight_pts = alt.selection_point(
on="mouseover",
nearest=True,
clear="mouseout",
empty=False,
)
points = points.add_params(highlight_pts)
lines = lines.encode(
size=alt.condition(highlight, alt.value(4), alt.value(1)),
points = points.encode(
opacity=alt.condition(highlight_pts, alt.value(1), alt.value(0)),
size=alt.condition(highlight_pts, alt.value(100), alt.value(0)),
)
chart = (lines + points).properties(background="transparent", **properties)
if interactive:
chart = chart.interactive()
selection = alt.selection_interval(
encodings=["x"],
mark=alt.BrushConfig(fill="gray", fillOpacity=0.3, stroke="none"),
name="brush",
)
chart = chart.add_params(selection)
return chart
@ -350,7 +371,6 @@ class LinePlot(Plot):
x_lim=self.x_lim,
y_lim=self.y_lim,
stroke_dash=self.stroke_dash,
interactive=self.interactive_chart,
height=self.height,
width=self.width,
)

View File

@ -532,6 +532,7 @@ class EventListener(str):
event_trigger.event_name = _event_name
event_trigger.has_trigger = _has_trigger
event_trigger.callback = _callback
return event_trigger
@ -600,10 +601,10 @@ def on(
"""
from gradio.components.base import Component
triggers_typed = cast(EventListener, triggers)
if not isinstance(triggers, Sequence) and triggers is not None:
triggers = [triggers]
triggers_typed = cast(Sequence[EventListener], triggers)
if isinstance(triggers_typed, EventListener):
triggers_typed = [triggers_typed]
if isinstance(inputs, Component):
inputs = [inputs]
@ -654,6 +655,10 @@ def on(
EventListenerMethod(t.__self__ if t.has_trigger else None, t.event_name) # type: ignore
for t in triggers_typed
]
if triggers:
for trigger in triggers:
if trigger.callback:
trigger.callback(trigger.__self__)
if every is not None:
from gradio.components import Timer

View File

@ -0,0 +1,89 @@
<script lang="ts">
import {get_object} from "../../process_json.ts";
import ParamTable from "$lib/components/ParamTable.svelte";
import ShortcutTable from "$lib/components/ShortcutTable.svelte";
import DemosSection from "$lib/components/DemosSection.svelte";
import FunctionsSection from "$lib/components/FunctionsSection.svelte";
import GuidesSection from "$lib/components/GuidesSection.svelte";
import CopyButton from "$lib/components/CopyButton.svelte";
import { style_formatted_text } from "$lib/text";
let obj = get_object("textbox");
</script>
<!--- Title -->
# {obj.name}
<!--- Usage -->
```python
gradio.DateTime(···)
```
<!-- Embedded Component -->
<div class="embedded-component">
<gradio-lite shared-worker>
import gradio as gr
with gr.Blocks() as demo:
gr.DateTime()
demo.launch()
</gradio-lite>
</div>
<!--- Description -->
### Description
## {@html style_formatted_text(obj.description)}
<!-- Behavior -->
### Behavior
## **As input component**: {@html style_formatted_text(obj.preprocess.return_doc.doc)}
##### Your function should accept one of these types:
```python
def predict(
value: float | datetime | str | None
)
...
```
<br>
## **As output component**: {@html style_formatted_text(obj.postprocess.parameter_doc[0].doc)}
##### Your function should return one of these types:
```python
def predict(···) -> float | datetime | str | None
...
return value
```
<!--- Initialization -->
### Initialization
<ParamTable parameters={obj.parameters} />
{#if obj.string_shortcuts && obj.string_shortcuts.length > 0}
<!--- Shortcuts -->
### Shortcuts
<ShortcutTable shortcuts={obj.string_shortcuts} />
{/if}
{#if obj.demos && obj.demos.length > 0}
<!--- Demos -->
### Demos
<DemosSection demos={obj.demos} />
{/if}
{#if obj.fns && obj.fns.length > 0}
<!--- Event Listeners -->
### Event Listeners
<FunctionsSection fns={obj.fns} event_listeners={true} />
{/if}
{#if obj.guides && obj.guides.length > 0}
<!--- Guides -->
### Guides
<GuidesSection guides={obj.guides}/>
{/if}

View File

@ -72,6 +72,7 @@
"@gradio/tabs": "workspace:^",
"@gradio/textbox": "workspace:^",
"@gradio/theme": "workspace:^",
"@gradio/datetime": "workspace:^",
"@gradio/timer": "workspace:^",
"@gradio/upload": "workspace:^",
"@gradio/uploadbutton": "workspace:^",

View File

@ -0,0 +1,53 @@
import { test, expect } from "@gradio/tootils";
test("gr.DateTime shows correct values", async ({ page }) => {
await page.locator("#date1").getByRole("textbox").first().click();
await page
.locator("#date1")
.getByRole("textbox")
.first()
.fill("2020-10-01 10:50:00");
await page.locator("body").first().click();
await expect(page.getByLabel("Last Change")).toHaveValue(
"2020-10-01 10:50:00"
);
await expect(page.getByLabel("Last Submit")).toHaveValue("");
await page.locator("#date1").getByRole("textbox").first().press("Enter");
await expect(page.getByLabel("Last Submit")).toHaveValue(
"2020-10-01 10:50:00"
);
await expect(page.getByLabel("Last Load")).toHaveValue("");
await page.locator("#date2").getByRole("textbox").first().click();
await page.locator("#date2").getByRole("textbox").first().fill("2000-02-22");
await page.locator("body").first().click();
await expect(page.getByLabel("Last Change")).toHaveValue("2000-02-22");
await page.getByRole("button", { name: "Load Date 1" }).click();
await expect(page.getByLabel("Last Load")).toHaveValue("2020-10-01 10:50:00");
await page.getByRole("button", { name: "Load Date 2" }).click();
await expect(page.getByLabel("Last Load")).toHaveValue("2000-02-22");
await page.locator("#date2").getByRole("textbox").first().click();
await page
.locator("#date2")
.getByRole("textbox")
.first()
.fill("2020-05-01xxx");
await page.locator("body").first().click();
await expect(page.getByLabel("Last Change")).toHaveValue("2000-02-22");
await page.locator("#date2").getByRole("textbox").first().click();
await page.locator("#date2").getByRole("textbox").first().fill("2020-05-02");
await page.locator("body").first().click();
await expect(page.getByLabel("Last Change")).toHaveValue("2020-05-02");
await page.locator("#date3").getByRole("textbox").first().click();
await page
.locator("#date3")
.getByRole("textbox")
.first()
.fill("2020-10-10 05:01:01");
await page.getByRole("button", { name: "Load Date 3" }).click();
await expect(page.getByLabel("Last Load")).toHaveValue("1602298861.0");
});

3
js/datetime/CHANGELOG.md Normal file
View File

@ -0,0 +1,3 @@
# @gradio/datetime
## 0.1.0

View File

@ -0,0 +1,5 @@
<script lang="ts">
export let value: string | null;
</script>
{value || ""}

204
js/datetime/Index.svelte Normal file
View File

@ -0,0 +1,204 @@
<script context="module" lang="ts">
export { default as BaseExample } from "./Example.svelte";
</script>
<script lang="ts">
import type { Gradio } from "@gradio/utils";
import { Block, BlockTitle } from "@gradio/atoms";
import { Back, Calendar } from "@gradio/icons";
export let gradio: Gradio<{
change: undefined;
submit: undefined;
}>;
export let label = "Time";
export let show_label = true;
export let info: string | undefined = undefined;
export let elem_id = "";
export let elem_classes: string[] = [];
export let visible = true;
export let value = "";
let old_value = value;
export let scale: number | null = null;
export let min_width: number | undefined = undefined;
export let include_time = true;
$: if (value !== old_value) {
old_value = value;
gradio.dispatch("change");
}
const format_date = (date: Date): string => {
if (date.toJSON() === null) return "";
const pad = (num: number): string => num.toString().padStart(2, "0");
const year = date.getFullYear();
const month = pad(date.getMonth() + 1); // getMonth() returns 0-11
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
const seconds = pad(date.getSeconds());
const date_str = `${year}-${month}-${day}`;
const time_str = `${hours}:${minutes}:${seconds}`;
if (include_time) {
return `${date_str} ${time_str}`;
}
return date_str;
};
let entered_value = value;
let datetime: HTMLInputElement;
let datevalue = "";
const date_is_valid_format = (date: string): boolean => {
if (date === "") return false;
const valid_regex = include_time
? /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/
: /^\d{4}-\d{2}-\d{2}$/;
const is_valid_date = date.match(valid_regex) !== null;
const is_valid_now =
date.match(/^(?:\s*now\s*(?:-\s*\d+\s*[dmhs])?)?\s*$/) !== null;
return is_valid_date || is_valid_now;
};
$: valid = date_is_valid_format(entered_value);
const submit_values = (): void => {
if (entered_value === value) return;
if (!date_is_valid_format(entered_value)) return;
old_value = value = entered_value;
gradio.dispatch("change");
};
</script>
<Block
{visible}
{elem_id}
{elem_classes}
{scale}
{min_width}
allow_overflow={false}
padding={true}
>
<div class="label-content">
<BlockTitle {show_label} {info}>{label}</BlockTitle>
</div>
<div class="timebox">
<input
class="time"
bind:value={entered_value}
class:invalid={!valid}
on:keydown={(evt) => {
if (evt.key === "Enter") {
submit_values();
gradio.dispatch("submit");
}
}}
on:blur={submit_values}
/>
{#if include_time}
<input
type="datetime-local"
class="datetime"
step="1"
bind:this={datetime}
bind:value={datevalue}
on:input={() => {
const date = new Date(datevalue);
entered_value = format_date(date);
submit_values();
}}
/>
{:else}
<input
type="date"
class="datetime"
step="1"
bind:this={datetime}
bind:value={datevalue}
on:input={() => {
const date = new Date(datevalue);
entered_value = format_date(date);
submit_values();
}}
/>
{/if}
<button
class="calendar"
on:click={() => {
datetime.showPicker();
}}><Calendar></Calendar></button
>
</div>
</Block>
<style>
.label-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
button {
cursor: pointer;
color: var(--body-text-color-subdued);
}
button:hover {
color: var(--body-text-color);
}
::placeholder {
color: var(--input-placeholder-color);
}
.timebox {
flex-grow: 1;
flex-shrink: 1;
display: flex;
position: relative;
box-shadow: var(--input-shadow);
background: var(--input-background-fill);
}
.timebox :global(svg) {
height: 18px;
}
.time {
padding: var(--input-padding);
color: var(--body-text-color);
font-weight: var(--input-text-weight);
font-size: var(--input-text-size);
line-height: var(--line-sm);
outline: none;
flex-grow: 1;
background: none;
border: var(--input-border-width) solid var(--input-border-color);
border-right: none;
border-top-left-radius: var(--input-radius);
border-bottom-left-radius: var(--input-radius);
}
.time.invalid {
color: var(--body-text-color-subdued);
}
.calendar {
display: inline-flex;
justify-content: center;
align-items: center;
transition: var(--button-transition);
box-shadow: var(--button-shadow);
text-align: center;
background: var(--button-secondary-background-fill);
color: var(--button-secondary-text-color);
font-weight: var(--button-large-text-weight);
font-size: var(--button-large-text-size);
border-top-right-radius: var(--input-radius);
border-bottom-right-radius: var(--input-radius);
padding: var(--size-2);
}
.datetime {
width: 0px;
padding: 0;
border: 0;
margin: 0;
background: none;
}
</style>

25
js/datetime/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "@gradio/datetime",
"version": "0.0.1",
"description": "Gradio UI packages",
"type": "module",
"author": "",
"license": "ISC",
"private": false,
"main_changeset": true,
"main": "Index.svelte",
"exports": {
".": "./Index.svelte",
"./example": "./Example.svelte",
"./package.json": "./package.json"
},
"dependencies": {
"@gradio/atoms": "workspace:^",
"@gradio/icons": "workspace:^",
"@gradio/statustracker": "workspace:^",
"@gradio/utils": "workspace:^"
},
"devDependencies": {
"@gradio/preview": "workspace:^"
}
}

17
js/icons/src/Back.svelte Normal file
View File

@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="12px"
height="24px"
fill="currentColor"
stroke-width="1.5"
viewBox="0 0 12 24"
>
<path
d="M9 6L3 12L9 18"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
</svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@ -0,0 +1,51 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24px"
height="24px"
viewBox="0 0 24 24"
>
<rect
x="2"
y="4"
width="20"
height="18"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
<line
x1="2"
y1="9"
x2="22"
y2="9"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
<line
x1="7"
y1="2"
x2="7"
y2="6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
<line
x1="17"
y1="2"
x2="17"
y2="6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
</svg>

After

Width:  |  Height:  |  Size: 730 B

View File

@ -1,6 +1,8 @@
export { default as Back } from "./Back.svelte";
export { default as Backward } from "./Backward.svelte";
export { default as Brush } from "./Brush.svelte";
export { default as BrushSize } from "./BrushSize.svelte";
export { default as Calendar } from "./Calendar.svelte";
export { default as Camera } from "./Camera.svelte";
export { default as Chart } from "./Chart.svelte";
export { default as Chat } from "./Chat.svelte";

View File

@ -3,7 +3,7 @@
</script>
<script lang="ts">
import type { Gradio } from "@gradio/utils";
import type { Gradio, SelectData } from "@gradio/utils";
import Plot from "./shared/Plot.svelte";
import { Block, BlockLabel } from "@gradio/atoms";
@ -31,8 +31,11 @@
export let gradio: Gradio<{
change: never;
clear_status: LoadingStatus;
select: SelectData;
}>;
export let show_actions_button = false;
export let _selectable = false;
export let x_lim: [number, number] | null = null;
</script>
<Block
@ -63,6 +66,10 @@
{caption}
{bokeh_version}
{show_actions_button}
{gradio}
{_selectable}
{x_lim}
on:change={() => gradio.dispatch("change")}
on:select={(e) => gradio.dispatch("select", e.detail)}
/>
</Block>

View File

@ -3,6 +3,7 @@
import { Plot as PlotIcon } from "@gradio/icons";
import { Empty } from "@gradio/atoms";
import type { ThemeMode } from "js/app/src/components/types";
import type { Gradio, SelectData } from "@gradio/utils";
export let value;
export let target: HTMLElement;
@ -11,6 +12,11 @@
export let caption: string;
export let bokeh_version: string | null;
export let show_actions_button: bool;
export let gradio: Gradio<{
select: SelectData;
}>;
export let x_lim: [number, number] | null = null;
export let _selectable: boolean;
let PlotComponent: any = null;
let _type = value?.type;
@ -45,7 +51,11 @@
{caption}
{bokeh_version}
{show_actions_button}
{gradio}
{_selectable}
{x_lim}
on:load
on:select
/>
{:else}
<Empty unpadded_box={true} size="large"><PlotIcon /></Empty>

View File

@ -1,17 +1,24 @@
<script lang="ts">
//@ts-nocheck
import { set_config } from "./altair_utils";
import { afterUpdate, onDestroy } from "svelte";
import { onMount, onDestroy } from "svelte";
import type { TopLevelSpec as Spec } from "vega-lite";
import vegaEmbed from "vega-embed";
import type { Gradio, SelectData } from "@gradio/utils";
import type { View } from "vega";
export let value;
export let target: HTMLDivElement;
export let colors: string[] = [];
export let caption: string;
export let show_actions_button: bool;
export let gradio: Gradio<{
select: SelectData;
}>;
let element: HTMLElement;
let parent_element: HTMLElement;
let view: View;
export let _selectable: bool;
let computed_style = window.getComputedStyle(target);
@ -19,6 +26,9 @@
let spec_width: number;
$: plot = value?.plot;
$: spec = JSON.parse(plot) as Spec;
$: if (spec && spec.params && !_selectable) {
spec.params = spec.params.filter((param) => param.name !== "brush");
}
$: if (old_spec !== spec) {
old_spec = spec;
spec_width = spec.width;
@ -34,21 +44,58 @@
? false
: true; // vega seems to glitch with width when orientation is set
const get_width = (): number => {
return Math.min(
parent_element.offsetWidth,
spec_width || parent_element.offsetWidth
);
};
let resize_callback = (): void => {};
const renderPlot = (): void => {
if (fit_width_to_parent) {
spec.width = Math.min(
parent_element.offsetWidth,
spec_width || parent_element.offsetWidth
);
spec.width = get_width();
}
vegaEmbed(element, spec, { actions: show_actions_button });
vegaEmbed(element, spec, { actions: show_actions_button }).then(
function (result): void {
view = result.view;
resize_callback = () => {
view.signal("width", get_width()).run();
};
if (!_selectable) return;
const callback = (event, item): void => {
const brushValue = view.signal("brush");
if (brushValue) {
if (Object.keys(brushValue).length === 0) {
gradio.dispatch("select", {
value: null,
index: null,
selected: false
});
} else {
const key = Object.keys(brushValue)[0];
let range: [number, number] = brushValue[key].map(
(x) => x / 1000
);
gradio.dispatch("select", {
value: brushValue,
index: range,
selected: true
});
}
}
};
view.addEventListener("mouseup", callback);
view.addEventListener("touchup", callback);
}
);
};
let resizeObserver = new ResizeObserver(() => {
if (fit_width_to_parent && spec.width !== parent_element.offsetWidth) {
renderPlot();
resize_callback();
}
});
afterUpdate(() => {
onMount(() => {
renderPlot();
resizeObserver.observe(parent_element);
});

View File

@ -77,7 +77,7 @@ export function set_config(
}
break;
case "line":
spec.config.mark = { stroke: accentColor };
spec.config.mark = { stroke: accentColor, cursor: "crosshair" };
layer.forEach((d: any) => {
if (d.encoding.color) {
d.encoding.color.scale.range = d.encoding.color.scale.range.map(

View File

@ -133,6 +133,7 @@
"@gradio/tabitem": "workspace:^",
"@gradio/tabs": "workspace:^",
"@gradio/textbox": "workspace:^",
"@gradio/datetime": "workspace:^",
"@gradio/timer": "workspace:^",
"@gradio/upload": "workspace:^",
"@gradio/uploadbutton": "workspace:^",

View File

@ -216,6 +216,9 @@ importers:
'@gradio/dataset':
specifier: workspace:^
version: link:js/dataset
'@gradio/datetime':
specifier: workspace:^
version: link:js/datetime
'@gradio/downloadbutton':
specifier: workspace:^
version: link:js/downloadbutton
@ -562,6 +565,9 @@ importers:
'@gradio/dataset':
specifier: workspace:^
version: link:../dataset
'@gradio/datetime':
specifier: workspace:^
version: link:../datetime
'@gradio/downloadbutton':
specifier: workspace:^
version: link:../downloadbutton
@ -1061,6 +1067,25 @@ importers:
specifier: workspace:^
version: link:../preview
js/datetime:
dependencies:
'@gradio/atoms':
specifier: workspace:^
version: link:../atoms
'@gradio/icons':
specifier: workspace:^
version: link:../icons
'@gradio/statustracker':
specifier: workspace:^
version: link:../statustracker
'@gradio/utils':
specifier: workspace:^
version: link:../utils
devDependencies:
'@gradio/preview':
specifier: workspace:^
version: link:../preview
js/downloadbutton:
dependencies:
'@gradio/button':

View File

@ -1,5 +1,5 @@
aiofiles>=22.0,<24.0
altair>=4.2.0,<6.0
altair>=5.0,<6.0
fastapi
ffmpy
gradio_client==1.0.2

View File

@ -0,0 +1,43 @@
from datetime import datetime
import gradio as gr
class TestDateTime:
def test_component_functions(self):
"""
Preprocess, postprocess
"""
def within_range(time1, time2, delta=30):
return abs(time1 - time2) < delta
dt = gr.DateTime(timezone="US/Pacific")
dt2 = gr.DateTime(timezone="Europe/Paris")
dt3 = gr.DateTime(timezone="Europe/Paris", include_time=False)
dt4 = gr.DateTime(timezone="US/Pacific", type="string")
now = datetime.now().timestamp()
assert dt.preprocess("2020-02-01 08:10:25") == 1580573425.0
assert dt2.preprocess("2020-02-01 08:10:25") == 1580541025.0
assert dt3.preprocess("2020-02-01") == 1580511600.0
assert within_range(dt.preprocess("now"), now)
assert not within_range(dt.preprocess("now - 1m"), now)
assert within_range(dt2.preprocess("now"), now)
assert within_range(dt.preprocess("now - 20s"), now - 20)
assert within_range(dt2.preprocess("now - 20s"), now - 20)
assert within_range(dt.preprocess("now - 10m"), now - 10 * 60)
assert within_range(dt.preprocess("now - 3h"), now - 3 * 60 * 60)
assert within_range(dt.preprocess("now - 12d"), now - 12 * 24 * 60 * 60)
assert dt4.preprocess("2020-02-01 08:10:25") == "2020-02-01 08:10:25"
assert len(dt4.preprocess("now - 10m")) == 19
assert dt.postprocess(1500000000) == "2017-07-13 19:40:00"
assert dt2.postprocess(1500000000) == "2017-07-14 04:40:00"
def test_in_interface(self):
"""
Interface, process
"""
dt = gr.DateTime(timezone="US/Pacific")
iface = gr.Interface(lambda x: x, dt, "number")
assert iface("2017-07-13 19:40:00") == 1500000000

View File

@ -31,7 +31,7 @@ class TestLinePlot:
"stroke_dash": None,
"overlay_point": None,
"title": None,
"tooltip": None,
"tooltip": [],
"x_title": None,
"y_title": None,
"color_legend_title": None,

View File

@ -6,7 +6,7 @@
#
aiofiles==23.2.1
# via gradio
altair==4.2.0
altair>=5.0
# via
# -r requirements.in
# gradio