szubtsovskiy / elm-visualization / Zoom

This module implements a convenient abstraction for panning and zooming: it lets the user focus on a regions of interest in a visualization. It is quite intuitive in that dragging the mouse coresponds to panning, mouse wheel zooming, and touch interactions are also supported.

The implementation is agnostic about the DOM, so it can be used with HTML, SVG, or WebGL.

Setting up zooming in your app

While there are many ways to use this module, a typical setup will look like this:

import Zoom exposing (OnZoom, Zoom)

type alias Model =
    { zoom : Zoom
    }

type Msg
    = ZoomMsg OnZoom

-- ...

Next, initialize and configure the zoom model:

init : () -> ( Model, Cmd Msg )
init () =
    ( { zoom = Zoom.init { width = width, height = height } }
    , Cmd.none
    )

Note that you will need to provide the width and height of the element that you want to setup the zooming behavior for. If you don't know this information, then you might want to use Browser.Dom.getElement to get it.

Next setup a subscription:

subscriptions : Model -> Sub Msg
subscriptions model =
    Zoom.subscriptions model.zoom ZoomMsg

Then handle update:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        ZoomMsg zoomMsg ->
            ( { model
                | zoom = Zoom.update zoomMsg model.zoom
              }
            , Cmd.none
            )

Finally, set up your view:

view : Model -> Html Msg
view model =
    svg
        (A.width width
            :: A.height height
            :: Zoom.transform model.zoom
            :: Zoom.events model.zoom ZoomMsg
        )
        [ myChildrenElements ]

Configuring the zoom behavior


type Zoom

This type will go into your model as it stores the internal state and configuration necessary to support the various user interactions.

init : { width : Basics.Float, height : Basics.Float } -> Zoom

Creates a brand new Zoom. You have to pass in the dimensions (in pixels) of the element that you want to make zoom-eable.

scaleExtent : Basics.Float -> Basics.Float -> Zoom -> Zoom

Allows you to set a minimum and maximum scale that the user will be able to zoom to.

Typically there is only limited resolution to the data, so setting the maximum such that the maximum resolution is comfortably visible (remember accessibility - some people will want to zoom a fair bit more than you might find necessary) would be a good idea.

The miminum zoom will always be asymptotically approaching zero, but setting a higher number is good, because the view can be "lost" if it gets too small. Typically you would set it such that the whole dataset fits into the view.

translateExtent : ( ( Basics.Float, Basics.Float ), ( Basics.Float, Basics.Float ) ) -> Zoom -> Zoom

Allows you to set a boundary where a user will be able to pan to. The format is ((left, top), (right, bottom)).

Typically you will want to set this to ((0, 0), (width, height)), however you can restrict it however you like. For example maps typically only restrict vertical movement, but not horizontal movement.

Updating the zoom


type OnZoom

This is the Msg type used. You will want to pass these to Zoom.update.

Note that when handling these messages, it is also extremely likely that the zoom transform has somehow changed, so you can also use that place in your update function to react to that. For example in a map application, you may want to fetch a different tile based on the zoom level:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Zoomed onZoom ->
            let
                oldTransform =
                    Zoom.asRecord model.zoom

                newZoom =
                    Zoom.update onZoom model.zoom

                newTransform =
                    Zoom.asRecord newZoom

                cmd =
                    if toTileCoords oldTransform /= toTileCoords newTransform then
                        fetchTile (toTileCoords newTransform)

                    else
                        Cmd.none
            in
            ( { model | zoom = newZoom }, cmd )

update : OnZoom -> Zoom -> Zoom

This is what you need to set up in your update function.

subscriptions : Zoom -> (OnZoom -> msg) -> Platform.Sub.Sub msg

Subrscriptions are used for allowing drags to continue outside the element as well for animated zooms on double-click.

Manipulating the zoom transform programmatically

setTransform : TransitionOption -> { scale : Basics.Float, translate : { x : Basics.Float, y : Basics.Float } } -> Zoom -> Zoom

Change the zoom transform programmatically.


type TransitionOption

Enumerates the ways a zoom transform can be changed.

instantly : TransitionOption

Changes the zoom transform instantly.

animatedAround : ( Basics.Float, Basics.Float ) -> TransitionOption

Animates the zoom transform minimizing movement around the specified point.

View

transform : Zoom -> Svg.Attribute msg

A convenience for setting up the tranform attribute for SVG elements.

asRecord : Zoom -> { scale : Basics.Float, translate : { x : Basics.Float, y : Basics.Float } }

Returns the actual transform for the view relative to the top left corner. You can then use these numbers to transform the view as necessary.

Handling User Events

events : Zoom -> (OnZoom -> msg) -> List (Svg.Attribute msg)

Sets up the event handlers necessary to support this behavior on various devices.

It is merely a convenience, implemented such:

events zoom tagger =
    Zoom.onDoubleClick zoom tagger
        :: Zoom.onWheel zoom tagger
        :: Zoom.onDrag zoom tagger
        ++ Zoom.onGesture zoom tagger
        ++ Zoom.onTouch zoom tagger

So if you want to customize the user experience, you can for example omit the onWheel handler in your own defintion.

onDoubleClick : Zoom -> (OnZoom -> msg) -> Svg.Attribute msg

Zooms in on double click, zooms out on double click while holding shift.

onWheel : Zoom -> (OnZoom -> msg) -> Svg.Attribute msg

Zooms on mousewheel.

onDrag : Zoom -> (OnZoom -> msg) -> List (Svg.Attribute msg)

Allows panning on mouse drag.

onGesture : Zoom -> (OnZoom -> msg) -> List (Svg.Attribute msg)

Supports pinch to zoom on desktop Safari.

onTouch : Zoom -> (OnZoom -> msg) -> List (Svg.Attribute msg)

Supports pinch to zoom on mobile devices.