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/800/300?image={index}",
            "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(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:
        return idom.html.div(idom.html.h1("You won!"), start_button)
    elif game_state == GameState.lost:
        return idom.html.div(idom.html.h1("You lost"), start_button)
    else:
        return idom.html.div(idom.html.h1("Click to play"), start_button)


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)
    grid_events = grid["eventHandlers"] = idom.Events()

    @grid_events.on("KeyDown", 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

    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


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 (seperate each coefficient by a space) 🔢:

from io import BytesIO

import matplotlib.pyplot as plt

import idom
from idom.widgets.html 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.html import Input


victory = idom.install("victory", fallback="loading...")


@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 victory.VictoryLine(
        {
            "data": data,
            "style": {
                "parent": {"width": "500px"},
                "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)

Install Javascript Modules

Simply install your javascript library of choice using the idom CLI:

idom install victory

Then import the module with Module:

import idom


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

Define Javascript Modules

Assuming you already installed victory as in the Install Javascript Modules section:

Click the bars to trigger an event 👇

from pathlib import Path

import idom


# we use this to make writing our React code a bit easier
idom.install("htm")

path_to_source_file = Path(__file__).parent / "super_simple_chart.js"
ssc = idom.Module("super-simple-chart", source_file=path_to_source_file)


idom.run(
    idom.component(
        lambda: ssc.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 React from "./react.js";
import htm from "./htm.js";

const html = htm.bind(React.createElement);

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 Slider

Assuming you already installed @material-ui/core as in the Install Javascript Modules section:

Move the slider and see the event information update 👇

import json

import idom


material_ui = idom.install("@material-ui/core", fallback="loading...")


@idom.component
def ViewSliderEvents():
    event, set_event = idom.hooks.use_state(None)

    return idom.html.div(
        material_ui.Slider(
            {
                "color": "primary",
                "step": 10,
                "min": 0,
                "max": 100,
                "defaultValue": 50,
                "valueLabelDisplay": "auto",
                "onChange": lambda *event: set_event(event),
            }
        ),
        idom.html.pre(json.dumps(event, indent=2)),
    )


idom.run(ViewSliderEvents)