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:
- Tracks the current viewport position
- On scroll, calculates which regions are newly exposed
- Encodes only the new data for those regions
- 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
Functions
Returns the payload for an initial full render.
Use this in mount/3 to send the initial chart state.
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 tobuild_elementasmarginin 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
)
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.
@spec scroll(t(), scroll_offset()) :: {String.t(), map(), t()}
Handles a scroll event, returning the incremental update payload.
Parameters
state- Current incremental statedelta-{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,:modekeys;modeis"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, whosebinaryis empty and which the client ignoresnew_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)
Updates the data in the incremental state.
Use this when the underlying data changes and you need to refresh.