How to return two customSVGSeries from a single function in clojurescript - 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.

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))

Related

Insert the result of a sequence generated with for in a FlexibleXYPlot in clojurescript [SOLVED]

In this question (How to return two customSVGSeries from a single function in clojurescript), I learned how to insert the result of a function creating two customSVGSeries into an XYPlot.
Now I want to insert the result of a for calling that same function:
(into [:> rvis/FlexibleXYPlot {...}]
(doall (for [[id {:keys [x y texto]}] etiquetas]
(crear-etiqueta id x y texto))))
Where etiquetas is a map with this form:
{:etiqueta-1 {:x 0, :y 0, :texto ["176.6"]}, :etiqueta-2 {:x 1, :y 2, :texto ["Hidrógeno"]}}
and crear-etiqueta is the function returning two customSGVSeries. The problem is that using the code above, nothing is shown in the plot.
I uploaded a repo with a MWE: https://github.com/lopezsolerluis/annie-test-for-stackoverflow
Edit
I used the excellent idea of Eugene... it works like a charm!
(into [:> rvis/FlexibleXYPlot {...}]
(mapcat (fn [[id {:keys [x y texto]}]]
(crear-etiqueta id x y texto))
etiquetas))
Expanding on my comment to the question.
Using for with two collections:
(into [:> rvis/FlexibleXYPlot {...}]
(for [[id {:keys [x y texto]}] etiquetas
series (crear-etiqueta id x y texto)]
series))
for will iterate over eqieuetas and for each item it will destructure it and pass the result into crear-etiqueta, which returns a collection. for then iterates over that collection and assigns the value of each item to series. Then the body is finally evaluated, which just returns the value of series.
Using the mapcat transducer:
(into [:> rvis/FlexibleXYPlot {...}]
(mapcat (fn [[id {:keys [x y texto]}]]
(crear-etiqueta id x y texto)))
etiquetas)
I won't go into the details of how it works - it's all documented here. I definitely recommend reading that reference in full because of immense usefulness of transducers in many contexts.

How to reset a counter in Re-frame (ClojureScript)

This must be one of those silly/complex things that everybody founds when learning a new framework. So I have this function:
(defn display-questions-list
[]
(let [counter (atom 1)]
[:div
(doall (for [question #(rf/subscribe [:questions])]
^{:key (swap! counter inc)} [question-item (assoc question :counter #counter)])])))
The #counter atom doesn't hold any important data, it's just a "visual" counter to display the number in the list. When the page is loaded for first time, all works fine, if there are five questions the list displays (1..5), the issue is that when a question is created/edited/deleted the subscription:
#(rf/subscribe [:questions])
is called again and then of course the list is displayed but now from 6 to 11. So I need a way to reset the #counter.
You should not be using an atom for this purpose. Your code should look more like this:
(ns tst.demo.core
(:use tupelo.test)
(:require [tupelo.core :as t]))
(defn display-questions-list
[]
[:div
(let [questions #(rf/subscribe [:questions])]
(doall (for [[idx question] (t/indexed questions)]
^{:key idx}
[question-item (assoc question :counter idx) ])))])
The tupelo.core/indexed function from the Tupelo library simply prepends a zero-based index value to each item in the collection:
(t/indexed [:a :b :c :d :e]) =>
([0 :a]
[1 :b]
[2 :c]
[3 :d]
[4 :e])
The source code is pretty simple:
(defn zip-lazy
"Usage: (zip-lazy coll1 coll2 ...)
(zip-lazy xs ys zs) => [ [x0 y0 z0]
[x1 y1 z1]
[x2 y2 z2]
... ]
Returns a lazy result. Will truncate to the length of the shortest collection.
A convenience wrapper for `(map vector coll1 coll2 ...)`. "
[& colls]
(assert #(every? sequential? colls))
(apply map vector colls))
(defn indexed
"Given one or more collections, returns a sequence of indexed tuples from the collections:
(indexed xs ys zs) -> [ [0 x0 y0 z0]
[1 x1 y1 z1]
[2 x2 y2 z2]
... ] "
[& colls]
(apply zip-lazy (range) colls))
Update
Actually, the main goal of the :key metadata is to provide a stable ID value for each item in the list. Since the items may be in different orders, using the list index value is actually a React antipattern. Using a unique ID either from within the data element (i.e. a user id, etc) or just the hashcode provides a unique reference value. So, in practice your code would be better written as this:
(defn display-questions-list
[]
[:div
(doall (for [question #(rf/subscribe [:questions])]
^{:key (hash question)}
[question-item (assoc question :counter idx)]))])
Some hashcode samples:
(hash 1) => 1392991556
(hash :a) => -2123407586
(hash {:a 1, :b [2 3 4]}) => 383153859

ClojureScript - assoc is not working inside a promise

I have an array of art pieces. I want to find the route length and associate it with each art pieces.
My code will look like:
(defn load-art-routes [art-list ctx]
(doall (map-indexed (fn [index art]
(let [user-location (select-keys (:coords (sub> ctx :geolocation)) [:latitude :longitude])
art-location (:geoLocation art)]
(->> (map-request {:origin (str (:latitude user-location) "," (:longitude user-location))
:destination (str (:lat art-location) "," (:lon art-location))
:mode (name (sub> ctx :transition-mode))})
(p/map (fn [data]
(let [route-length (ocall js/Math "round" (/ (get-in data [:routes 0 :legs 0 :distance :value]) (* 0.621371192 1000)) 2)
route-duration (ocall js/Math "floor" (/ (get-in data [:routes 0 :legs 0 :duration :value]) 60))]
(js/console.log "load-art-routes route-length " route-length")
(assoc art :route-length route-length))))
(p/error (fn [error]
(util/log (str "GOOGLE DIRECTIONS API ERRORS" params) error)
))))) art-list))
art-list)
(defn map-request [params]
(when params
(let [endpoint google-directions-api-endpoint]
(->> (make-get-req (str endpoint "&" (encode-query-params params))
{})
(p/map (fn [data]
(util/log "GOOGLE DIRECTIONS API " data)
data))
(p/error (fn [error]
(util/log (str "GOOGLE DIRECTIONS API ERRORS" params ) error)
))))))
The route length calculation is correct but, assoc is not working. It is not actually associating it. I don't know what the issue is. Can anyone help me?
Please simplify this example! In the process, you will discover the problem.
First, update your question to include the require that shows what p/map, p/error, etc are. Also, put map-request before load-art-routes just like it must be in your source file.
Then, you should start by removing the thread-last ->> operator and use let with names for intermediate values:
(let [aa (map-request ...)
bb (p/map (fn [data] ...) aa)
cc (p/error (fn [error] ...) bb) ]
<bb or cc here?> )
My suspicion is that your p/error call is swallowing the results of p/map and returning nil.
This looks like you are trying to write "mutable" code.
Reformatting the code and fixing one error makes this more obvious:
(defn load-art-routes [art-list ctx]
(doall (map-indexed (fn [index art]
(let [user-location (select-keys (:coords (sub> ctx :geolocation)) [:latitude :longitude])
art-location (:geoLocation art)]
(->> (map-request {:origin (str (:latitude user-location) "," (:longitude user-location))
:destination (str (:lat art-location) "," (:lon art-location))
:mode (name (sub> ctx :transition-mode))})
(p/map (fn [data]
(let [route-length (ocall js/Math "round" (/ (get-in data [:routes 0 :legs 0 :distance :value]) (* 0.621371192 1000)) 2)
route-duration (ocall js/Math "floor" (/ (get-in data [:routes 0 :legs 0 :duration :value]) 60))]
(js/console.log "load-art-routes route-length " route-length)
(assoc art :route-length route-length))))
(p/error (fn [error]
(util/log (str " GOOGLE DIRECTIONS API ERRORS " params) error)
))))) art-list))
art-list)
load-art-routes simply returns the original art-list and kicks of some work in promises. These promises only update their versions of the list but given that we are using immutable data structures the returned art-list themselves remain unchanged. There is also a suspicious second art-list in the p/error call?
You'll probably want to restructure this to something that either returns a Promise or accepts a callback that will be called once all route-length have been computed.

How to override onload methods in ClojureScript?

I am trying to override onload function of document and Image in ClojureScript. I think that set! should be possible to do it, but i am not getting any success. Relevant code is as follows :
(defn load-image [img-path]
(let [img (js/Image.)]
(do (set! (.-src img) img-path)
img)))
(defn add-img-canvas [img-path width height]
(let [img (load-image img-path)]
(set! (.-onload img)
(fn [] ;; This function is never called.
(let [canvas (get-scaled-canvas img width height)]
(do (pr-str canvas)
(swap! game-state :canvas canvas)))))))
(defn hello-world []
(let [count (atom 1)]
(fn []
[:div
[:h1 (:text #game-state)]
[:div (do (swap! count inc) (str "count is " #count))]
[:canvas (:canvas #game-state)]])))
(reagent/render-component [hello-world]
(. js/document (getElementById "app")))
(set! (.-onload js/document)
(fn [] ;; This function is also never called.
(add-img-canvas (:img-src game-state) 100 130)))
;;(. js/document onload)
Anonymous functions in add-img-canvas is not getting called. What am i doing wrong ?
I think it may be down to the difference between document.onload vs window.onload. The latter does work as expected.
See this for more details between the two.

Strange reduce behavior in Clojure

EDIT: This was not a problem with reduce or the function being reduced. I shadowed the clojure.core/range function.
I have a function
(defn- roundfn [[xi ci bi oi :as state] r]
(let [[xn cn bn] (newstate [xi ci bi] 0)
exfn (word<-x xn)]
[xn cn bn
(into oi
[(exfn [6 3 6 1])
(exfn [4 1 4 7])
(exfn [2 7 2 5])
(exfn [0 5 0 3])])]))
where x1,x2, and x4 are themselves vectors. x3 is a value.
When I reduce this function like
(reduce roundfn [[][] 0 []] (range 3))
or
(reduce roundfn [[][] 0 []] (vec (range 3)))
I'm receiving IndexOutOfBoundsException clojure.lang.PersistentVector.arrayFor (PersistentVector.java:107)
When I reduce this function like
(reduce roundfn [[][] 0 []] [0 1 2])
it works as expected
(Working off of this version of the source -- link to the current revision of the file mentioned in a comment on the question.)
Firstly, running your code produces the exception at my REPL in all the cases you listed, including the literal [0 1 2] case. It would be astounding if this were not the case. (NB. I use rabbit-round in place of roundfn, since that's the name of the function quoted in the question text as found in the source on GitHub.)
The problem's source is as follows (based on the link to the full source given in a comment on the question):
The function listed as roundfn in the question text is called rabbit-round in the source. The problematic call is (reduce rabbit-round [[] [] 0 []] [0 1 2]).
The reduce than calls rabbit-round with the initial arguments; therein a call to roundfn takes place (roundfn being a separate function in the original source): (roundfn [[] [] 0] 0). It is here that the exception gets thrown.
roundfn is a composition of two functions; already the first turns out to be the source of the problem: (update-counter-system [[] [] 0] 0) throws.
There's a further reduce call in there using counter-system-round as the reduction function. The exception is thrown when the latter is first applied: (counter-system-round [[] 0] 0).
Looking at counter-system-round we see that it attempts to compute (nth c round) in the first line. At this point, c is bound to [] (an empty vector) and round will be 0. Thus this call amounts to (nth [] 0) and correctly throws IndexOutOfBoundsException.
I was shadowing the clojure.core\range function with my [lower upper :as range] destructuring
Below is the version that didn't work
(generate-keystream [_ {:keys [ss] :as initmap} [lower upper :as range]]
(let [ns (conj ss [])]
(reduce rabbit-round ns (range 3))))
The following works as expected (note the change to :as rng instead of :as range)
(generate-keystream [_ {:keys [ss] :as initmap} [lower upper :as rng]]
(let [ns (conj ss [])]
(reduce rabbit-round ns (range 3))))
This explains why (range 3) and (vec (range 3)) both didn't work, but [0 1 2] did. The first two were evaluating to ([0 0] 3) and (vec ([0 0] 3)) in my original code, while [0 1 2] was evaluating to [0 1 2] and succeeding.
Lesson learned. Don't shadow functions
As a side note, this works too...
(generate-keystream [_ {:keys [ss] :as initmap} [lower upper :as range]]
(let [ns (conj ss [])]
(reduce rabbit-round ns (clojure.core/range 3))))