JuliaGizmos / Interact.jl

Interactive widgets to play with your Julia code
Other
520 stars 75 forks source link

Widget options #16

Open shashi opened 10 years ago

shashi commented 10 years ago

IPython widgets have attributes such as label, orientation, readout, etc. It would be nice to have an options field in Widgets which is of a composite type. A nice API through kwargs would be good. It would be fantastic if this field is allowed to be a Signal and lift is obviated.

stevengj commented 10 years ago

Yes, @ellisonbg just told me about the readout option in the slider; I really want this (see shashi/IJuliaWidgets.jl#8). (Actually, I think I'd like readout=true to be the default.)

shashi commented 10 years ago

@stevengj as of now, it's hard coded to readout=true. readout will work if you check out IJuliaWidgets on master.

shashi commented 10 years ago

Persistent, composable options API proposal:

The idea is to represent widget attributes as persistent maps, making effective use of React and efficient diffs possible.

typealias Options PersistentHashMap{Symbol, Any}
options(;kwargs...) = phmap(kwargs)
option(key, value) = phmap((key,value))
# Then we have all these other functions for type-correctness & validation
label(value::String) = option(:label, value)
readout(value::Bool) = option(:readout, value)

For ease of composing we define & which will give precedence to the right most argument.

(&)(::Options, ::Options) -> ::Options # merged options
(&)(::Widget, ::Options) -> ::Widget   # Applies options to widget

Now we can do things like:

slideropts = label("Some Label") & orientation(:vertical) & range(1:100)

InputWidget would boil down to:

const defaultopts = options(label="", css=options(), readout=true, ...)

immutable InputWidget{view, T}
    options::Options
    signal::Input{T}
    InputWidget(opts::Options, signal) = new(defaultopts & opts, signal)
end
signal(i::InputWidget) = i.signal

# E.g.
slider{T <: Number}(r::Range{T};
                                input::Input=Input(0), min::T=first(r),
                                max::T=last(r), step::T=step(r), value::T=median(r), kwargs...) =
    InputWidget{:Slider, T}(options(kwargs...), input)
slider(opts::Options, input) =  InputWidget{:Slider, T}(options(kwargs...), input)

# The best part:
attach(w::InputWidget, opts::Signal{Options}) = foldl(&, w, opts)

# E.g.
txt = textbox("")
attach(slider(1:100, label="type in the box above to change this"), lift(label, txt))

The advantage of this is that, it's possible to have a diff function which gives us a patch to get from one option state to another. We can send only the diff to IPython in an update comm message. This diffing is also efficient since Options is an immutable data structure.

cc @dcjones

stevengj commented 10 years ago

It seems awfully complicated to use persistent maps and diffs when usually we will just have a few options. Why not just use a Dict?

Also, in most real cases it seems like you will set the options once when you create the widget and never touch them. For this case you just want to do widget(foo, option1=true, option2="hello", ...) rather than attaching an Options object etcetera manually.

dcjones commented 10 years ago
const defaultopts = options(label="", css=options(), readout=true, ...)

What does css=options() do? Is the intent that this options scheme will allow attaching arbitrary css to widgets?

shashi commented 10 years ago

@stevengj Okay I may not have been very clear in my explanation, but things like slider(range, label="foo", value=x) will still continue to work like they do now! But in the event that someone wants a widget to change its options over time, you are going to be able to do it by attaching a signal of options to the widget (along with the current way of creating a signal of widgets).

This is more convenient because 1) you don't keep producing Input nodes for each update if you are not careful to do something like input = Input(0); lift(x -> slider(range, input=input, vlaue=x, signal_x) which I think is uglier than attach(slider(range), lift(x->option(:value, x), signal_x)) 2) you can apply the same signal of options to multiple widgets 3) combine multiple option signals without dealing with widgets until you need to. These are just extra features that this kind of design makes available.

Persistent Dicts are important because: 1) You wont have unexpected changes in options which some arbitrary function is looking at, 2) React functions like droprepeats which rely on equality of two updates will work as intended. With Dicts, for example, this function drops all updates! 3) You can do diffs without saving an entire copy of the options dict, 4) diffs are really fast because you can use === to compare two objects and then fall back to ==. This is the approach used in virutal DOM diffing libraries such as https://github.com/Matt-Esch/virtual-dom, whereas they create a persistent representation of the DOM tree, we can instead do the same for the state of our widgets because we have JS counterparts which turn these into DOM. I am going to do #7 and tag 0.1.0 and then work on these...

@dcjones Yes, you will be able to attach arbitrary CSS to the widgets. :D

stevengj commented 10 years ago

I think it adds a lot of complexity for a feature that it is not clear anyone will use. Why would you want a widget to change its options based on a signal?

In fact, I'm not convinced it's that useful to support changing options at all. For the time being, I would just implement setting options when the widget is created. Then you don't even need to store the options in the Julia widget object.

You can always extend the underlying implementation later to support dynamic options if there is demand for it. But I would wait and see whether that actually happens.

dcjones commented 10 years ago

@shashi Do you have any particular use cases in mind? I imagine there will rarely be a need for options like orientation or readout to change after initialization. But maybe you have grander plans for this than just the prebaked options in ipython widgets.

shashi commented 10 years ago

I realize IJulia is not the target platform for something like this to be awfully useful. It would just be nice to support this feature since IPython Notebook already has its side figured out here. These examples are quite nice.

One use case is making dashboards that are customizable using widgets themselves: e.g. have set of preference widgets where you can configure default values or look and feel or whatever. This will also allow for animations in the HTML widget based on any changing data. But yeah, let's wait and see if people actually want this.

garborg commented 10 years ago

Not saying this is the time or the solution, but this functionality would make Interact more powerful.

If the widget was a container widget, and the signal was coming from a select widget, a different layout / set of widgets inside the container per signal is something I would use.

I've done this with Angular where the front end was simple enough and the math was involved enough that I would have accepted a cruder front end in exchange for a numeric-friendly backend.

Anyway, it seems like a practical use case for Interact, even if the supported front ends aren't quite designed for it.

garborg commented 10 years ago

Oops, I guess I missed and repeated the last paragraph of @shashi's last comment... The difference being I was talking about a dashboard where relevant widgets (as opposed to look and feel) change based on the tab a user clicks or what's applicable to a certain input.