I mostly gave up on using the AlignmentViewer JS package I was intending to use and I am now investigating what it would take to just write a simple one myself. There is a performance problem with this though, which as far as I can tell is due to a bunch of elements being loaded into the DOM, although they are not visible at a given time. Is there a straightforward way to control this? I.e. put the whole thing in a scrollable container and only load into the dom what is visible? For some context, MSAs are things like these: https://en.wikipedia.org/wiki/Multiple_sequence_alignment#/media/File:RPLP0_90_ClustalW_aln.gif Basically, you have a couple hundred to tens of thousand strings of (usually, but not necessarily equal) length up to ~10k and you want to put them all underneath each other, color each character and calculate summary statistics per column. In my case, it will always only be up to a few thousand rows though. The super naive thing I am doing now is this:
(defn msa-viewer
[alignment]
(when (seq alignment)
(let [seq-length (-> alignment
first
:clustalo/aligned-sequence
count)]
[re-com/scroller
:size "100px"
:child
[re-com/v-box
:gap "5px"
:children
(cons
[:div
{:style {:white-space "nowrap"}}
(into
[:span {:style {:display "inline-block"}} ""]
(for [i (range 1 (inc seq-length))]
^{:key i}
[:span {:style {:display "inline-block"
:width "20px"
:text-align "center"
:font-family "monospace"
:font-weight "bold"}}
i]))]
(for [{:keys [fasta/header clustalo/aligned-sequence]} alignment]
^{:key header}
[:div
{:style {:white-space "nowrap"}}
(into
[:span {:style {:display "inline-block"
:font-weight "bold"
:font-family "sans-serif"}}
header]
(for [[idx c] (map-indexed vector aligned-sequence)]
^{:key idx}
[:span {:style {:display "inline-block"
:width "20px"
:text-align "center"
:font-family "monospace"}}
c]))]))]])))Either so-called "virtual lists" (e.g. https://github.com/bvaughn/react-window) or progressive rendering (render 100 or so rows at each frame with js/requestAnimationFrame).
Did webpack not work for you?
Oh, another alternative, which might or might not be performant enough, is to create a vanilla JS component that renders the whole thing and wrap it in a React/Reagent component. So you don't end up creating thousands of instances.
I started reading into the webpack thing and then got sidetracked a bit: I realized their distro comes with precompiled "standalone" files, so I required these instead to see if that does it, but somebody just pointed out to me that this might not work for other reasons, possibly bundling react already or something.
I am basically trying to assess my options at this point. I.e. should I spend a couple of hours trying it with webpack with the real possibility of this not working out, especially given that their (and frankly most) implementations of an AlignmentViewer does not really address many things that would actually be kinda relevant for the my specific use case. Or could I just implement the damn thing myself in a reasonable time frame.
It depends on what kind of functionality you need. It seems that that alignment viewer library that you shared earlier does have some degree of interactivity, and it might perform well even on huge input data. Just make sure you don't underestimate the amount of work some particular thing might require.
Really hard to do for me in JS land 😄
Oh, have you tried their older library? It seems to be JS only, without any fluff. https://github.com/sanderlab/alignmentviewer
Hmm, maybe it's not a library but a full on app. But should be possible to extract and use the relevant code from it.
They claim it is embeddable, but I haven't seen any actual examples where somebody does it yet
Ah sorry, you were talking about the old one
Why is this crazy slow?
(defn msa-viewer-scrollable
[alignment row-height container-height]
(let [scroll-offset (r/atom 0)
total-rows (count alignment)
visible-count (.ceil js/Math (/ container-height row-height))
buffer 5]
(fn [alignment row-height container-height]
(let [first-row (max 0 (- (int (/ @scroll-offset row-height)) buffer))
first-row (if (< first-row 0) 0 first-row)
last-row (min total-rows (+ first-row visible-count (* 2 buffer)))
top-spacer-height (* first-row row-height)
bottom-spacer-height (* (- total-rows last-row) row-height)]
[:div {:style {:height (str container-height "px")
:overflow-y "auto"
:white-space "nowrap"
:border "solid black 1px"}
:on-scroll (fn [e]
(reset! scroll-offset (.-scrollTop (.-target e))))}
[:div
[:div {:style {:height (str top-spacer-height "px")}}]
(for [row (subvec alignment first-row last-row)]
^{:key (:name row)}
[:div
(into
[:span {:style {:display "inline-block"
:width "100px"
:font-weight "bold"}}
(:name row)]
(for [[idx c] (map-indexed vector (:sequence row))]
^{:key idx}
[:span {:style {:display "inline-block"
:width "20px"
:text-align "center"
:font-family "monospace"}}
c]))])
[:div {:style {:height (str bottom-spacer-height "px")}}]]]))))
(def sample-alignment
(vec
(map (fn [i]
{:name (str "Seq" i) :sequence (apply concat (repeat 100 "ACGT-ACGT"))})
(range 100))))Impossible to tell without a profiler result, but probably because you create an element per character.
Also you shouldn't use ^{:key idx}. When you have nothing but the index for a key, it's better to avoid lazy seqs altogether.
Mh, so I looked into the Webpack thing some more, and if I understand this correctly: this
:js-options {:js-provider :external
:external-index "frontend/target/index.js"}
and this:
npx webpack --entry ./frontend/target/index.js --output-path ./frontend/resources/public/js --output-filename libs.js
bundle the external JS and then I load this in the HTML
<script src="/js/libs.js"></script>
<script src="/js/compiled/app.js"></script>
But then I get:
Error: Dependency: alignment-viewer-2 not provided by external JS. Do you maybe need a recompile?
shadow$bridge libs.js:2
<anonymous> shadow.js.shim.module$alignment_viewer_2.js:3
globalEval app.js:434
evalLoad app.js:1405
<anonymous> app.js:2014
and
TypeError: shadow.js.shim.module$alignment_viewer_2.FastaAlignment is undefined
unknown_client$core$mount_root core.cljs:86
unknown_client$core$init core.cljs:93
<anonymous> shadow.module.app.append.js:4
globalEval app.js:434
evalLoad app.js:1405
<anonymous> app.js:2016
It seems both lib.js and app.js have been loaded in the HTML thoughMaybe I'll better take this over to shadow-cljs