Draft
^{:clay {:quarto {:draft true}}}
(ns graph.layout.structural)

problem: graph layout algorithms return x, y, but remove the other data from nodes the x,y need to be merged back into the graph. idea: Structural merge (like Reagent, respecting keys) can combine layout and data

Reagent: define component f (data)

=> f1 [[:div {:key 1} “apples”] [:div {:key 2} “pear”] ]

=> f2 [[:div {:key 2} “pears”] [:div {:key 1} “apples] ] compute the updates, with the keys: swap the dom elements and update pear to pears without the keys: replace everything

(defn pairs [coll]
  (map-indexed (fn [i x]
                 (or
                   (and (map? x) (contains? x :id) [[:id (get x :id)] x])
                   (and (map? x) (contains? x :key) [[:key (get x :key)] x])
                   (and (map? x) (contains? x :idx) [[:key (get x :idx)] x])
                   [[:idx i] x]))
               coll))
(defn merge-structure [a b]
  (cond
    (and (map? a) (map? b))
    (merge-with merge-structure a b)

    (and (sequential? a) (sequential? b))
    (let [a-pairs (pairs a)
          a-keys (into #{} (map first a-pairs))
          b-pairs (pairs b)
          b-map (into {} b-pairs)
          extra (for [[k v] b-pairs
                      :when (not (contains? a-keys k))]
                  v)]
      (-> (into (empty a)
                (map (fn [[k v]]
                       (merge-structure v (get b-map k))))
                a-pairs)
          (into extra)))

    :else (or b a)))
(def g
  {:a [{:key 1 :happy "yes"}
       {:key 2 :happy "no"}]})
(def layout
  {:a [{:key 2 :happy "absolutely" :x 1 :y 2}]})
(merge-structure g layout)
{:a [{:key 1, :happy "yes"} {:key 2, :happy "absolutely", :x 1, :y 2}]}

=> {:a [{:key 1, :happy “yes”} {:key 2, :happy “absolutely”, :x 1, :y 2}]}

Mixed keys and unkeys in sequences

(merge-structure
  {:nodes [{:key 1 :a "A1"}
           "raw"
           {:key 2 :b "B1"}]}
  {:nodes [{:key 2 :b "B2"}
           "raw-updated"
           {:key 1 :a "A2" :extra true}]})
{:nodes
 [{:key 1, :a "A2", :extra true} "raw-updated" {:key 2, :b "B2"}]}

=> {:nodes [{:key 1 :a “A2” :extra true} “raw-updated” {:key 2 :b “B2”}]}

Sequences of unequal length, no keys

(merge-structure
  {:list [1 2 3]}
  {:list [10 20]})
{:list [10 20 3]}

=> {:list [10 20 3]}

Deeply nested structure with partial key use

(merge-structure
  {:tree {:children [{:key 1 :label "A"}
                     {:label "B"}]}}
  {:tree {:children [{:label "A+" :x 1 :y 1}
                     {:label "B+" :x 2}]}})
{:tree
 {:children
  [{:key 1, :label "A"} {:label "B+", :x 2} {:label "A+", :x 1, :y 1}]}}

=> {:tree {:children [{:key 1 :label “A+” :x 1 :y 1} {:label “B+” :x 2}]}}

Vectors inside maps, mixed nesting

(merge-structure
  {:grid {:rows [[{:key :a :val 1}]
                 [{:key :b :val 2}]]}}
  {:grid {:rows [[{:key :a :x 5}]
                 [{:key :b :val 22 :y 9}]]}})
{:grid {:rows [[{:key :a, :val 1, :x 5}] [{:key :b, :val 22, :y 9}]]}}

=> {:grid {:rows [[{:key :a :val 1 :x 5}] [{:key :b :val 22 :y 9}]]]}}

Mismatched structures — map vs primitive

(merge-structure
  {:a {:nested "yes"}}
  {:a "overwrite"})
{:a "overwrite"}

=> {:a “overwrite”}

List vs vector: compatible sequences

(merge-structure
  {:x [{:key 1 :val "v"}]}
  {:x '({:key 1 :extra true})})
{:x [{:key 1, :val "v", :extra true}]}

=> {:x [{:key 1 :val “v” :extra true}]}

Terminal values — nil handling

(merge-structure
  {:value "keep"}
  {:value nil})
{:value "keep"}

=> {:value “keep”}

Mismatched structure — missing keys

(merge-structure
  {:data [{:key 1 :a 1} {:key 2 :b 2}]}
  {:data [{:c 3}]})
{:data [{:key 1, :a 1} {:key 2, :b 2} {:c 3}]}

=> {:data [{:key 1 :a 1 :c 3} {:key 2 :b 2}]}

(defn merge-replace [a b]
  (cond
    (and (map? a) (map? b))
    (merge-with merge-replace a b)

    (and (sequential? a) (sequential? b))
    (let [a-map (into {} (pairs a))
          b-map (into {} (pairs b))
          keys  (distinct (concat (keys a-map) (keys b-map)))]
      (mapv (fn [k]
              (if (contains? b-map k)
                (merge-replace (get a-map k) (get b-map k))
                (get a-map k)))
            keys))

    ;; fallback
    :else b))
(merge-replace
  {:a [{:key 1 :value "old"} {:key 2 :value "old"}]
   :b {:deep {:z 1}}}
  {:a [{:key 2 :value "new"}]
   :b {:deep {:z 99}}})
{:a [{:key 1, :value "old"} {:key 2, :value "new"}], :b {:deep {:z 99}}}

=> {:a [{:key 1 :value “old”} {:key 2 :value “new”}] :b {:deep {:z 99}}}

(defn merge-dissoc [a removals]
  (cond
    (and (map? a) (map? removals))
    (reduce-kv
      (fn [acc k rem]
        (if (contains? acc k)
          (let [val (get acc k)]
            (cond
              (and (map? val) (map? rem))
              (assoc acc k (merge-dissoc val rem))

              (and (sequential? val) (sequential? rem))
              (assoc acc k (merge-dissoc val rem))

              :else
              (dissoc acc k)))
          acc))
      a removals)

    (and (sequential? a) (sequential? removals))
    (let [a-map (into {} (pairs a))
          r-map (into {} (pairs removals))
          result (reduce-kv
                   (fn [m k v]
                     (if (contains? m k)
                       (let [val (get m k)]
                         (cond
                           (and (map? val) (map? v))
                           (assoc m k (merge-dissoc val v))

                           (and (sequential? val) (sequential? v))
                           (assoc m k (merge-dissoc val v))

                           :else
                           (dissoc m k)))
                       m))
                   a-map r-map)]
      (->> (sort-by key result)                             ; preserve order
           (mapv val)))

    :else a))

leave unchanged if shape doesn’t match

(merge-dissoc
  {:a [{:key 1 :foo "yes" :bar 1}
       {:key 2 :foo "no"}]
   :b "keep me"}
  {:a [{:key 2 :foo "remove this"}]})
{:a [{:key 1, :foo "yes", :bar 1} {}], :b "keep me"}

=> {:a [{:key 1 :foo “yes” :bar 1} {:key 2}] :b “keep me”}

source: src/graph/layout/structural.clj