#html

#html コマンドは、その名の通り html をインフォビューに表示させるコマンドです。ProofWidgets4 というライブラリで定義されています。

JSX によく似た構文を使うことができます。

import ProofWidgets

-- JSX ライクな構文が使えるようにする
open scoped ProofWidgets.Jsx

#html <p>"ここに HTML を書きます"</p>

-- いろんなHTMLタグを書くことができる
#html
  <div>
    <h3>"見出し"</h3>
    <p>
      <b>"強調されたテキスト"</b>
      <i>"斜体のテキスト"</i>
    </p>
    <a href="https://lean-lang.org/">"リンク"</a>
  </div>

-- 画像も表示できる
#html
  <img src={"https://upload.wikimedia.org/wikipedia/commons/6/6a/Julia-set_N_z3-1.png"}
    alt="julia set"/>

コンポーネント紹介

HTMLタグを使用できるだけでなく、様々なコンポーネントが定義されています。

MarkdownDisplay

<MarkdownDisplay /> コンポーネントを使用すると、Markdown や TeX を表示させることができます。

import ProofWidgets

open ProofWidgets Jsx

-- Markdown と TeX を表示する
#html <MarkdownDisplay contents={"
  ## Riemann zeta function

  The Riemann zeta function is defined as
  $$
  \\zeta(s) = \\sum_{n=1}^∞ \\frac{1}{n^s}
  $$

  for $\\mathrm{Re} (s) > 0$.
"}/>

GraphDisplay

<GraphDisplay /> コンポーネントを使用すると、有向グラフを表示させることができます。

import ProofWidgets

open ProofWidgets Jsx GraphDisplay

/-- `Edge` を作る -/
def mkEdge (st : String × String) : Edge := {source := st.1, target := st.2}

/-- 文字列として与えられたラベルから `Vertex` を作る -/
def mkVertex (id : String) : Vertex := {id := id}

-- 有向グラフを表示する
#html
  <GraphDisplay
    vertices={#["a", "b", "c", "d", "e"].map mkVertex}
    edges={#[("a", "b"), ("b", "c"), ("c", "d"), ("d", "e"), ("e", "a")].map mkEdge}
  />

LineChart

ProofWidgets4 には Recharts ライブラリ に対するサポートがあり、<LineChart /> コンポーネントを使用すると、関数のグラフを表示させることができます。

import ProofWidgets

open Lean ProofWidgets Recharts

open scoped Jsx

/-- 与えられた関数 `fn` の `[0, 1]` 区間上での値のグラフ -/
def Plot (fn : Float → Float) (steps := 100) : Html :=
  -- `[0, 1)` 区間を `steps` 個に分割する
  let grids := Array.range steps
    |>.map (fun x => x.toFloat / steps.toFloat)

  -- データを JSON に変換
  let y := grids.map fn
  let jsonData : Array Json := grids.zip y
    |>.map (fun (x,y) => json% {x: $(toJson x), y: $(toJson y)});

  <LineChart width={400} height={400} data={jsonData}>
    <XAxis dataKey?="x" />
    <YAxis dataKey?="y" />
    <Line type={.monotone} dataKey="y" stroke="#8884d8" dot?={Bool.false} />
  </LineChart>

#html Plot (fun x => (x - 0.3) ^ 2 + 0.1)
#html Plot (fun x => 0.2 + 0.5 * Float.sin (7 * x))

使用例

#html コマンドを使うと SVG 画像を infoview 内で直接表示させることができるという点、さらに ProofWidgets が SVG 画像の作成をある程度サポートしているという点を利用すると、「二分木を画像として infoview に表示させる」ということが可能です。

import ProofWidgets

open ProofWidgets Svg

/-- デフォルトの表示領域(Frame) -/
def defaultFrame : Frame := {
  xmin := 0 -- 左下隅の x 座標
  ymin := 0 -- 左下隅の y 座標
  width := 450 -- 横方向のピクセル数
  height := 450 -- 縦方向のピクセル数
  xSize := 450 -- width と同じ値なので、ピクセル数と座標の値は一致
}

/-- ノードを描画するときの円の半径 -/
def radius := 16

/-- ノードを描画するときの円の塗りつぶし色(RGB) -/
def fillColor := (0.74, 0.87, 1.0)

/-- ノードのラベルのフォントサイズ -/
def fontsize := 14

/-- ノードのラベルの色(RGB) -/
def textColor := (0.0, 0.0, 0.0)

/-- 位置情報を付加したノードのデータ -/
structure NodePos where
  /-- ノードのx座標(右に行くほど大きい) -/
  x : Float
  /-- ノードのy座標(下に行くほど大きい) -/
  y : Float
  /-- ラベル -/
  label : String

/-- 上面と下面を反転して計測したy座標 -/
def NodePos.y_inv (self : NodePos) : Float :=
  defaultFrame.height.toFloat - self.y

/-- ノード(円とラベル)を作成する -/
def createNodeElement (node : NodePos) : Array (Element defaultFrame) :=
  let circle := circle (node.x, node.y_inv) (.px radius)
    |>.setFill fillColor
  let adjust := fontsize.toFloat * 0.35 -- ラベルの位置調整用
  let text := text (node.x - adjust, node.y_inv - adjust) node.label (.px fontsize)
    |>.setFill textColor
  #[circle, text]

/-- `createNodeElement` の表示結果をテストするための関数 -/
def createNodeHtml (node : NodePos) : Html :=
  let svg : Svg defaultFrame := { elements := createNodeElement node }
  svg.toHtml

-- ノードの描画テスト
#html createNodeHtml (node := { x := 150, y := 30, label := "A" })

/-- エッジの色(RGB) -/
def edgeColor := (50.0, 50.0, 50.0)

/-- エッジの太さ(ピクセル) -/
def edgeWidth := 2

/-- エッジ(ノードの親子関係)を作成する -/
def createEdgeElement (parent child : NodePos) : Element defaultFrame :=
  line (parent.x, parent.y_inv) (child.x, child.y_inv)
  |>.setStroke edgeColor (.px edgeWidth)

/-- `createEdgeElement` の表示結果をテストするための関数 -/
def createEdgeHtml (parent child : NodePos) : Html :=
  let svg : Svg defaultFrame := { elements := #[createEdgeElement parent child] }
  svg.toHtml

#html createEdgeHtml
  (parent := { x := 150, y := 30, label := "A" })
  (child := { x := 100, y := 80, label := "B" })

/-- (ラベル付きの)二分木 -/
inductive BinTree (α : Type) where
  /-- 空の木 -/
  | empty
  /-- ノード -/
  | node (val : α) (left right : BinTree α)

variable {α : Type} [ToString α]

/-- 二分木をノードの配列に変換する。 -/
def BinTree.toNodes (tree : BinTree α) : Array α :=
  match tree with
  | .empty => #[]
  | .node val left right => #[val] ++ (left.toNodes ++ right.toNodes)

/-- 二分木のエッジを配列にする。(親, 子) のペアにして返すことに注意。 -/
def BinTree.toEdges {β : Type} (tree : BinTree β) : Array (β × β) :=
  match tree with
  | .empty => #[]
  | .node a left right =>
    let leftEdges :=
      match left with
      | .empty => #[]
      | .node b _ _ => #[(a, b)] ++ toEdges left
    let rightEdges :=
      match right with
      | .empty => #[]
      | .node c _ _ => #[(a, c)] ++ toEdges right
    leftEdges ++ rightEdges

/-- 二分木のレイアウト情報。ラベルの情報に加えて、各ノードが描画されるべき位置の情報を持つ。 -/
abbrev BinTreeLayout (α : Type) := BinTree (α × Nat × Nat)

/-- 3つ組データを構造体の項に変換する -/
def NodePos.ofPair (p : α × Nat × Nat) : NodePos :=
  let (label, x, y) := p
  { x := x.toFloat, y := y.toFloat, label := toString label }

def BinTree.toElementsFromLayout (tree : BinTreeLayout α) : Array (Svg.Element defaultFrame) :=
  let nodes := tree.toNodes
    |>.map NodePos.ofPair
    |>.map createNodeElement
    |>.flatten
  let edges := tree.toEdges
    |>.map (fun ((v1, x1, y1), (v2, x2, y2)) => (NodePos.ofPair (v1, x1, y1), NodePos.ofPair (v2, x2, y2)))
    |>.map (fun (parent, child) => createEdgeElement parent child)
  edges ++ nodes

/-- 2分木の描画情報が与えられたときに、それを SVG 画像として描画する -/
def BinTree.toHtmlFromLayout (tree : BinTreeLayout α) : Html :=
  let svg : Svg defaultFrame := { elements := tree.toElementsFromLayout }
  svg.toHtml

-- 二分木の描画テスト
-- レイアウト情報を手動で与えて描画している
#html
  let treeLayout := BinTree.node ("A", (150, 30))
    (.node ("B", (100, 80)) .empty .empty)
    (.node ("C", (200, 80))
      (.node ("D", (170, 130)) .empty .empty)
      (.node ("E", (230, 130)) .empty .empty))
  BinTree.toHtmlFromLayout treeLayout

/-- グリッドの1刻み -/
def step := 30

/-- 2分木のレイアウト情報が渡されたときに、各ノードのレイアウト位置を一様にずらす -/
def BinTree.shift (tree : BinTreeLayout α) (shiftFn : Nat × Nat → Nat × Nat) : BinTreeLayout α :=
  match tree with
  | .empty => .empty
  | .node (a, (x, y)) left right =>
    let (x', y') := shiftFn (x, y)
    .node (a, (x', y')) (shift left shiftFn) (shift right shiftFn)

/-- 2分木の描画幅。二分木を描画したときに何グリッド占めるか。 -/
def BinTree.width (tree : BinTree α) : Nat :=
  tree.toNodes.size - 1

/-- 二分木のレイアウトを計算する関数 -/
def BinTree.layout (tree : BinTree α) : BinTreeLayout α :=
  match tree with
  | .empty => .empty
  | .node a .empty .empty =>
    .node (a, (step, step)) .empty .empty
  | .node a .empty right =>
    let rightLayout := layout right
    let rightShifted := rightLayout.shift (fun (x, y) => (x + step, y + step))
    .node (a, (step, step)) .empty rightShifted
  | .node a left .empty =>
    let leftLayout := layout left
    let leftShifted := leftLayout.shift (fun (x, y) => (x, y + step))
    .node (a, (left.width + 2) * step, step) leftShifted .empty
  | .node a left right =>
    let leftLayout := layout left
    let rightLayout := layout right
    let leftShifted := leftLayout.shift (fun (x, y) => (x, y + step))
    let rightShifted := rightLayout.shift (fun (x, y) => (x + (left.width + 2) * step, y + step))
    .node (a, ((left.width + 2) * step, step)) leftShifted rightShifted

/-- 二分木の葉 -/
def BinTree.leaf (val : α) : BinTree α :=
  .node val .empty .empty

-- 二分木の描画テスト
-- 二分木からレイアウト情報を計算し、それを元に描画している
#html
  let tree := BinTree.node "A"
    (BinTree.leaf "B")
    (BinTree.node "C"
      (BinTree.leaf "D")
      (BinTree.leaf "E"))
  BinTree.toHtmlFromLayout (BinTree.layout tree)