Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ uv add d2-widget

## Usage

The following examples demonstrate how to use D2Widget with increasing complexity.
The following examples demonstrate how to use Widget with increasing complexity.

### Basic Usage

The simplest way to use D2Widget is to pass a D2 diagram as a string to the constructor.
The simplest way to use Widget is to pass a D2 diagram as a string to the constructor.

```python
from d2_widget import D2Widget
from d2_widget import Widget

D2Widget("x -> y")
Widget("x -> y")
```

<img src="./assets/examples/simple.svg" alt="simple example" width="400"/>
Expand All @@ -39,9 +39,9 @@ D2Widget("x -> y")
You can add direction and layout settings directly in the D2 markup.

```python
from d2_widget import D2Widget
from d2_widget import Widget

D2Widget("""
Widget("""
direction: right
x -> y
""")
Expand All @@ -55,9 +55,9 @@ You can specify compile options using the second argument to the constructor.
You can read about the semantics of the options in the [D2 documentation](https://www.npmjs.com/package/@terrastruct/d2#compileoptions).

```python
from d2_widget import D2Widget
from d2_widget import Widget

D2Widget("""
Widget("""
direction: right
x -> y
""",
Expand All @@ -76,12 +76,35 @@ x -> y
You can access the generated SVG using the `svg` attribute.

```python
from d2_widget import D2Widget
from d2_widget import Widget

w = D2Widget("x -> y")
w = Widget("x -> y")
w.svg
```

### `%%d2` Cell Magic

You can use the `%%d2` cell magic to display a D2 diagram in a Jupyter notebook.

First, you need to load the extension:

```python
%load_ext d2_widget
```

Then, you can use the `%%d2` cell magic to display a D2 diagram.
You can pass compile options to the cell magic using keyword arguments.

```python
%%d2 sketch=True themeID=200
direction: right
x -> y
y -> z { style.animated: true }
z -> x
```

<img src="./assets/examples/cell-magic.gif" alt="example with cell magic" width="100%"/>

## Development

We recommend using [uv](https://github.com/astral-sh/uv) for development.
Expand Down
Binary file added assets/examples/cell-magic.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 27 additions & 5 deletions example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"metadata": {},
"outputs": [],
"source": [
"from d2_widget import D2Widget"
"from d2_widget import Widget"
]
},
{
Expand All @@ -26,7 +26,7 @@
"metadata": {},
"outputs": [],
"source": [
"D2Widget(\n",
"Widget(\n",
" \"\"\"\n",
"direction: right\n",
"D2 Parser: {\n",
Expand Down Expand Up @@ -58,7 +58,7 @@
"metadata": {},
"outputs": [],
"source": [
"D2Widget(\n",
"Widget(\n",
" \"\"\"\n",
"direction: right\n",
"\n",
Expand Down Expand Up @@ -151,7 +151,7 @@
"metadata": {},
"outputs": [],
"source": [
"D2Widget(\n",
"Widget(\n",
" \"\"\"\n",
"how does the cat go?: {\n",
" link: layers.cat\n",
Expand All @@ -172,7 +172,29 @@
"metadata": {},
"outputs": [],
"source": [
"D2Widget(\"x -> y\", {\"themeID\": 200})"
"Widget(\"x -> y\", {\"themeID\": 200})"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"%load_ext d2_widget"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%d2 themeID=201 sketch=True pad=25\n",
"direction: right\n",
"x -> y { style.animated: true }\n",
"y -> z { style.animated: true }\n",
"z -> x { style.animated: true }"
]
}
],
Expand Down
19 changes: 9 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@ authors = [
{name = "Péter Ferenc Gyarmati", email = "dev.petergy@gmail.com"},
]
license = {file = "LICENSE"}
dependencies = [
"anywidget>=0.9",
"traitlets>=5",
]
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.9"
keywords = ["anywidget", "d2", "diagram", "jupyter", "widget"]
classifiers = [
"Development Status :: 4 - Beta",
Expand All @@ -21,13 +17,16 @@ classifiers = [
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"anywidget>=0.9",
"traitlets>=5",
]

[project.urls]
Homepage = "https://github.com/peter-gy/d2-widget"
Expand All @@ -39,10 +38,10 @@ Documentation = "https://github.com/peter-gy/d2-widget#readme"
# https://peps.python.org/pep-0735/
[dependency-groups]
dev = [
"watchfiles",
"jupyterlab",
"marimo",
"ruff",
"jupyterlab>=4",
"marimo>=0.13",
"ruff>=0.11",
"watchfiles>=1",
]

[build-system]
Expand Down
152 changes: 16 additions & 136 deletions src/d2_widget/__init__.py
Original file line number Diff line number Diff line change
@@ -1,142 +1,22 @@
import importlib.metadata
import pathlib
from d2_widget._utils import parse_magic_arguments
from d2_widget._version import __version__
from d2_widget._widget import Widget

import anywidget
import traitlets
from typing import TypedDict, Optional, Literal, Annotated
__all__ = ["Widget", "__version__"]

try:
__version__ = importlib.metadata.version("d2-widget")
except importlib.metadata.PackageNotFoundError:
__version__ = "unknown"

def load_ipython_extension(ipython) -> None: # type: ignore[no-untyped-def]
"""Extend IPython with interactive D2 widget display when using the `%d2` magic command."""
from IPython.core.magic import register_cell_magic
from IPython.display import display

class RenderOptions(TypedDict, total=False):
"""A `TypedDict` containing options for rendering D2 diagrams.
@register_cell_magic
def d2(line, cell):
options = parse_magic_arguments(line)
display(Widget(cell, options))

Matches `RenderOptions` TypeScript interface from `@terrastruct/d2 <https://www.npmjs.com/package/@terrastruct/d2#RenderOptions>`_.
"""

sketch: Annotated[
bool,
"Enable sketch mode [default: false]",
]
themeID: Annotated[
int,
"Theme ID to use [default: 0]",
]
darkThemeID: Annotated[
int,
"Theme ID to use when client is in dark mode",
]
center: Annotated[
bool,
"Center the SVG in the containing viewbox [default: false]",
]
pad: Annotated[
int,
"Pixels padded around the rendered diagram [default: 100]",
]
scale: Annotated[
float,
"Scale the output. E.g., 0.5 to halve the default size. The default will render SVG's that will fit to screen. Setting to 1 turns off SVG fitting to screen.",
]
forceAppendix: Annotated[
bool,
"Adds an appendix for tooltips and links [default: false]",
]
target: Annotated[
str,
"Target board/s to render. If target ends with '', it will be rendered with all of its scenarios, steps, and layers. Otherwise, only the target board will be rendered. E.g. target: 'layers.x.*' to render layer 'x' with all of its children. Pass '' to render all scenarios, steps, and layers. By default, only the root board is rendered. Multi-board outputs are currently only supported for animated SVGs and so animateInterval must be set to a value greater than 0 when targeting multiple boards.",
]
animateInterval: Annotated[
int,
"If given, multiple boards are packaged as 1 SVG which transitions through each board at the interval (in milliseconds).",
]
salt: Annotated[
str,
"Add a salt value to ensure the output uses unique IDs. This is useful when generating multiple identical diagrams to be included in the same HTML doc, so that duplicate IDs do not cause invalid HTML. The salt value is a string that will be appended to IDs in the output.",
]
noXMLTag: Annotated[
bool,
"Omit XML tag (<?xml ...?>) from output SVG files. Useful when generating SVGs for direct HTML embedding.",
]


class CompileOptions(RenderOptions, total=False):
"""A `TypedDict` containing options for compiling D2 diagrams, extending `RenderOptions`.

Matches `CompileOptions` TypeScript interface from `@terrastruct/d2 <https://www.npmjs.com/package/@terrastruct/d2#CompileOptions>`_.
"""

layout: Annotated[
Literal["dagre", "elk"],
"Layout engine to use [default: 'dagre']",
]
fontRegular: Annotated[
bytes,
"A byte array containing .ttf file to use for the regular font. If none provided, Source Sans Pro Regular is used.",
]
fontItalic: Annotated[
bytes,
"A byte array containing .ttf file to use for the italic font. If none provided, Source Sans Pro Italic is used.",
]
fontBold: Annotated[
bytes,
"A byte array containing .ttf file to use for the bold font. If none provided, Source Sans Pro Bold is used.",
]
fontSemibold: Annotated[
bytes,
"A byte array containing .ttf file to use for the semibold font. If none provided, Source Sans Pro Semibold is used.",
]


# These defaults ensure that multi-layer, animated diagrams are rendered by default.
DEFAULT_OPTIONS: CompileOptions = {
"noXMLTag": True,
"target": "*",
"animateInterval": 1500,
}


class D2Widget(anywidget.AnyWidget):
"""An `anywidget.AnyWidget` wrapper for D2 diagrams.

This widget allows you to render D2 diagrams in Jupyter notebooks and other
compatible environments.

D2 is a modern diagram scripting language that turns text to diagrams.
You can read about D2 diagrams at `https://d2lang.com <https://d2lang.com>`_ and access an
online playground at `https://play.d2lang.com <https://play.d2lang.com>`_.
"""

_esm = pathlib.Path(__file__).parent / "static" / "widget.js"
_css = pathlib.Path(__file__).parent / "static" / "widget.css"

_svg = traitlets.Unicode().tag(sync=True)
diagram = traitlets.Unicode().tag(sync=True)
options = traitlets.Dict().tag(sync=True)

def __init__(self, diagram: str, options: Optional[CompileOptions] = None):
"""Initializes a D2Widget.

Args:
diagram (str): The diagram script to render.
options (Optional[CompileOptions]): The options to use for rendering.
"""
super().__init__()
self.diagram = diagram
self.options = DEFAULT_OPTIONS if options is None else options

@property
def svg(self) -> str:
"""The SVG representation of the diagram.

This property might not be immediately up to date if accessed very shortly
after the widget was rendered. Ensure to access the property only after
the widget has been rendered in your notebook environment.

Returns:
str: The SVG representation of the diagram.
"""
return self._svg
def unload_ipython_extension(ipython) -> None: # type: ignore[no-untyped-def]
"""Clean up by removing the registered cell magic."""
if "d2" in ipython.magics_manager.cell_magics:
del ipython.magics_manager.cell_magics["d2"]
Loading