Observable Notebook как среда для прототипирования

Observable Notebook представляет собой интерактивную среду разработки, ориентированную на визуализацию данных, исследовательское программирование и быстрое прототипирование. Платформа особенно тесно связана с экосистемой D3.js, поскольку её создатель — Майк Босток, автор библиотеки D3.

Главное отличие Observable от традиционных редакторов JavaScript заключается в реактивной модели вычислений. Код выполняется не сверху вниз, а как сеть зависимостей между ячейками.

Каждая ячейка notebook:

  • содержит самостоятельный фрагмент кода;
  • автоматически экспортирует результат;
  • может использовать значения других ячеек;
  • пересчитывается при изменении зависимостей.

Простейший пример:

width = 800

height = width / 2

area = width * height

При изменении width автоматически пересчитываются height и area.

Такой подход особенно полезен для визуализации данных, поскольку графики мгновенно обновляются при изменении параметров, фильтров или входных данных.


Структура notebook

Notebook состоит из независимых ячеек.

Типы ячеек:

  • JavaScript-ячейки;
  • Markdown-ячейки;
  • HTML-ячейки;
  • визуальные компоненты;
  • импортируемые модули;
  • интерактивные контролы.

Пример markdown-ячейки:

# График продаж

Пример HTML:

html`<div class="container"></div>`

Пример вычисляемой ячейки:

data = [
  {x: 10, y: 20},
  {x: 30, y: 50}
]

Каждая переменная становится доступной глобально внутри notebook.


Подключение D3.js

В Observable D3 обычно уже доступен через встроенный импорт:

d3 = require("d3@7")

Современный вариант через ES Modules:

import * as d3 from "d3"

После импорта доступны все модули библиотеки:

  • d3-selection
  • d3-scale
  • d3-axis
  • d3-shape
  • d3-array
  • d3-force
  • d3-geo
  • d3-transition
  • и другие

Observable автоматически кэширует зависимости и управляет загрузкой модулей.


Создание SVG в Observable

В классическом JavaScript SVG обычно создаётся через DOM API или append-цепочки. В Observable распространён подход с возвратом готового DOM-узла из ячейки.

Пример:

svg = d3.create("svg")
    .attr("width", 600)
    .attr("height", 400)

svg.node()

Метод d3.create() создаёт изолированный элемент, который затем отображается notebook-средой.


Реактивное обновление графиков

Одно из ключевых преимуществ Observable — автоматическое обновление визуализаций.

Пример:

width = 800

svg = d3.create("svg")
    .attr("width", width)
    .attr("height", 200)

svg.append("rect")
    .attr("width", width)
    .attr("height", 100)

svg.node()

При изменении width SVG пересоздаётся автоматически.

В традиционном приложении пришлось бы:

  • отслеживать изменение состояния;
  • вручную обновлять DOM;
  • контролировать очистку старых элементов.

Observable выполняет эти задачи автоматически.


Использование viewof

Ключевая особенность Observable — механизм viewof.

Он позволяет связывать пользовательский интерфейс с реактивными данными.

Пример:

viewof radius = Inputs.range([10, 100], {
  value: 40,
  step: 1,
  label: "Радиус"
})

Значение доступно через переменную:

radius

Теперь можно использовать её в визуализации:

svg = d3.create("svg")
    .attr("width", 300)
    .attr("height", 300)

svg.append("circle")
    .attr("cx", 150)
    .attr("cy", 150)
    .attr("r", radius)

svg.node()

Ползунок мгновенно изменяет размер круга.


Inputs и интерактивные элементы

Observable содержит встроенную библиотеку Inputs.

Основные элементы:

Компонент Назначение
Inputs.range() Ползунок
Inputs.select() Выпадающий список
Inputs.checkbox() Флажки
Inputs.toggle() Переключатель
Inputs.button() Кнопка
Inputs.text() Текстовый ввод
Inputs.table() Таблица
Inputs.search() Поиск

Пример списка:

viewof color = Inputs.select(
  ["red", "green", "blue"],
  {label: "Цвет"}
)

Использование:

svg = d3.create("svg")
    .attr("width", 200)
    .attr("height", 200)

svg.append("rect")
    .attr("width", 100)
    .attr("height", 100)
    .attr("fill", color)

svg.node()

Работа с данными

Observable упрощает загрузку данных.

CSV

data = FileAttachment("sales.csv").csv()

JSON

data = FileAttachment("data.json").json()

Текст

text = FileAttachment("notes.txt").text()

Изображения

img = FileAttachment("map.png").image()

После загрузки данные становятся реактивными.


Трансформация данных

Observable хорошо подходит для исследовательского анализа данных.

Пример группировки:

grouped = d3.group(data, d => d.category)

Агрегация:

totals = d3.rollup(
  data,
  v => d3.sum(v, d => d.value),
  d => d.category
)

Сортировка:

sorted = data.slice().sort((a, b) => b.value - a.value)

Фильтрация:

filtered = data.filter(d => d.value > 100)

Каждая трансформация автоматически обновляет графики.


Генерация масштабов

Observable идеально подходит для экспериментирования с масштабами D3.

Линейная шкала:

x = d3.scaleLinear()
    .domain([0, 100])
    .range([0, 500])

Категориальная шкала:

color = d3.scaleOrdinal()
    .domain(["A", "B", "C"])
    .range(["red", "green", "blue"])

Временная шкала:

x = d3.scaleTime()
    .domain([
      new Date("2024-01-01"),
      new Date("2024-12-31")
    ])
    .range([0, 700])

Observable позволяет мгновенно проверять влияние изменений на визуализацию.


Построение осей

Пример оси X:

svg.append("g")
    .attr("transform", "translate(0,300)")
    .call(d3.axisBottom(x))

Ось Y:

svg.append("g")
    .call(d3.axisLeft(y))

Реактивность Observable особенно полезна при настройке:

  • диапазонов;
  • тиков;
  • форматирования;
  • временных интервалов;
  • локализации.

Построение line chart

Полный пример:

svg = {
  const width = 700
  const height = 400
  const margin = 40

  const svg = d3.create("svg")
      .attr("width", width)
      .attr("height", height)

  const x = d3.scaleLinear()
      .domain(d3.extent(data, d => d.x))
      .range([margin, width - margin])

  const y = d3.scaleLinear()
      .domain(d3.extent(data, d => d.y))
      .range([height - margin, margin])

  const line = d3.line()
      .x(d => x(d.x))
      .y(d => y(d.y))

  svg.append("path")
      .datum(data)
      .attr("fill", "none")
      .attr("stroke", "steelblue")
      .attr("stroke-width", 2)
      .attr("d", line)

  svg.append("g")
      .attr("transform", `translate(0,${height - margin})`)
      .call(d3.axisBottom(x))

  svg.append("g")
      .attr("transform", `translate(${margin},0)`)
      .call(d3.axisLeft(y))

  return svg.node()
}

Блоковая форма { ... } позволяет использовать локальные переменные внутри ячейки.


Анимации и transitions

Observable прекрасно подходит для исследования анимаций.

Пример:

svg.select("circle")
    .transition()
    .duration(1000)
    .attr("r", 80)

Реактивная модель позволяет создавать управляемые анимации:

viewof duration = Inputs.range([100, 5000], {
  value: 1000,
  label: "Длительность"
})

Использование:

.transition()
.duration(duration)

Асинхронный код

Observable поддерживает async/await напрямую в ячейках.

Пример:

data = await d3.json("https://example.com/data.json")

Также поддерживаются:

  • Promise;
  • fetch API;
  • стриминг данных;
  • WebSocket;
  • генераторы.

Observable автоматически ожидает завершения промисов.


Генераторы и анимационные циклы

Особая возможность Observable — поддержка генераторов.

Пример:

counter = {
  let i = 0

  while (true) {
    yield i++
    await Promises.delay(1000)
  }
}

Значение обновляется каждую секунду.

Использование в графике:

text = html`<div>${counter}</div>`

Генераторы часто применяются для:

  • таймеров;
  • потоковых данных;
  • live-графиков;
  • симуляций;
  • force-directed layout.

Mutable и изменяемое состояние

По умолчанию Observable ориентирован на декларативную реактивность. Однако иногда требуется изменяемое состояние.

Для этого используется Mutable.

Пример:

mutable count = 0

Изменение:

mutable count++

Получение значения:

count

Такой механизм полезен при:

  • обработке событий;
  • анимациях;
  • drag-and-drop;
  • ручном обновлении данных.

Инкапсуляция через модули

Observable поддерживает импорт ячеек из других notebook.

Пример:

import {chart} from "@user/charts"

Импорт конкретной функции:

import {legend} from "@d3/color-legend"

Преимущества:

  • повторное использование кода;
  • публикация компонентов;
  • создание библиотек;
  • разделение логики;
  • совместная разработка.

Observable Plot

Observable активно использует библиотеку Observable Plot — высокоуровневую надстройку над D3.

Пример:

Plot.plot({
  marks: [
    Plot.barY(data, {
      x: "category",
      y: "value"
    })
  ]
})

Plot упрощает:

  • создание типовых графиков;
  • агрегацию;
  • faceting;
  • binning;
  • legends;
  • scale management.

При необходимости всегда можно перейти к низкоуровневому D3.


Работа с DOM

Observable позволяет напрямую возвращать DOM-узлы.

Пример:

element = {
  const div = document.createElement("div")
  div.textContent = "Hello"
  return div
}

Либо через tagged templates:

html`
  <div class="card">
    <h2>Статистика</h2>
  </div>
`

SVG и Canvas

Observable поддерживает обе модели рендеринга.

SVG

Подходит для:

  • интерактивности;
  • небольших наборов данных;
  • осей;
  • hover-эффектов.

Canvas

Подходит для:

  • десятков тысяч объектов;
  • heatmap;
  • scatterplot большого объёма;
  • анимации.

Пример canvas:

canvas = {
  const canvas = document.createElement("canvas")
  canvas.width = 800
  canvas.height = 400

  const context = canvas.getContext("2d")

  context.fillStyle = "steelblue"
  context.fillRect(10, 10, 200, 100)

  return canvas
}

Force Simulation

Observable особенно популярен для force-directed графов.

Пример:

simulation = d3.forceSimulation(nodes)
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(400, 300))
    .force("link", d3.forceLink(links))

Реактивная среда позволяет:

  • изменять силы в реальном времени;
  • тестировать параметры;
  • отслеживать состояние симуляции;
  • подключать интерактивные контролы.

Отладка визуализаций

Observable существенно упрощает debugging.

Можно:

  • выводить промежуточные массивы;
  • проверять scale;
  • тестировать accessor-функции;
  • инспектировать DOM;
  • быстро менять параметры.

Пример:

x.domain()

или:

data.slice(0, 10)

Каждая ячейка служит отдельной точкой анализа.


Публикация notebook

Observable поддерживает публикацию notebook как веб-страницы.

Возможности:

  • публичные notebook;
  • приватные notebook;
  • совместное редактирование;
  • встраивание графиков;
  • экспорт кода;
  • публикация статических версий.

Notebook может выступать одновременно:

  • исследовательской средой;
  • документацией;
  • интерактивной статьёй;
  • демонстрацией алгоритма;
  • полноценным приложением.

Экспорт в standalone-приложения

Observable-проекты можно переносить в обычные JavaScript-приложения.

Распространённые подходы:

  • экспорт ячеек;
  • использование Observable Runtime;
  • перенос логики в ES Modules;
  • интеграция с React/Vue/Svelte;
  • публикация через npm.

Пример runtime:

import {Runtime, Inspector} from "@observablehq/runtime"

Observable Runtime

Observable Runtime позволяет запускать notebook вне платформы Observable.

Пример:

const runtime = new Runtime()

runtime.module(notebook, Inspector.into("#app"))

Это делает возможным:

  • внедрение notebook в сайты;
  • использование notebook как модулей;
  • серверный рендеринг;
  • интеграцию с frontend-framework.

Архитектурные преимущества Observable для D3

Observable особенно эффективен при разработке D3-проектов благодаря нескольким особенностям:

Реактивность

Автоматическая синхронизация зависимостей избавляет от ручного управления состоянием.

Интерактивность

UI-контролы встроены непосредственно в среду.

Изолированные вычисления

Каждая ячейка независима.

Исследовательская модель

Notebook идеально подходит для data exploration.

Встроенная публикация

Результат легко превратить в интерактивную документацию.

Быстрое прототипирование

Изменения сразу отражаются на графиках.

Простота экспериментов

Можно быстро сравнивать:

  • шкалы;
  • алгоритмы;
  • способы агрегации;
  • визуальные представления;
  • параметры анимации.

Ограничения Observable

Несмотря на преимущества, существуют ограничения.

Не всегда подходит для крупных production-приложений

Notebook-архитектура ориентирована прежде всего на исследование и прототипирование.

Сложность контроля жизненного цикла

При сложных взаимозависимостях возможны неожиданные реактивные обновления.

Ограниченная структура проекта

Notebook менее удобен для:

  • крупных команд;
  • сложной модульной архитектуры;
  • enterprise-разработки.

Переход к классическому приложению

Часто требуется перенос кода в:

  • React;
  • Vue;
  • Svelte;
  • чистые ES Modules.

Типичный workflow разработки D3 через Observable

Наиболее распространённый сценарий:

  1. Загрузка данных.
  2. Исследование структуры данных.
  3. Создание scale.
  4. Построение базового графика.
  5. Добавление интерактивности.
  6. Эксперименты с анимацией.
  7. Настройка производительности.
  8. Выделение reusable-компонентов.
  9. Перенос в production-код.

Observable особенно ценен на этапах исследования и проектирования визуализации, где скорость итераций важнее строгой архитектуры приложения.