Skip to content

Commit 4e9bde3

Browse files
authored
Merge pull request #256 from ClojureCivitas/aog-7
AoG in Clojure wip
2 parents e869141 + 005a55b commit 4e9bde3

File tree

1 file changed

+121
-107
lines changed

1 file changed

+121
-107
lines changed

src/data_visualization/aog_in_clojure_part1.clj

Lines changed: 121 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@
166166
;; Malli - Schema validation
167167
[malli.core :as m]
168168
[malli.error :as me]
169+
[malli.util :as mu]
169170

170171
;; RDatasets - Example datasets
171172
[scicloj.metamorph.ml.rdatasets :as rdatasets]))
@@ -536,7 +537,7 @@
536537
;;
537538
;; **Why compute transforms ourselves?**
538539

539-
;; 1. Consistency - We want the isualizations to match the
540+
;; 1. Consistency - We want the visualizations to match the
540541
;; statistical computations of our Clojure libraries.
541542
;; 2. Efficiency - Especially with browser-based rendering targets,
542543
;; what we wish to pass to the target is summaries (say, 20 histogram bars)
@@ -556,6 +557,15 @@
556557
;; **You can skim this section** - it's reference material. The schemas will be
557558
;; used by validation helpers later, and referenced in examples as needed.
558559

560+
;; ### ⚙️ Malli Registry Setup
561+
;;
562+
;; Create a registry that includes both default schemas and malli.util schemas.
563+
;; This enables declarative schema utilities like :merge, :union, :select-keys.
564+
565+
(def registry
566+
"Malli registry with default schemas and util schemas (for :merge, etc.)"
567+
(merge (m/default-schemas) (mu/schemas)))
568+
559569
;; ### ⚙️ Core Type Schemas
560570

561571
(def DataType
@@ -662,25 +672,12 @@
662672

663673
;; ### ⚙️ Layer Schema
664674

665-
(def Layer
666-
"Schema for a complete layer specification.
667-
668-
A layer is a flat map with distinctive :=... keys containing all the
669-
information needed to render a visualization layer:
670-
- Data source
671-
- Aesthetic mappings (x, y, color, size, etc.)
672-
- Plot type
673-
- Visual attributes
674-
- Optional statistical transformation
675-
- Optional faceting"
675+
(def BaseLayer
676+
"Base layer fields shared across all plot types."
676677
[:map
677678
;; Data (required for most layers)
678679
[:=data {:optional true} Dataset]
679680

680-
;; Positional aesthetics
681-
[:=x {:optional true} PositionalAesthetic]
682-
[:=y {:optional true} PositionalAesthetic]
683-
684681
;; Other aesthetics
685682
[:=color {:optional true} ColorAesthetic]
686683
[:=size {:optional true} SizeAesthetic]
@@ -692,8 +689,7 @@
692689
;; Attributes (constant visual properties)
693690
[:=alpha {:optional true} AlphaAttribute]
694691

695-
;; Plot type and transformation
696-
[:=plottype {:optional true} PlotType]
692+
;; Transformation
697693
[:=transformation {:optional true} Transformation]
698694

699695
;; Histogram-specific
@@ -704,6 +700,73 @@
704700
[:=scale-y {:optional true} ScaleSpec]
705701
[:=scale-color {:optional true} ScaleSpec]])
706702

703+
(def Layer
704+
"Schema for a complete layer specification with plottype-specific requirements.
705+
706+
Uses :multi to dispatch on :=plottype and enforce different requirements:
707+
- :scatter, :line, :area require both :=x and :=y
708+
- :bar, :histogram require :=x (y is optional)
709+
- nil (no plottype) allows incomplete layers for composition
710+
711+
This replaces the nested conditionals in validate-layer with declarative schemas."
712+
(m/schema
713+
[:multi {:dispatch :=plottype}
714+
715+
;; Scatter requires both x and y
716+
[:scatter
717+
[:merge
718+
BaseLayer
719+
[:map
720+
[:=plottype [:enum :scatter]]
721+
[:=x PositionalAesthetic]
722+
[:=y PositionalAesthetic]]]]
723+
724+
;; Line requires both x and y
725+
[:line
726+
[:merge
727+
BaseLayer
728+
[:map
729+
[:=plottype [:enum :line]]
730+
[:=x PositionalAesthetic]
731+
[:=y PositionalAesthetic]]]]
732+
733+
;; Bar requires x, y optional
734+
[:bar
735+
[:merge
736+
BaseLayer
737+
[:map
738+
[:=plottype [:enum :bar]]
739+
[:=x PositionalAesthetic]
740+
[:=y {:optional true} PositionalAesthetic]]]]
741+
742+
;; Histogram requires x, y optional
743+
[:histogram
744+
[:merge
745+
BaseLayer
746+
[:map
747+
[:=plottype [:enum :histogram]]
748+
[:=x PositionalAesthetic]
749+
[:=y {:optional true} PositionalAesthetic]]]]
750+
751+
;; Area requires both x and y
752+
[:area
753+
[:merge
754+
BaseLayer
755+
[:map
756+
[:=plottype [:enum :area]]
757+
[:=x PositionalAesthetic]
758+
[:=y PositionalAesthetic]]]]
759+
760+
;; Incomplete layer (no plottype) - for composition
761+
[nil
762+
[:merge
763+
BaseLayer
764+
[:map
765+
[:=plottype {:optional true} [:maybe nil?]]
766+
[:=x {:optional true} PositionalAesthetic]
767+
[:=y {:optional true} PositionalAesthetic]]]]]
768+
{:registry registry}))
769+
707770
(def Layers
708771
"Schema for one or more layers.
709772
@@ -712,15 +775,17 @@
712775
[:or Layer [:vector Layer]])
713776

714777
(def PlotSpec
715-
"Schema for a complete plot specification.
778+
"Schema for a plot specification (complete or partial).
716779
717-
A plot spec is a map containing:
718-
- Layers: Vector of layer maps
780+
A plot spec is a map that can contain:
781+
- Layers: Vector of layer maps (optional - allows partial specs)
719782
- Plot-level properties: target, width, height
720-
- Plot-level scales (optional)"
783+
- Plot-level scales (optional)
784+
785+
All fields are optional to support composition via =* and =+."
721786
[:map
722-
;; Layers (required)
723-
[:=layers [:vector Layer]]
787+
;; Layers (optional - allows partial specs with just plot-level properties)
788+
[:=layers {:optional true} [:vector Layer]]
724789

725790
;; Plot-level properties (all optional)
726791
[:=target {:optional true} Backend]
@@ -784,65 +849,19 @@
784849
"Validate a layer with context-aware checks.
785850
786851
Performs:
787-
1. Schema validation (structure)
788-
2. Semantic validation (required fields for plottype)
789-
3. Data validation (columns exist)
852+
1. Schema validation (structure + plottype-specific requirements via :multi)
853+
2. Data column validation (columns exist) - runtime check
790854
791855
Returns nil if valid, error map if invalid."
792856
[layer]
793-
;; First check schema
857+
;; Schema validation now handles both structure AND plottype-specific requirements
794858
(or
795859
(when-let [schema-errors (validate Layer layer)]
796860
{:type :schema-error
797861
:errors schema-errors
798-
:message "Layer structure is invalid"})
799-
800-
;; Check plottype-specific requirements
801-
(let [plottype (:=plottype layer)]
802-
(when plottype
803-
(case plottype
804-
;; Scatter/line need x and y
805-
(:scatter :line)
806-
(when-not (and (:=x layer) (:=y layer))
807-
{:type :missing-required-aesthetic
808-
:plottype plottype
809-
:missing (cond
810-
(and (nil? (:=x layer)) (nil? (:=y layer))) [:=x :=y]
811-
(nil? (:=x layer)) [:=x]
812-
:else [:=y])
813-
:message (str plottype " plots require both :=x and :=y")})
814-
815-
;; Bar needs at least x
816-
:bar
817-
(when-not (:=x layer)
818-
{:type :missing-required-aesthetic
819-
:plottype plottype
820-
:missing [:=x]
821-
:message "Bar plots require :=x"})
822-
823-
;; Histogram needs just x
824-
:histogram
825-
(when-not (:=x layer)
826-
{:type :missing-required-aesthetic
827-
:plottype plottype
828-
:missing [:=x]
829-
:message "Histogram requires :=x"})
830-
831-
;; Area needs x and y
832-
:area
833-
(when-not (and (:=x layer) (:=y layer))
834-
{:type :missing-required-aesthetic
835-
:plottype plottype
836-
:missing (cond
837-
(and (nil? (:=x layer)) (nil? (:=y layer))) [:=x :=y]
838-
(nil? (:=x layer)) [:=x]
839-
:else [:=y])
840-
:message "Area plots require both :=x and :=y"})
841-
842-
;; Default - no specific requirements
843-
nil)))
844-
845-
;; Check data-related validations if data is present
862+
:message "Layer validation failed"})
863+
864+
;; Data column validation (runtime check - can't be done in schema)
846865
(when-let [data (:=data layer)]
847866
(let [column-keys (cond
848867
;; Tablecloth dataset
@@ -930,15 +949,15 @@
930949
(defn- plot-spec?
931950
"Check if x is a plot spec (map with :=layers or plot-level keys).
932951
933-
Plot specs are maps that can have:
934-
- :=layers key with vector of layer maps
935-
- Plot-level :=... keys like :=target, :=width, :=height"
952+
Plot specs are maps that have at least one key starting with :=
953+
Uses Malli validation as a fallback check for well-formed specs."
936954
[x]
937955
(and (map? x)
938-
(or (contains? x :=layers)
939-
;; Has plot-level keys
940-
(some #(-> % name first (= \=))
941-
(keys x)))))
956+
;; Has at least one := key
957+
(some #(-> % name first (= \=))
958+
(keys x))
959+
;; Validates against PlotSpec schema (additional safety check)
960+
(valid? PlotSpec x)))
942961

943962
;; ### ⚙️ Renderer
944963

@@ -1083,14 +1102,12 @@
10831102
(reduce =* (=* x y) more))))
10841103

10851104
;; Test helper: check if result is a valid layer vector
1086-
(defn- valid-layers? [x]
1087-
(and (vector? x)
1088-
(seq x)
1089-
(every? map? x)
1090-
(every? #(some (fn [[k _]]
1091-
(-> k name first (= \=)))
1092-
%)
1093-
x)))
1105+
(defn- valid-layers?
1106+
"Check if x is a valid vector of layers using Malli validation.
1107+
1108+
Note: This specifically checks for a vector of layers, not a single layer."
1109+
[x]
1110+
(valid? [:vector Layer] x))
10941111

10951112
(defn =+
10961113
"Combine multiple plot specifications for overlay (sum).
@@ -2383,7 +2400,6 @@ iris
23832400
(=* attrs-or-spec (scatter))
23842401
(let [result (merge {:=plottype :scatter}
23852402
(update-keys attrs-or-spec =key))]
2386-
(validate! Layer result)
23872403
{:=layers [result]})))
23882404
([spec attrs]
23892405
;; Threading-friendly: (-> spec (scatter {:alpha 0.5}))
@@ -2685,7 +2701,6 @@ iris
26852701
([]
26862702
(let [result {:=transformation :linear
26872703
:=plottype :line}]
2688-
(validate! Layer result)
26892704
{:=layers [result]}))
26902705
([spec-or-data]
26912706
(let [spec (if (plot-spec? spec-or-data)
@@ -2750,19 +2765,19 @@ iris
27502765

27512766
;; Apply linear regression transform to points.
27522767
;;
2753-
;; Handles both single and grouped regression based on :group key in points.
2768+
;; Handles both single and grouped regression based on `:group` key in points.
27542769
;;
27552770
;; Args:
2756-
;; - layer: Layer map containing transformation specification
2757-
;; - points: Sequence of point maps with :x, :y, and optional :group keys
2771+
;; - `layer`: Layer map containing transformation specification
2772+
;; - `points`: Sequence of point maps with `:x`, `:y`, and optional `:group` keys
27582773
;;
27592774
;; Returns:
2760-
;; - For ungrouped: {:type :regression :points points :fitted [p1 p2]}
2761-
;; - For grouped: {:type :grouped-regression :points points :groups {group-val {:fitted [...] :points [...]}}}
2775+
;; - For ungrouped: `{:type :regression :points points :fitted [p1 p2]}`
2776+
;; - For grouped: `{:type :grouped-regression :points points :groups {group-val {:fitted [...] :points [...]}}}`
27622777
;;
27632778
;; Edge cases:
27642779
;; - Returns original points if regression fails (< 2 points, degenerate data)
2765-
;; - Handles nil fitted values gracefully (skipped during rendering)
2780+
;; - Handles `nil` fitted values gracefully (skipped during rendering)
27662781
(defmethod apply-transform :linear
27672782
[layer points]
27682783
(when-not (seq points)
@@ -2966,7 +2981,6 @@ iris
29662981
:=plottype :bar
29672982
:=bins :sturges}
29682983
(update-keys opts-or-spec =key))]
2969-
(validate! Layer result)
29702984
{:=layers [result]})))
29712985
([spec opts]
29722986
(=* spec (histogram opts))))
@@ -3016,19 +3030,19 @@ iris
30163030
;; Apply histogram transform to points.
30173031
;;
30183032
;; Bins continuous x values and counts occurrences per bin.
3019-
;; Handles both single and grouped histograms based on :group key in points.
3033+
;; Handles both single and grouped histograms based on `:group` key in points.
30203034
;;
30213035
;; Args:
3022-
;; - layer: Layer map containing :=bins specification
3023-
;; - points: Sequence of point maps with :x and optional :group keys
3036+
;; - `layer`: Layer map containing `:=bins` specification
3037+
;; - `points`: Sequence of point maps with `:x` and optional `:group` keys
30243038
;;
30253039
;; Returns:
3026-
;; - For ungrouped: {:type :histogram :points points :bars [{:x-min :x-max :x-center :height}...]}
3027-
;; - For grouped: {:type :grouped-histogram :points points :groups {group-val {:bars [...] :points [...]}}}
3040+
;; - For ungrouped: `{:type :histogram :points points :bars [{:x-min :x-max :x-center :height}...]}`
3041+
;; - For grouped: `{:type :grouped-histogram :points points :groups {group-val {:bars [...] :points [...]}}}`
30283042
;;
30293043
;; Edge cases:
3030-
;; - Returns nil bars if compute-histogram fails (empty, non-numeric, or identical values)
3031-
;; - Histogram with nil bars will not render (graceful degradation)
3044+
;; - Returns `nil` bars if `compute-histogram` fails (empty, non-numeric, or identical values)
3045+
;; - Histogram with `nil` bars will not render (graceful degradation)
30323046
(defmethod apply-transform :histogram
30333047
[layer points]
30343048
(when-not (seq points)

0 commit comments

Comments
 (0)