Commit 60c1f161 authored by André Espaze's avatar André Espaze
Browse files

Move to a Plotly web component for pure Elm rendering

parent 76eb8832076e
port module Plot exposing (main) module Plot exposing (main)
import Browser import Browser
import Common exposing (classes) import Common exposing (classes)
import Dict import Dict
import Either exposing (Either) import Either exposing (Either)
import Html.Styled exposing (..) import Html.Styled exposing (..)
import Html.Styled.Attributes as A
import Html.Styled.Events exposing (onClick) import Html.Styled.Events exposing (onClick)
import Http import Http
import Json.Decode as Decode exposing (Decoder) import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode
import KeywordMultiSelector import KeywordMultiSelector
import KeywordSelector import KeywordSelector
import LruCache exposing (LruCache) import LruCache exposing (LruCache)
...@@ -24,6 +26,7 @@ type alias Model = ...@@ -24,6 +26,7 @@ type alias Model =
, searchString : String , searchString : String
, searchedSeries : List String , searchedSeries : List String
, selectedSeries : List String , selectedSeries : List String
, selectedNamedSeries : List NamedSerie
, activeSelection : Bool , activeSelection : Bool
, cache : SeriesCache , cache : SeriesCache
} }
...@@ -56,8 +59,6 @@ type Msg ...@@ -56,8 +59,6 @@ type Msg
| ToggleItem String | ToggleItem String
| SearchSeries String | SearchSeries String
| MakeSearch | MakeSearch
| OnApply
| GotPlot (Result Http.Error String)
| RenderPlot (Result String ( SeriesCache, List NamedSerie )) | RenderPlot (Result String ( SeriesCache, List NamedSerie ))
...@@ -70,6 +71,17 @@ type alias Trace = ...@@ -70,6 +71,17 @@ type alias Trace =
} }
encodeTrace : Trace -> Encode.Value
encodeTrace t =
Encode.object
[ ( "type", Encode.string t.type_ )
, ( "name", Encode.string t.name )
, ( "x", Encode.list Encode.string t.x )
, ( "y", Encode.list Encode.float t.y )
, ( "mode", Encode.string t.mode )
]
type alias TraceArgs = type alias TraceArgs =
String -> List String -> List Float -> String -> Trace String -> List String -> List Float -> String -> Trace
...@@ -80,21 +92,22 @@ scatterPlot = ...@@ -80,21 +92,22 @@ scatterPlot =
type alias PlotArgs = type alias PlotArgs =
{ data : List Trace { div : String
, data : List Trace
} }
port renderPlot : PlotArgs -> Cmd msg encodePlotArgs : PlotArgs -> Encode.Value
encodePlotArgs x =
Encode.object
type alias RenderArgs = [ ( "div", Encode.string x.div )
{ plotlyResponse : String , ( "data", Encode.list encodeTrace x.data )
, selectedSeries : List String ]
, permalinkQuery : String
}
port renderPlotly : RenderArgs -> Cmd msg plotFigure : List (Attribute msg) -> List (Html msg) -> Html msg
plotFigure =
node "plot-figure"
fetchSeries : List String -> Model -> Task String ( SeriesCache, List NamedSerie ) fetchSeries : List String -> Model -> Task String ( SeriesCache, List NamedSerie )
...@@ -194,11 +207,6 @@ update msg model = ...@@ -194,11 +207,6 @@ update msg model =
else else
KeywordSelector.select xm xs |> List.take 20 KeywordSelector.select xm xs |> List.take 20
plotUrl =
UB.crossOrigin model.urlPrefix
[ "tsplot" ]
(List.map (\x -> UB.string "series" x) model.selectedSeries)
in in
case msg of case msg of
CatalogReceived (Ok x) -> CatalogReceived (Ok x) ->
...@@ -234,19 +242,7 @@ update msg model = ...@@ -234,19 +242,7 @@ update msg model =
newModel { model | searchedSeries = keywordMatch model.searchString model.series } newModel { model | searchedSeries = keywordMatch model.searchString model.series }
RenderPlot (Ok ( cache, namedSeries )) -> RenderPlot (Ok ( cache, namedSeries )) ->
let ( { model | cache = cache, selectedNamedSeries = namedSeries }, Cmd.none )
vals =
List.map
(\( name, serie ) ->
scatterPlot
name
(Dict.keys serie)
(Dict.values serie)
"lines"
)
namedSeries
in
( { model | cache = cache }, renderPlot <| PlotArgs vals )
RenderPlot (Err x) -> RenderPlot (Err x) ->
let let
...@@ -255,32 +251,6 @@ update msg model = ...@@ -255,32 +251,6 @@ update msg model =
in in
newModel model newModel model
OnApply ->
( model, Http.get { url = plotUrl, expect = Http.expectString GotPlot } )
GotPlot (Ok x) ->
let
validUrl =
Common.maybe
("http://dummy" ++ plotUrl)
(always plotUrl)
(Url.fromString plotUrl)
q =
validUrl
|> Url.fromString
|> Maybe.map (.query >> Maybe.withDefault "")
|> Maybe.withDefault ""
in
( model, renderPlotly <| RenderArgs x model.selectedSeries q )
GotPlot (Err x) ->
let
_ =
Debug.log "Error on GotPlot" x
in
newModel model
selectorConfig : KeywordMultiSelector.Config Msg selectorConfig : KeywordMultiSelector.Config Msg
selectorConfig = selectorConfig =
...@@ -292,12 +262,7 @@ selectorConfig = ...@@ -292,12 +262,7 @@ selectorConfig =
, toggleMsg = ToggleItem , toggleMsg = ToggleItem
} }
, actionSelector = , actionSelector =
{ action = { action = Nothing
Just
{ attrs = [ classes [ T.white, T.bg_dark_blue ] ]
, html = text "Apply"
, clickMsg = OnApply
}
, defaultText = text "" , defaultText = text ""
, toggleMsg = ToggleItem , toggleMsg = ToggleItem
} }
...@@ -309,27 +274,54 @@ selectorConfig = ...@@ -309,27 +274,54 @@ selectorConfig =
view : Model -> Html Msg view : Model -> Html Msg
view model = view model =
let let
cls = plotDiv =
classes [ T.pb2, T.f4, T.fw6, T.db, T.navy, T.link, T.dim ] "plotly_div"
args =
let
data =
List.map
(\( name, serie ) ->
scatterPlot
name
(Dict.keys serie)
(Dict.values serie)
"lines"
)
model.selectedNamedSeries
in
PlotArgs plotDiv data |> encodePlotArgs |> Encode.encode 0
children = selector =
[ a [ cls, onClick ToggleSelection ] [ text "Series selection" ] ] let
cls =
classes [ T.pb2, T.f4, T.fw6, T.db, T.navy, T.link, T.dim ]
ctx = children =
KeywordMultiSelector.Context [ a [ cls, onClick ToggleSelection ] [ text "Series selection" ] ]
model.searchString
model.searchedSeries
model.selectedSeries
in
div [ classes [ T.center, T.pt4, T.w_90 ] ]
(if model.activeSelection then
List.append children
[ KeywordMultiSelector.view selectorConfig ctx
]
else ctx =
children KeywordMultiSelector.Context
) model.searchString
model.searchedSeries
model.selectedSeries
in
form [ classes [ T.center, T.pt4, T.w_90 ] ]
(if model.activeSelection then
List.append children
[ KeywordMultiSelector.view selectorConfig ctx
]
else
children
)
in
div [ classes [ T.bg_light_blue ] ]
[ header [] [ selector ]
, div [ A.id plotDiv ] []
, plotFigure [ A.attribute "args" args ] []
, footer [] []
]
main : Program String Model Msg main : Program String Model Msg
...@@ -352,7 +344,7 @@ main = ...@@ -352,7 +344,7 @@ main =
c = c =
LruCache.empty 100 LruCache.empty 100
in in
( Model p [] "" [] [] True c, initialGet p ) ( Model p [] "" [] [] [] True c, initialGet p )
sub model = sub model =
if model.activeSelection then if model.activeSelection then
......
...@@ -28,6 +28,27 @@ ...@@ -28,6 +28,27 @@
<link rel="stylesheet" href="./tsview_static/style.css"> <link rel="stylesheet" href="./tsview_static/style.css">
<script src="./tsview_static/util.js"></script> <script src="./tsview_static/util.js"></script>
<script src="https://unpkg.com/@webcomponents/custom-elements@1.2.1/custom-elements.min.js">
</script>
<script>
class PlotFigure extends HTMLElement {
static get observedAttributes() {
return ['args'];
}
attributeChangedCallback(name, old_value, new_value) {
if ( name == 'args' ) {
let args = JSON.parse(new_value);
Plotly.newPlot(
args.div,
args.data,
{showlegend: true},
{displaylogo: false, modeBarButtonsToRemove: ["sendDataToCloud"]}
);
}
}
}
window.customElements.define("plot-figure", PlotFigure);
</script>
</head> </head>
{% block body %} {% block body %}
......
...@@ -2,45 +2,14 @@ ...@@ -2,45 +2,14 @@
{% block body %} {% block body %}
<form id="tsviewform" class="form-inline"> <div id="app"></div>
<div id="series_selector"></div> <script src="./tsview_static/plot_elm.js"></script>
<script src="./tsview_static/plot_elm.js"></script> <script>
<script>
const baseurl = "{{ homeurl }}" const baseurl = "{{ homeurl }}"
var app = Elm.Plot.init({ var app = Elm.Plot.init({
node: document.getElementById("series_selector"), node: document.getElementById("app"),
flags: baseurl flags: baseurl
}); });
app.ports.renderPlot.subscribe(function(args) {
Plotly.newPlot(
"plot",
args.data.map(function(o) {
o.type = o.type_;
return o;
}),
{showlegend: true},
{displaylogo: false, modeBarButtonsToRemove: ["sendDataToCloud"]}
)
});
app.ports.renderPlotly.subscribe(function(args) {
let $target = $('#output')
$target.html(args.plotlyResponse)
$target.append(`<a href="tsview?${args.permalinkQuery}">Permalink</a>`)
args.selectedSeries.forEach(function(name){
$target.append(`<br/> <a href="tshistory/${name}" target=_blank>View ${name} history</a>`)
});
});
</script>
</form>
<div id="plot" class="plotly-graph-div"></div>
<div id="output" style="margin-top: 1em; padding-bottom:2em;">
No data yet.
</div>
<script>
init_form('tsviewform', 'tsplot');
</script> </script>
{% endblock %} {% endblock %}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment