Search code examples
clojurescriptreact-vis

How to return two customSVGSeries from a single function in clojurescript


I would like to draw two different customCVGseries using a single function; but, of course, this code (striped to a minimum) only returns the last one:

(defn make-label-with-line [x y key]     
   ^{:key key}
   [:> rvis/CustomSVGSeries {:onValueMouseOver (fn [] (reset! mouse-over? true))
                             :onValueMouseOut  (fn [] (reset! mouse-over? false))
                             :data [{:x x :y y
                                  :customComponent (fn [_ position-in-pixels]
                                    (if (and @middle-button-pressed? @mouse-over?)
                                      (reset! pos (calculate-xy position-in-pixels)))
                                    (let [[delta-x delta-y] @pos]
                                     (r/as-element [:g 
                                                      [:text
                                                        [:tspan {:x delta-x :y (+ delta-y 18)} "Hidrógeno "]
                                                        [:tspan {:x delta-x :y (+ delta-y 36)} "Alfa"]]])))}]}]
   ^{:key (str key "line")}
   [:> rvis/CustomSVGSeries {:data [{:x x :y y
                                  :customComponent (fn []
                                    (let [[delta-x delta-y] @pos]
                                     (r/as-element [:g 
                                                      [:polyline {:points [0 0 0 delta-y delta-x delta-y]
                                                                  :stroke "black" :fill "none"}]])))}]}])

I tried wrapping both in a :div, and even in a vector ([ and ]), but I get errors (I can copy them if they are useful).

I need them to be two elements, and not one, because I need that only the first be aware of :onValueMouseOver and :onValueMouseOut events: I need them to 'drag' the label (:text) over the graph, and the polyline can be too big and stretch over a big portion of the graph, and capture unwanted events.

In this screenshot I show the area captured by those events when I use the following working code:

one customSVGseries is "too" big

(r/as-element [:g
                 [:polyline {:points [0 0 0 inc-y inc-x inc-y]
                            :stroke "black" :fill "none"}]
                 [:text
                    [:tspan {:x delta-x :y (+ delta-y 18)} "Hidrógeno "]
                    [:tspan {:x delta-x :y (+ delta-y 36)} "Alfa"]]])

I even thought that using two lines (instead of a polyline) the "area" would more "limited"; I mean, that the user should put the mouse exactly over the lines to trigger the events. But I was wrong: the area subjected to the events is the same.

(r/as-element [:g
                 [:line {:x1 0 :y1 0 :x2 0 :y2 inc-y :stroke "black"}]
                 [:line {:x1 0 :y1 inc-y :x2 inc-x :y2 inc-y :stroke "black"}]
                 [:text
                    [:tspan {:x delta-x :y (+ delta-y 18)} "Hidrógeno "]
                    [:tspan {:x delta-x :y (+ delta-y 36)} "Alfa"]]])

I was thinking in using two functions (one for the text and one for the polyline); but there should be a better way! :) What bothers me most is that I must be missing something obvious... :/

Edit

I tried the solution proposed by Eugene Pakhomov, but now neither series show up in the graph; I get no errors either (it's as if they were commented-out...). I copy the full function in case I'm missing something obvious:

(let [mouse-over? (atom false) 
      pos (atom [0 18])]
  (defn crear-etiqueta [x y key position] 
    (if-not (= position [0 18]) (reset! pos position))
    [:<>
      ^{:key key}
      [:> rvis/CustomSVGSeries {:onValueMouseOver (fn [d] (reset! mouse-over? true))
                                :onValueMouseOut  (fn [d] (if-not @button-cen-pressed? (reset! mouse-over? false)))
                                :data [{:x x :y y
                                  :customComponent (fn [_ position-in-pixels]
                                    (if (and @button-cen-pressed? @mouse-over?)
                                      (reset! pos (calcular-xy-etiqueta position-in-pixels)))
                                    (let [[inc-x inc-y] @pos]
                                     (r/as-element [:g {:className "etiqueta"}
                                                      [:text
                                                        [:tspan {:x inc-x :y (+ inc-y 0)} "Hidrógeno "]
                                                        [:tspan {:x inc-x :y (+ inc-y 18)} "Alfa"]]])))}]}]
      ^{:key (str key "line")}
       [:> rvis/CustomSVGSeries {:data [{:x x :y y
                                  :customComponent (fn []
                                    (let [[inc-x inc-y] @pos]
                                     (r/as-element [:g {:className "etiqueta"}
                                                     [:polyline {:points [0 (if (< inc-y 5) -10 5) 0 inc-y inc-x inc-y]
                                                                 :stroke "black" :fill "none"}]])))}]}]
      ]))

Edit 2

I'm more confused. Reading about the use of [] and () here, I called crear-etiqueta like this: [crear-etiqueta 100 100 "key" [0 0]]... ¡But that was even worst! I even tried the simplest case, and didn't work:

(defn test-component [x y]
  ^{:key key}
      [:> rvis/CustomSVGSeries {:data [{:x x :y y :customComponent "square" :size 30}]}])

(defn line-chart []
  [:div
  [:> rvis/FlexibleXYPlot
[...]
    [test-component 176 550]]])

But if I change [test-component 176 550] with (test-component 176 550), it works.

Please excuse my wanderings; I realize I'm still learning.

Edit 3

The solution of Eugene Pakhomov certainly works... at least when the function for creating the two elements is called "simply". Now I have another problem:

The function should be called over a collection of items, each one having this form:

{:etiqueta-1 {:y 6071.758666687525, :x 176.60089063427614, :texto ["176.6"], :pos [0 18], :mouse-over? false}}

So I tried to insert them like this:

(into [:> rvis/FlexibleXYPlot {...}] 
      (doall (for [[id {:keys [x y texto]}] (:etiquetas (get @perfiles @perfil-activo))]
               (crear-etiqueta id x y texto [@perfil-activo :etiquetas id])))

But this doesn' work. It shows nothing. I updated the repo to show this.


Solution

  • Whenever you need to return multiple elements from a single Reagent component, use React fragments. In Reagent, it means wrapping those multiple elements in a single [:<> ...].

    But seems like you're out of luck when it comes to react-vis and React fragments - the library doesn't actually render the children (elements created with rvis/CustomSVGSeries) directly but rather it extracts all the information from them and then constructs what it needs to based on that information. React fragments aren't series themselves, and react-vis doesn't go inside fragments.

    What you can do, however, is to make your series-creating function return a simple vector of series (no need for the :key metadata), and into that vector inside the Hiccup vector that creates rvis/FlexibleXYPlot element:

    (into [:> rvis/FlexibleXYPlot {...}]
          (create-vector-of-series))