Examples¶

Slideshow¶

Try clicking the image 🖱️

import idom


@idom.component
def Slideshow():
    index, set_index = idom.hooks.use_state(0)

    def next_image(event):
        set_index(index + 1)

    return idom.html.img(
        {
            "src": f"https://picsum.photos/id/{index}/800/300",
            "style": {"cursor": "pointer"},
            "onClick": next_image,
        }
    )


idom.run(Slideshow)

Click Counter¶

import idom


@idom.component
def ClickCount():
    count, set_count = idom.hooks.use_state(0)

    return idom.html.button(
        {"onClick": lambda event: set_count(count + 1)},
        [f"Click count: {count}"],
    )


idom.run(ClickCount)

To Do List¶

Try typing in the text box and pressing ‘Enter’ 📋

import idom


@idom.component
def Todo():
    items, set_items = idom.hooks.use_state([])

    async def add_new_task(event):
        if event["key"] == "Enter":
            set_items(items + [event["value"]])

    tasks = []

    for index, text in enumerate(items):

        async def remove_task(event, index=index):
            set_items(items[:index] + items[index + 1 :])

        task_text = idom.html.td(idom.html.p(text))
        delete_button = idom.html.td({"onClick": remove_task}, idom.html.button(["x"]))
        tasks.append(idom.html.tr(task_text, delete_button))

    task_input = idom.html.input({"onKeyDown": add_new_task})
    task_table = idom.html.table(tasks)

    return idom.html.div(
        idom.html.p("press enter to add a task:"),
        task_input,
        task_table,
    )


idom.run(Todo)

The Game Snake¶

Click to start playing and use the arrow keys to move 🎮

Slow internet may cause inconsistent frame pacing 😅

import asyncio
import enum
import random
import time

import idom


class GameState(enum.Enum):
    init = 0
    lost = 1
    won = 2
    play = 3


@idom.component
def GameView():
    game_state, set_game_state = idom.hooks.use_state(GameState.init)

    if game_state == GameState.play:
        return GameLoop(grid_size=6, block_scale=50, set_game_state=set_game_state)

    start_button = idom.html.button(
        {"onClick": lambda event: set_game_state(GameState.play)},
        "Start",
    )

    if game_state == GameState.won:
        menu = idom.html.div(idom.html.h3("You won!"), start_button)
    elif game_state == GameState.lost:
        menu = idom.html.div(idom.html.h3("You lost"), start_button)
    else:
        menu = idom.html.div(idom.html.h3("Click to play"), start_button)

    menu_style = idom.html.style(
        """
        .snake-game-menu h3 {
            margin-top: 0px !important;
        }
        """
    )

    return idom.html.div({"className": "snake-game-menu"}, menu_style, menu)


class Direction(enum.Enum):
    ArrowUp = (0, -1)
    ArrowLeft = (-1, 0)
    ArrowDown = (0, 1)
    ArrowRight = (1, 0)


@idom.component
def GameLoop(grid_size, block_scale, set_game_state):
    # we `use_ref` here to capture the latest direction press without any delay
    direction = idom.hooks.use_ref(Direction.ArrowRight.value)
    # capture the last direction of travel that was rendered
    last_direction = direction.current

    snake, set_snake = idom.hooks.use_state([(grid_size // 2 - 1, grid_size // 2 - 1)])
    food, set_food = use_snake_food(grid_size, snake)

    grid = create_grid(grid_size, block_scale)

    @idom.event(prevent_default=True)
    def on_direction_change(event):
        if hasattr(Direction, event["key"]):
            maybe_new_direction = Direction[event["key"]].value
            direction_vector_sum = tuple(
                map(sum, zip(last_direction, maybe_new_direction))
            )
            if direction_vector_sum != (0, 0):
                direction.current = maybe_new_direction

    grid_wrapper = idom.html.div({"onKeyDown": on_direction_change}, grid)

    assign_grid_block_color(grid, food, "blue")

    for location in snake:
        assign_grid_block_color(grid, location, "white")

    new_game_state = None
    if snake[-1] in snake[:-1]:
        assign_grid_block_color(grid, snake[-1], "red")
        new_game_state = GameState.lost
    elif len(snake) == grid_size ** 2:
        assign_grid_block_color(grid, snake[-1], "yellow")
        new_game_state = GameState.won

    interval = use_interval(0.5)

    @idom.hooks.use_effect
    async def animate():
        if new_game_state is not None:
            await asyncio.sleep(1)
            set_game_state(new_game_state)
            return

        await interval

        new_snake_head = (
            # grid wraps due to mod op here
            (snake[-1][0] + direction.current[0]) % grid_size,
            (snake[-1][1] + direction.current[1]) % grid_size,
        )

        if snake[-1] == food:
            set_food()
            new_snake = snake + [new_snake_head]
        else:
            new_snake = snake[1:] + [new_snake_head]

        set_snake(new_snake)

    return grid_wrapper


def use_snake_food(grid_size, current_snake):
    grid_points = {(x, y) for x in range(grid_size) for y in range(grid_size)}
    points_not_in_snake = grid_points.difference(current_snake)

    food, _set_food = idom.hooks.use_state(current_snake[-1])

    def set_food():
        _set_food(random.choice(list(points_not_in_snake)))

    return food, set_food


def use_interval(rate):
    usage_time = idom.hooks.use_ref(time.time())

    async def interval() -> None:
        await asyncio.sleep(rate - (time.time() - usage_time.current))
        usage_time.current = time.time()

    return asyncio.ensure_future(interval())


def create_grid(grid_size, block_scale):
    return idom.html.div(
        {
            "style": {
                "height": f"{block_scale * grid_size}px",
                "width": f"{block_scale * grid_size}px",
                "cursor": "pointer",
                "display": "grid",
                "grid-gap": 0,
                "grid-template-columns": f"repeat({grid_size}, {block_scale}px)",
                "grid-template-rows": f"repeat({grid_size}, {block_scale}px)",
            },
            "tabIndex": -1,
        },
        [
            idom.html.div(
                {"style": {"height": f"{block_scale}px"}},
                [create_grid_block("black", block_scale) for i in range(grid_size)],
            )
            for i in range(grid_size)
        ],
    )


def create_grid_block(color, block_scale):
    return idom.html.div(
        {
            "style": {
                "height": f"{block_scale}px",
                "width": f"{block_scale}px",
                "backgroundColor": color,
                "outline": "1px solid grey",
            }
        }
    )


def assign_grid_block_color(grid, point, color):
    x, y = point
    block = grid["children"][x]["children"][y]
    block["attributes"]["style"]["backgroundColor"] = color


idom.run(GameView)

Matplotlib Plot¶

Pick the polynomial coefficients (separate each coefficient by a space) 🔢:

from io import BytesIO

import matplotlib.pyplot as plt

import idom
from idom.widgets import image


@idom.component
def PolynomialPlot():
    coefficients, set_coefficients = idom.hooks.use_state([0])

    x = [n for n in linspace(-1, 1, 50)]
    y = [polynomial(value, coefficients) for value in x]

    return idom.html.div(
        plot(f"{len(coefficients)} Term Polynomial", x, y),
        ExpandableNumberInputs(coefficients, set_coefficients),
    )


@idom.component
def ExpandableNumberInputs(values, set_values):
    inputs = []
    for i in range(len(values)):

        def set_value_at_index(event, index=i):
            new_value = float(event["value"] or 0)
            set_values(values[:index] + [new_value] + values[index + 1 :])

        inputs.append(poly_coef_input(i + 1, set_value_at_index))

    def add_input():
        set_values(values + [0])

    def del_input():
        set_values(values[:-1])

    return idom.html.div(
        idom.html.div(
            "add/remove term:",
            idom.html.button({"onClick": lambda event: add_input()}, "+"),
            idom.html.button({"onClick": lambda event: del_input()}, "-"),
        ),
        inputs,
    )


def plot(title, x, y):
    fig, axes = plt.subplots()
    axes.plot(x, y)
    axes.set_title(title)
    buffer = BytesIO()
    fig.savefig(buffer, format="png")
    plt.close(fig)
    return image("png", buffer.getvalue())


def poly_coef_input(index, callback):
    return idom.html.div(
        {"style": {"margin-top": "5px"}},
        idom.html.label(
            "C",
            idom.html.sub(index),
            " × X",
            idom.html.sup(index),
        ),
        idom.html.input(
            {
                "type": "number",
                "onChange": callback,
            },
        ),
    )


def polynomial(x, coefficients):
    return sum(c * (x ** (i + 1)) for i, c in enumerate(coefficients))


def linspace(start, stop, n):
    if n == 1:
        yield stop
        return
    h = (stop - start) / (n - 1)
    for i in range(n):
        yield start + h * i


idom.run(PolynomialPlot)

Simple Dashboard¶

Try interacting with the sliders 📈

import asyncio
import random
import time

import idom
from idom.widgets import Input


victory = idom.web.module_from_template(
    "react",
    "victory-line",
    fallback="⌛",
    # not usually required (see issue #461 for more info)
    unmount_before_update=True,
)
VictoryLine = idom.web.export(victory, "VictoryLine")


@idom.component
def RandomWalk():
    mu = idom.hooks.use_ref(0)
    sigma = idom.hooks.use_ref(1)

    return idom.html.div(
        RandomWalkGraph(mu, sigma),
        idom.html.style(
            """
            .number-input-container {margin-bottom: 20px}
            .number-input-container input {width: 48%;float: left}
            .number-input-container input + input {margin-left: 4%}
            """
        ),
        NumberInput(
            "Mean",
            mu.current,
            mu.set_current,
            (-1, 1, 0.01),
        ),
        NumberInput(
            "Standard Deviation",
            sigma.current,
            sigma.set_current,
            (0, 1, 0.01),
        ),
    )


@idom.component
def RandomWalkGraph(mu, sigma):
    interval = use_interval(0.5)
    data, set_data = idom.hooks.use_state([{"x": 0, "y": 0}] * 50)

    @idom.hooks.use_effect
    async def animate():
        await interval
        last_data_point = data[-1]
        next_data_point = {
            "x": last_data_point["x"] + 1,
            "y": last_data_point["y"] + random.gauss(mu.current, sigma.current),
        }
        set_data(data[1:] + [next_data_point])

    return VictoryLine(
        {
            "data": data,
            "style": {
                "parent": {"width": "100%"},
                "data": {"stroke": "royalblue"},
            },
        }
    )


@idom.component
def NumberInput(label, value, set_value_callback, domain):
    minimum, maximum, step = domain
    attrs = {"min": minimum, "max": maximum, "step": step}

    value, set_value = idom.hooks.use_state(value)

    def update_value(value):
        set_value(value)
        set_value_callback(value)

    return idom.html.fieldset(
        {"class": "number-input-container"},
        [idom.html.legend({"style": {"font-size": "medium"}}, label)],
        Input(update_value, "number", value, attributes=attrs, cast=float),
        Input(update_value, "range", value, attributes=attrs, cast=float),
    )


def use_interval(rate):
    usage_time = idom.hooks.use_ref(time.time())

    async def interval() -> None:
        await asyncio.sleep(rate - (time.time() - usage_time.current))
        usage_time.current = time.time()

    return asyncio.ensure_future(interval())


idom.run(RandomWalk)

Dynamically Loaded React Components¶

This method is not recommended for use in production applications, but it’s great while you’re experimenting:

import idom


victory = idom.web.module_from_template("react", "victory-bar", fallback="⌛")
VictoryBar = idom.web.export(victory, "VictoryBar")

bar_style = {"parent": {"width": "500px"}, "data": {"fill": "royalblue"}}
idom.run(idom.component(lambda: VictoryBar({"style": bar_style})))

Define Javascript Modules¶

Shows a very simple chart implemented in vanilla Javascript:

from pathlib import Path

import idom


file = Path(__file__).parent / "super_simple_chart.js"
ssc = idom.web.module_from_file("super-simple-chart", file, fallback="⌛")
SuperSimpleChart = idom.web.export(ssc, "SuperSimpleChart")

idom.run(
    idom.component(
        lambda: SuperSimpleChart(
            {
                "data": [
                    {"x": 1, "y": 2},
                    {"x": 2, "y": 4},
                    {"x": 3, "y": 7},
                    {"x": 4, "y": 3},
                    {"x": 5, "y": 5},
                    {"x": 6, "y": 9},
                    {"x": 7, "y": 6},
                ],
                "height": 300,
                "width": 500,
                "color": "royalblue",
                "lineWidth": 4,
                "axisColor": "silver",
            }
        )
    )
)
import { h, render } from "https://unpkg.com/preact?module";
import htm from "https://unpkg.com/htm?module";

const html = htm.bind(h);

export function bind(node, config) {
  return {
    create: (component, props, children) => h(component, props, ...children),
    render: (element) => render(element, node),
    unmount: () => render(null, node),
  }
}

export function SuperSimpleChart(props) {
  const data = props.data;
  const lastDataIndex = data.length - 1;

  const options = {
    height: props.height || 100,
    width: props.width || 100,
    color: props.color || "blue",
    lineWidth: props.lineWidth || 2,
    axisColor: props.axisColor || "black",
  };

  const xData = data.map((point) => point.x);
  const yData = data.map((point) => point.y);

  const domain = {
    xMin: Math.min(...xData),
    xMax: Math.max(...xData),
    yMin: Math.min(...yData),
    yMax: Math.max(...yData),
  };

  return html`<svg
    width="${options.width}px"
    height="${options.height}px"
    viewBox="0 0 ${options.width} ${options.height}"
  >
    ${makePath(props, domain, data, options)} ${makeAxis(props, options)}
  </svg>`;
}

function makePath(props, domain, data, options) {
  const { xMin, xMax, yMin, yMax } = domain;
  const { width, height } = options;
  const getSvgX = (x) => ((x - xMin) / (xMax - xMin)) * width;
  const getSvgY = (y) => height - ((y - yMin) / (yMax - yMin)) * height;

  let pathD = "M " + getSvgX(data[0].x) + " " + getSvgY(data[0].y) + " ";
  pathD += data.map((point, i) => {
    return "L " + getSvgX(point.x) + " " + getSvgY(point.y) + " ";
  });

  return html`<path
    d="${pathD}"
    style=${{
      stroke: options.color,
      strokeWidth: options.lineWidth,
      fill: "none",
    }}
  />`;
}

function makeAxis(props, options) {
  return html`<g>
    <line
      x1="0"
      y1=${options.height}
      x2=${options.width}
      y2=${options.height}
      style=${{ stroke: options.axisColor, strokeWidth: options.lineWidth * 2 }}
    />
    <line
      x1="0"
      y1="0"
      x2="0"
      y2=${options.height}
      style=${{ stroke: options.axisColor, strokeWidth: options.lineWidth * 2 }}
    />
  </g>`;
}

Material UI Button¶

Click the button to change the indicator 👇

import idom


mui = idom.web.module_from_template("react", "@material-ui/core@^5.0", fallback="⌛")
Switch = idom.web.export(mui, "Switch")


@idom.component
def DayNightSwitch():
    checked, set_checked = idom.hooks.use_state(False)

    return idom.html.div(
        Switch(
            {
                "checked": checked,
                "onChange": lambda event, checked: set_checked(checked),
            }
        ),
        "🌞" if checked else "🌚",
    )


idom.run(DayNightSwitch)

Pigeon Maps¶

Click the map to create pinned location 📍:

import idom


pigeon_maps = idom.web.module_from_template("react", "pigeon-maps", fallback="⌛")
Map, Marker = idom.web.export(pigeon_maps, ["Map", "Marker"])


@idom.component
def MapWithMarkers():
    marker_anchor, add_marker_anchor, remove_marker_anchor = use_set()

    markers = list(
        map(
            lambda anchor: Marker(
                {
                    "anchor": anchor,
                    "onClick": lambda: remove_marker_anchor(anchor),
                },
                key=str(anchor),
            ),
            marker_anchor,
        )
    )

    return Map(
        {
            "defaultCenter": (37.774, -122.419),
            "defaultZoom": 12,
            "height": "300px",
            "metaWheelZoom": True,
            "onClick": lambda event: add_marker_anchor(tuple(event["latLng"])),
        },
        markers,
    )


def use_set(initial_value=None):
    values, set_values = idom.hooks.use_state(initial_value or set())

    def add_value(lat_lon):
        set_values(values.union({lat_lon}))

    def remove_value(lat_lon):
        set_values(values.difference({lat_lon}))

    return values, add_value, remove_value


idom.run(MapWithMarkers)

Cytoscape Notework Graph¶

You can move the nodes in the graph 🕸️:

import random

import idom


react_cytoscapejs = idom.web.module_from_template(
    # we need to use this template because react-cytoscapejs uses a default export
    "react",
    "react-cytoscapejs",
    exports_default=True,
    fallback="⌛",
)
Cytoscape = idom.web.export(react_cytoscapejs, "default")


@idom.component
def RandomNetworkGraph():
    return Cytoscape(
        {
            "style": {"width": "100%", "height": "200px"},
            "elements": random_network(20),
            "layout": {"name": "cose"},
        }
    )


def random_network(number_of_nodes):
    conns = []
    nodes = [{"data": {"id": 0, "label": 0}}]

    for src_node_id in range(1, number_of_nodes + 1):
        tgt_node = random.choice(nodes)
        src_node = {"data": {"id": src_node_id, "label": src_node_id}}

        new_conn = {"data": {"source": src_node_id, "target": tgt_node["data"]["id"]}}

        nodes.append(src_node)
        conns.append(new_conn)

    return nodes + conns


idom.run(RandomNetworkGraph)