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

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.

Related

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

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. Reset atom in Reagent when re-render occurs

I'm displaying a set of questions for a quiz test and I'm assigning a number to each question just to number them when they are shown in the browser:
(defn questions-list
[]
(let [counter (atom 0)]
(fn []
(into [:section]
(for [question #(re-frame/subscribe [:questions])]
[display-question (assoc question :counter (swap! counter inc))])))))
The problem is that when someone edits a question in the browser (and the dispatch is called and the "app-db" map is updated) the component is re-rendered but the atom "counter" logically starts from the last number not from zero. So I need to reset the atom but I don't know where. I tried with a let inside the anonymous function but that didn't work.
In this case I'd just remove the state entirely. I haven't tested this code, but your thinking imperatively here. The functional version of what your trying to do is something along the lines of:
Poor but stateless:
(let [numbers (range 0 (count questions))
indexed (map #(assoc (nth questions %) :index %) questions)]
[:section
(for [question indexed]
[display-question question])])
but this is ugly, and nth is inefficient. So lets try one better. Turns out map can take more than one collection as it's argument.
(let [numbers (range 0 (count questions))
indexed (map (fn [idx question] (assoc question :index idx)) questions)]
[:section
(for [question indexed]
[display-question question])])
But even better, turns out there is a built in function for exactly this. What I'd actually write:
[:section
(doall
(map-indexed
(fn [idx question]
[display-question (assoc question :index idx)])
questions))]
Note: None of this code has actually been run, so you might have to tweak it a bit before it works. I'd recommend looking up all of the functions in ClojureDocs to make sure you understand what they do.
If you need counter to be just an index for a question, you could instead use something like this:
(defn questions-list
[]
(let [questions #(re-frame/subscribe [:questions])
n (count questions)]
(fn []
[:section
[:ul
(map-indexed (fn [idx question] ^{:key idx} [:li question]) questions)]])))
Note: here I used [:li question] because I assumed that question is some kind of text.
Also, you could avoid computing the count for questions in this component and do it with a layer 3 subscription:
(ns your-app.subs
(:require
[re-frame.core :as rf]))
;; other subscriptions...
(rf/reg-sub
:questions-count
(fn [_ _]
[(rf/subscribe [:questions])])
(fn [[questions] _]
(count questions)))
Then in the let binding of your component you would need to replace n (count questions) with n #(re-frame/subscribe [:questions-count]).

Clojure function in let binding

If I have a function that evaluates to a function
(defn func1 [c1 c2]
(fn [x1 x2]
...do some stuff with c1 c2 x1))
that I use elsewhere in a map or reduce, is it better to use inline
(defn func2 [x y z]
(reduce (func1 x y) z (range 20)))
or to let bind it first
(defn func2 [x y z]
(let [ffunc (func1 x y)]
(reduce ffunc z (range 20))))
In the first case I would be worried that a new function over x and y is generated each step through the reduce.
The evaluation of the function call (func1 x y) is done once in each case.
The rule for evaluating a function call in Clojure consists of evaluating all the expressions that are provided as its arguments and then invoking the function with those values.
If you define the following higher order function:
(defn plus []
(println "calling plus")
+)
And then call reduce in the following way:
(reduce (plus) [0 1 2 3])
A single calling plus is printed, showing the function plus is invoked only once.
The same thing happens when using the let form:
(let [f (plus)]
(reduce f [0 1 2 3]))
Hope it helps.

How to build a vector via a call to reduce

I'm trying to figure why this particular function isn't working as expected. I suspect from the error message that it has something to do with the way I'm creating the empty vector for the accumulator.
I have a simple function that returns a sequence of 2-element vectors:
(defn zip-with-index
"Returns a sequence in which each element is of the
form [i c] where i is the index of the element and c
is the element at that index."
[coll]
(map-indexed (fn [i c] [i c]) coll))
That works fine. The problem comes when I try to use it in another function
(defn indexes-satisfying
"Returns a vector containing all indexes of coll that satisfy
the predicate p."
[p coll]
(defn accum-if-satisfies [acc zipped]
(let [idx (first zipped)
elem (second zipped)]
(if (p elem)
(conj acc idx)
(acc))))
(reduce accum-if-satisfies (vector) (zip-with-index coll)))
It compiles, but when I attempt to use it I get an error:
user=> (indexes-satisfying (partial > 3) [1 3 5 7])
ArityException Wrong number of args (0) passed to: PersistentVector
clojure.lang.AFn.throwArity (AFn.java:437)
I can't figure out what's going wrong here. Also if there is a more 'Clojure-like' way of doing what I'm trying to do, I'm interested in hearing about that also.
The problem is probably on the else clause of accum-if-satisfies, should be just acc not (acc).
You could use filter and then map instead of reduce. Like that:
(map #(first %)
(filter #(p (second %))
(zip-with-index coll)))
You could also call map-indexed with vector instead of (fn [i c] [i c]).
The whole code would look like that:
(defn indexes-satisfying
[p coll]
(map #(first %)
(filter #(p (second %))
(map-indexed vector coll))))
As for a more Clojure-like way, you could use
(defn indexes-satisfying [pred coll]
(filterv #(pred (nth coll %))
(range (count coll))))
Use filter instead of filterv to return a lazy seq rather than a vector.
Also, you should not use defn to define inner functions; it will instead define a global function in the namespace where the inner function is defined and have subtle side effects besides that. Use letfn instead:
(defn outer [& args]
(letfn [(inner [& inner-args] ...)]
(inner ...)))
One more way to do it would be:
(defn indexes-satisfying [p coll]
(keep-indexed #(if (p %2) % nil) coll))