metasoarous / datview

Effortlessly compose data visualizations and controls for Datomic and DataScript data
Eclipse Public License 1.0
25 stars 5 forks source link

Make controls more composable #5

Open metasoarous opened 8 years ago

metasoarous commented 8 years ago

This is pretty open ended... The bottom line though is that you shouldn't have to override a representation to specify which controls something needs. Representations have a lot of gunk in them (passing down context etc), and folks shouldn't have to wade through that just to pick some controls. Maybe controls should be separately registered? What should the shape be? This would let us just specify the namespaced keywords for which controls we wanted, and separately specify how we group them together.

bamarco commented 7 years ago

What about having some controller-state (maybe as a datomic entity) and a representation-middleware-fn. Each controller can be represented as a set of buttons to display. Then you can mess with the context. So you could change the pull query in the context to only pull certain summary fields with a collapse-controller. A tab-controller could have a set of tabs which are stored posh queries which will change which posh query gets called. The names of the tabs will be displayed as buttons. Then you have a container-controller which puts all the views of the different controllers into a button bar and and represents the main-query run through all the controller-middleware. Also things like an add-controller which holds the local state for temporary entity before moving it into the normal data flow.

metasoarous commented 7 years ago

This all sounds great! Some high-level comments/questions:

Cheers!

bamarco commented 7 years ago

I think a control is just a representation of a user controlled instrument. There are two main classes of control. Those that adjust the data and those that adjust the layout. If you haven't seen it already http://nickrossiter.org.uk/process/VisualizationFoundationsIEEE.pdf Fig 14 on p 13 has the exact definitions I'm using here. Basically schema is to data as layout is to representation. With that in mind we should have a routing function that maps schema to layout which will serve as our rules transformation. A very simple example (using only schema type) might look like:

(def simple-layout {:e.type/todo (fn [app schema data]
                                   [represent :db.type/bool data]
                                   [represent :db.type/string data]
                                   [represent :db.type/string data])})

(def edit-layout {:e.type/todo (fn [app schema data]
                                 [represent :control [instrument app :edit :db.type/boolean data :todo/complete?)]
                                 [represent :control [instrument app :edit :db.type/string data :e/name)])})

(def modal-layout {:e.type/todo (fn [app schema data]
                                  ;; ???: could be implemented as layout middleware
                                  (if @modal-state
                                    [layout simple-layout [app schema data]]
                                    [layout edit-layout [app schema data]])
                                  [represent :control [instrument modal-state :toggle])})

(def pull-layout
  {:default (fn [app schema data]
                   [represent (:type schema) data])
   :db.type/ref (fn [app schema data]
           (for [attr (:attrs schema)]
                 [layout pull-layout [app (get schema attr) (get data attr)]]))})

(def pull-form-layout
  {:default (fn [app schema data]
           (for [attr (:attrs schema)]
                 [represent :control [instrument app :edit (get-type schema attr) (get data attr) attr]]))})
metasoarous commented 7 years ago

So one thing to think about with all this is that the semantics of how we scope layouts should be quite flexible. Not just by :default, :db.type/ref, or :e.type/Todo etc as keys; It's too easy to want to specify some really specific combination of criteria for applying a layout here. But actually, this has a lot to do with what I've been thinking about how context should work as well. I'm coming to the conclusion that we should be able to use entities with scoping attributes to model the context data we want to apply in one situation versus another. Of course, this leaves the issue of precedence, which is what's been bugging me. A lot of the solutions feel hacky. But I think it's the way to go for a number of reasons. Perhaps we should pick up that line of thought in another issue, and then revisit here.

bamarco commented 7 years ago

I totally agree on the flexibility. Anything that is in the schema necessarily needs to be available for layout decisions. My plan was to just route it (using cond as opposed to a map). I'm not entirely grokking the scoping attributes concept. So opening a ticket is probably the right move.

bamarco commented 7 years ago

@metasoarous Okay so this code actually runs. Let's talk about what should be different to make this more correct/orthogonal/simple.

(rep/register-representation
  ::arrow-button
  (fn [app _ [state handler]]
    [re-com/button
     :label ">"
     :on-click handler]))

(defn layout-controls [representation-fn]
  (fn [app [id {:keys [controls] :as context}] data]
    [representation-fn
     app
     [id
      (deep-merge
      {:layout {:controls
              (for [[k {:keys [represent state handler] :as control}] controls]
                ^{:key k}
                [rep/represent app [represent] [state handler]])}}
      context)]
     data]))

(defn collapser [representation-fn]
  (fn [app [id {:as context
                {:keys [pull-summary pull-expression] :as subscriptions} :subscriptions}] data]
    (let [collapsed? (r/atom false)
          collapse-handler (fn [] (log/info "clicked") (swap! collapsed? not))]
      (fn [app [id context] data]
        (let [collapsed? @collapsed?
              context (deep-merge
                        {:controls
                         {::collapser
                          {:represent ::arrow-button
                           :state collapsed?
                           :handler collapse-handler}}}
                        context
                        {:subscriptions {:pull-expression (if collapsed? pull-summary pull-expression)}})]
;;           (log/info "context:" context)
          [representation-fn app [id context] data])))))

(defn t-box [& {{:keys [a b] :as children} :children
                :keys [adjustable?]
                :or {:adjustable? false}}]
  (let [a [re-com/h-box :children a]]
    (if adjustable?
      [re-com/v-split :panel-1 a :panel-2 b]
      [re-com/v-box :children children])))

(defn represent-children-with-controls [layout]
  (layout assoc :children [(:controls layout) (:representation layout)]))

(defn rebox-children [layout]
  (let [[child child2] (:children layout)]
  (layout assoc
          :child child
          :panel-1 child
          ;; FIXME: second can be out of bounds
          :panel-2 child2)))

(defn box-layout [representation-fn boxer]
  (fn [app [id context] data]
    [representation-fn app
     [id (deep-merge {:layout
                      {:boxer boxer}}
                     context)]
     data]))

(defn box-layouter [representation-fn]
  (fn [app [id {:as context
                 {:as layout
                 :keys [boxer controls]} :layout}] data]
    [boxer :children [controls [representation-fn app [id context] data]]]))

(defn pull-subscription [representation-fn]
  (fn [app [id context] data]
    [representation-fn app [id (deep-merge {:subscriptions {:pull-data ((:pull-expression (:subscriptions context)) @data)}}
                                           context)] data]))

(defn subscriber [representation-fn]
  (fn [app [id context] layout]
    [representation-fn app [id context] (:subscriptions context)]))

(rep/register-representation
  ::simple
  [collapser pull-subscription subscriber layout-controls #(box-layout % t-box) box-layouter]
  (fn [app [_ context] {:keys [pull-data]}]
    [re-com/label :label (pr-str pull-data)]))

(defn hello [app]
  (let [hello-db (r/atom {:hello-all "Hello, World!" :hello-some "Hello."})]
    (fn [app]
      [rep/represent app [::simple {:subscriptions {:pull-expression :hello-all
                                                    :pull-summary :hello-some}}]
       hello-db])))