Visualize.Incremental (Visualize v0.2.25)

Copy Markdown View Source

High-level API for incremental canvas rendering with scroll optimization.

Use this module when building scrolling/panning visualizations to minimize data transfer and client-side rendering work.

How it works

Instead of sending the entire chart each frame, incremental rendering:

  1. Tracks the current viewport position
  2. On scroll, calculates which regions are newly exposed
  3. Encodes only the new data for those regions
  4. Client copies existing canvas content shifted, then renders new data

Every entry point returns the event name "canvas_incremental" with a payload whose mode is "full", "scroll_x", "scroll_y", "scroll_xy" or "none" (a zero delta: nothing to draw, nothing to push). render_viewport/1 and initial_render/1 hand the whole dataset to build_element with the viewport, canvas size and margin in the options map; it is the callback that decides what a full frame shows. Region builds receive the data filtered to the exposed region.

Example Usage

# Initialize state in mount/3
def mount(_params, _session, socket) do
  state = Visualize.Incremental.new(
    canvas_size: {800, 600},
    data: load_data(),
    x_accessor: &Map.get(&1, :x),
    y_accessor: &Map.get(&1, :y),
    build_element: &build_line_element/2
  )

  {:ok, assign(socket, incremental: state)}
end

# Handle scroll events
def handle_event("scroll", %{"dx" => dx, "dy" => dy}, socket) do
  state = socket.assigns.incremental

  {event_type, payload, new_state} =
    Visualize.Incremental.scroll(state, {dx, dy})

  socket =
    socket
    |> assign(:incremental, new_state)
    |> push_event(event_type, payload)

  {:noreply, socket}
end

Summary

Functions

Returns the payload for an initial full render.

Creates a new incremental rendering context.

Renders the full viewport, for initial render or forced refresh.

Handles a scroll event, returning the incremental update payload.

Updates the data in the incremental state.

Types

bounds()

@type bounds() ::
  {x :: number(), y :: number(), width :: number(), height :: number()}

scroll_offset()

@type scroll_offset() :: {dx :: number(), dy :: number()}

t()

@type t() :: %Visualize.Incremental{
  build_element: (list(), map() -> Visualize.IR.Element.t()),
  canvas_height: number(),
  canvas_width: number(),
  data: list(),
  margin: map(),
  viewport_x: number(),
  viewport_y: number(),
  x_accessor: (any() -> number()),
  y_accessor: (any() -> number())
}

Functions

initial_render(state)

@spec initial_render(t()) :: {String.t(), map(), t()}

Returns the payload for an initial full render.

Use this in mount/3 to send the initial chart state.

new(opts)

@spec new(keyword()) :: t()

Creates a new incremental rendering context.

Options

  • :canvas_size - Required. {width, height} of the canvas in pixels.
  • :data - Required. The full dataset.
  • :x_accessor - Required. Function to extract x coordinate from data point.
  • :y_accessor - Required. Function to extract y coordinate from data point.
  • :build_element - Required. Function (data, opts) -> Element.t() to build the element tree for a subset of data.
  • :viewport_x - Initial x offset (default: 0).
  • :viewport_y - Initial y offset (default: 0).
  • :margin - Chart margins %{top:, right:, bottom:, left:} (default: all 0), passed to build_element as margin in its options map.

Example

Visualize.Incremental.new(
  canvas_size: {800, 600},
  data: my_data,
  x_accessor: &(&1.timestamp),
  y_accessor: &(&1.value),
  build_element: fn data, _opts ->
    # Build your chart element from the data subset
    build_line_chart(data)
  end
)

render_viewport(state)

@spec render_viewport(t()) :: {binary(), t()}

Renders the full viewport, for initial render or forced refresh.

Returns

A tuple of {binary, new_state} where binary is the full chart encoded for canvas rendering.

scroll(state, arg)

@spec scroll(t(), scroll_offset()) :: {String.t(), map(), t()}

Handles a scroll event, returning the incremental update payload.

Parameters

  • state - Current incremental state
  • delta - {dx, dy} scroll delta in pixels

Returns

A tuple of {event_type, payload, new_state} where:

  • event_type - Always "canvas_incremental"
  • payload - Map with :binary, :dx, :dy, :mode keys; mode is "full" (the scroll was too large: the whole viewport is re-sent), "scroll_x", "scroll_y" or "scroll_xy" (copy-shift plus the exposed regions), or "none" for a zero delta, whose binary is empty and which the client ignores
  • new_state - Updated state with new viewport position

Example

{event_type, payload, new_state} = Visualize.Incremental.scroll(state, {20, 0})
push_event(socket, event_type, payload)

update_data(state, new_data)

@spec update_data(t(), list()) :: t()

Updates the data in the incremental state.

Use this when the underlying data changes and you need to refresh.