Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
hhaensel opened this issue Dec 6, 2020 · 6 comments
Open

Comments

@hhaensel
Copy link
Member

hhaensel commented Dec 6, 2020

@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:

  • The vue-slider-component emits an array of numbers when there are more than one handle, but only a normal number, in case of a single handle
  • the parsed array is of type Any

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:

  • typecast the Array elementwise
  • check whether the julian receiving component is an array but the payload isn't, then make an array out of it
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
Copy link
Member

essenciary commented Dec 7, 2020

@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
Copy link
Member Author

hhaensel commented Dec 7, 2020

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

image

@essenciary
Copy link
Member

It looks lovely 😍

@hhaensel
Copy link
Member Author

hhaensel commented Dec 8, 2020

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
Copy link
Member Author

hhaensel commented Dec 8, 2020

image

@essenciary
Copy link
Member

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants