GenieFramework / StippleUI.jl

StippleUI is a library of reactive UI elements for Stipple.jl.
MIT License
82 stars 15 forks source link

WIP: Multi-handle slider / vue-slider-component / array transmission #10

Open hhaensel opened 3 years ago

hhaensel commented 3 years ago

@essenciary These days I had the need for a slider with multiple handles. After some search I landed with vue-slider-component by @nightcatsama

It's a really nice component, but it is not part of the quasar library. Do you think it makes sense to include it nevertheless?

In this context I experienced two problems:

This latter poses a general problem for Stipple. Typed arrays are not correctly handled by Stipple and the only way is to use arrays of type any.

I propose a solution along the following lines:

newval = if valtype <: Array && eltype(val) != Any
    if typeof(payload["newval"]) <: Array
        convert.(eltype(val), payload["newval"])
    else
        [convert(eltype(val), payload["newval"])]
    end
else
    # continue with the normal Base.parse ... 
end

We could also hide this part in the Base.parse ... What do you think?

P.S.: If there are more complex data types, such as plots it gets even more difficult. If you change a ploton the front end, Stipple spits errors, because the chart data are not correctly resolved to julian data types. Solving this would be even more complicated. Perhaps you have an idea? Of, course read-only types would help here...

essenciary commented 3 years ago

@hhaensel Thanks for the updates. The slider looks great and it doesn't seem to have large dependencies so it makes total sense to add to Stipple. Can you please share an example that I can test myself? Just brainstorming, I'm thinking in the line of adding some type hints (metadata) with the payload so that we somehow know how the data should be converted and maybe having a conversion API?

hhaensel commented 3 years ago

after some tweaking it looks quite similar to the quasar library ...

image

essenciary commented 3 years ago

It looks lovely 😍

hhaensel commented 3 years ago

Here's an example without any encapsulation, mainly plain HTML ...

using Stipple
using StippleCharts
using StippleUI
using Genie, Genie.Router, Genie.Renderer.Html

import Genie.Renderer.Json.JSONParser.JSONText

using Colors

OptDict = Dict{Symbol, Any}
opts(;kwargs...) = OptDict(kwargs...)

const plot_opts_dict = OptDict(
    :chart => OptDict(:type => :line),
    :xaxis => OptDict(:type => :numeric),
    :yaxis => OptDict(:min => -5, :max => 5, :tickAmount => 10,
                      :labels => OptDict(:formatter => JSONText("function(val, index) { return val.toFixed(1); }"))
              )
)

rgb(c::Colorant) = Int.(255 .* (red(c), green(c), blue(c)))

Colors.color_names["var(--q-color-primary)"] = rgb(colorant"#0779e4")
Colors.color_names["var(--q-color-secondary)"] = rgb(colorant"#6cb1f2")

defaultcolors = [RGBA(0,143/255,251/255,0.85), RGBA(0,227/255,150/255,0.85), RGBA(254/255,176/255,25/255,0.85)]

setopacity(c::AbstractString, alpha) = setopacity(parse(Colorant, c), alpha)

function setopacity(c::Colorant, alpha)
    cc = RGBA(c)
    cc = RGBA(cc.r, cc.g, cc.b, convert(typeof(cc.r), cc.alpha .* alpha))
    "#" * lowercase(Colors.hex(cc, :AUTO))
end

function dotOptions(color::Colorant;
     bordercolor = color, borderradius = "50%", fontsize = "11px", transform = "",
     boxshadow = "", transition="100ms")
     bc = typeof(bordercolor) <: Color ? lowercase("#" * Colors.hex(bordercolor, :AUTO)) : setopacity(bordercolor, 1)
     dotOptions(lowercase("#" * Colors.hex(color, :AUTO));
        bordercolor = bc, borderradius = borderradius, fontsize = fontsize, transform = transform,
        boxshadow = boxshadow, transition = transition)
end

function dotOptions(color::AbstractString="var(--q-color-primary)";
     bordercolor = color, borderradius = "50%", fontsize = "11px", transform = "",
     boxshadow = "", transition="100ms")
     hovercolor = try
         setopacity(color, 0.5)
     catch ex
         color
     end
     @info color, hovercolor
    """
    {
        style: {
            'background-color': '$color',
            'color': '$hovercolor',
            'border-radius': '$borderradius',
            'transition': '$transition'
        },
        focusStyle: {
            'transform': '$transform',
            'box-shadow': '$boxshadow',
            'transition': '$transition'
        },
        tooltipStyle: {
            'background-color': '$color',
            'border-color': '$color',
            'font-size' : '11px',
            'font-family': 'Lato,sans-serif',
            'font-weight': '700',
        }
    }
    """
end

xx = Base.range(0, 4π, length=200) |> collect

Base.@kwdef mutable struct HHDashboard <: ReactiveModel
    name::R{String} = "World"
    a::R{Array{Any}} = [1.0, 2, 2.5]
    b::R{Array{Any}} = [0.0, π/6, π/3]
    c::R{Float64} = 0.0
    plot_data::R{Vector{PlotSeries}} = [PlotSeries(t, PlotData(zip(xx, a .* sin.(xx .- b) .+ c) |> collect)) for (t, a, b) in collect(zip(["Sine 1", "Sine 2", "Sine 3"], a, b))]
    plot_options_dict::OptDict = plot_opts_dict
    js_code::R{String} = ""
end

Stipple.register_components(HHDashboard, StippleCharts.COMPONENTS)
Stipple.register_components(HHDashboard, [:vueSlider => Symbol("window[ 'vue-slider-component' ]")])

# model = Stipple.init(HHDashboard())
models = Dict{String, ReactiveModel}()

row_module(args...) = row(cell(class="st-module", args...))

labeled_slider(label::AbstractString, size, args...; kwargs...) = row([cell(size=size, h6(label)), cell(slider(args...; kwargs...))])

function ui(user)
    channel = string(hash(user))

    model = if haskey(models, channel)
        models[channel]
    else
        model = models[channel] = Stipple.init(HHDashboard(), channel = channel)

        onany(model.a, model.b, model.c) do a, b, c
            @info "amplitude: $a, phase: $b, offset: $c"
            model.plot_data[] = [PlotSeries(t, PlotData(zip(xx, a .* sin.(xx .- b) .+ c) |> collect)) for (t, a, b) in collect(zip(["Sine 1", "Sine 2", "Sine 3"], a, b))]
        end

        return model
    end
    # update plot_data when a, b or c are changed

    db = dashboard(vm(model), class="container", [
        heading("Stipple x-y Line Plot"),
        row_module([
            h2(["Hello ", span("", @text(:name)), "!"]),
            p("I am $user")
        ]),
        row_module(
            row([
                cell(class="st-br st-ph", [
                    h5("What is your name?"),
                    textfield("", :name, placeholder="type your name", label="Name", outlined="", filled="")
                ]),
                cell(class="st-br st-ph", [
                    h5("Sine oder Cosine?"),
                    row([
                        cell(size=3, h6("Amplitude")),cell(
                        """
                        <vue-slider
                            class="q-slider__pin-text q-slider__pin-value-marker-text"
                            style="padding-top: 25px;"
                            v-model="a"
                            height="3px"
                            width="100%"
                            min="0"
                            max="3"
                            interval="0.1"
                            :order="false"
                            :dot-size="15"
                            :tooltip="'always'"
                            :dot-options = "[
                            $(join(dotOptions.(defaultcolors;
                                borderradius="25%", transform="rotate(45deg)"), ","))
                            ]"
                            :process = "dotsPos => [[(dotsPos.length > 1) ? Math.min(...dotsPos) : 0, Math.max(...dotsPos), { 'background-color': 'var(--q-color-primary)' }]]"
                        ></vue-slider>
                        """)
                    ]),
                    row([
                        cell(size=3, h6("Phase")),cell(
                        """
                        <vue-slider
                            class="q-slider__pin-text q-slider__pin-value-marker-text"
                            style="padding-top: 25px;"
                            v-model="b"
                            height="3px"
                            width="100%"
                            min="0"
                            max="3"
                            interval="0.1"
                            :order="false"
                            :dot-size="15"
                            :tooltip="'always'"
                            :dot-options = "[
                                $(join(dotOptions.(defaultcolors;
                                    borderradius="25%", transform="rotate(45deg)"), ","))                            ]"
                            :process = "dotsPos => [[(dotsPos.length > 1) ? Math.min(...dotsPos) : 0, Math.max(...dotsPos), { 'background-color': 'var(--q-color-primary)' }]]"
                        ></vue-slider>
                        """)
                    ]),
                    labeled_slider("Offset",    3, -3:0.01:3, :c; markers=true, label=true, labelalways=true)
                ])
            ])
        ),
        row_module(plot(:plot_data; options=:plot_options_dict)),
        # make a nice bottom section
        row("&nbsp;")
    ], title = "Stipple x-y ApexChart", channel = channel)

    css = join([
        script(src="https://cdn.jsdelivr.net/npm/vue@latest/dist/vue.min.js"),
        script(src="https://cdn.jsdelivr.net/npm/vue-slider-component@latest/dist/vue-slider-component.umd.min.js"),
        link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/vue-slider-component@latest/theme/default.css"),
        style("""
            .vue-slider-dot-handle:hover {
                box-shadow: 0px 0px 0px 6px;
                transform: rotate(45deg);
                transition: 100ms
            }

            .st-ph {
                padding-left: 20px;
                padding-right: 20px;
            }
            .st-ph:first-child {
                padding-left: 0px;
            }
            .st-ph:last-child {
                padding-right: 0px;
            }

            .st-pv {
                padding-top: 20px;
                padding-bottom: 20px;
            }
            .st-pv:first-child {
                padding-top: 0px;
            }
            .st-pv:last-child {
                padding-bottom: 0px;
            }

            .st-bb:last-child {
                border-bottom: 0
            }
        """)
    ], "\n")

    css * db |> html
end

route("/") do
    # identify user from the header
  headers = Genie.Requests.getheaders()
  user = headers["User-Agent"] |> split |> last
  # deliver a user-spcific ui
  ui(user)
end

Genie.config.server_host = "127.0.0.1"
up(open_browser = true)
hhaensel commented 3 years ago

image

essenciary commented 3 years ago

@hhaensel Sorry, missed this update! Can we release this somehow? Should we pack it as a distinct package or add it to StippleUI? If we put it in UI we need a way to load (additional) assets per component.