|
166 | 166 | ;; Malli - Schema validation |
167 | 167 | [malli.core :as m] |
168 | 168 | [malli.error :as me] |
| 169 | + [malli.util :as mu] |
169 | 170 |
|
170 | 171 | ;; RDatasets - Example datasets |
171 | 172 | [scicloj.metamorph.ml.rdatasets :as rdatasets])) |
|
556 | 557 | ;; **You can skim this section** - it's reference material. The schemas will be |
557 | 558 | ;; used by validation helpers later, and referenced in examples as needed. |
558 | 559 |
|
| 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 | + |
559 | 569 | ;; ### ⚙️ Core Type Schemas |
560 | 570 |
|
561 | 571 | (def DataType |
|
662 | 672 |
|
663 | 673 | ;; ### ⚙️ Layer Schema |
664 | 674 |
|
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." |
676 | 677 | [:map |
677 | 678 | ;; Data (required for most layers) |
678 | 679 | [:=data {:optional true} Dataset] |
679 | 680 |
|
680 | | - ;; Positional aesthetics |
681 | | - [:=x {:optional true} PositionalAesthetic] |
682 | | - [:=y {:optional true} PositionalAesthetic] |
683 | | - |
684 | 681 | ;; Other aesthetics |
685 | 682 | [:=color {:optional true} ColorAesthetic] |
686 | 683 | [:=size {:optional true} SizeAesthetic] |
|
692 | 689 | ;; Attributes (constant visual properties) |
693 | 690 | [:=alpha {:optional true} AlphaAttribute] |
694 | 691 |
|
695 | | - ;; Plot type and transformation |
696 | | - [:=plottype {:optional true} PlotType] |
| 692 | + ;; Transformation |
697 | 693 | [:=transformation {:optional true} Transformation] |
698 | 694 |
|
699 | 695 | ;; Histogram-specific |
|
704 | 700 | [:=scale-y {:optional true} ScaleSpec] |
705 | 701 | [:=scale-color {:optional true} ScaleSpec]]) |
706 | 702 |
|
| 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 | + |
707 | 770 | (def Layers |
708 | 771 | "Schema for one or more layers. |
709 | 772 | |
|
784 | 847 | "Validate a layer with context-aware checks. |
785 | 848 | |
786 | 849 | Performs: |
787 | | - 1. Schema validation (structure) |
788 | | - 2. Semantic validation (required fields for plottype) |
789 | | - 3. Data validation (columns exist) |
| 850 | + 1. Schema validation (structure + plottype-specific requirements via :multi) |
| 851 | + 2. Data column validation (columns exist) - runtime check |
790 | 852 | |
791 | 853 | Returns nil if valid, error map if invalid." |
792 | 854 | [layer] |
793 | | - ;; First check schema |
| 855 | + ;; Schema validation now handles both structure AND plottype-specific requirements |
794 | 856 | (or |
795 | 857 | (when-let [schema-errors (validate Layer layer)] |
796 | 858 | {:type :schema-error |
797 | 859 | :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 |
| 860 | + :message "Layer validation failed"}) |
| 861 | + |
| 862 | + ;; Data column validation (runtime check - can't be done in schema) |
846 | 863 | (when-let [data (:=data layer)] |
847 | 864 | (let [column-keys (cond |
848 | 865 | ;; Tablecloth dataset |
@@ -2383,7 +2400,6 @@ iris |
2383 | 2400 | (=* attrs-or-spec (scatter)) |
2384 | 2401 | (let [result (merge {:=plottype :scatter} |
2385 | 2402 | (update-keys attrs-or-spec =key))] |
2386 | | - (validate! Layer result) |
2387 | 2403 | {:=layers [result]}))) |
2388 | 2404 | ([spec attrs] |
2389 | 2405 | ;; Threading-friendly: (-> spec (scatter {:alpha 0.5})) |
@@ -2685,7 +2701,6 @@ iris |
2685 | 2701 | ([] |
2686 | 2702 | (let [result {:=transformation :linear |
2687 | 2703 | :=plottype :line}] |
2688 | | - (validate! Layer result) |
2689 | 2704 | {:=layers [result]})) |
2690 | 2705 | ([spec-or-data] |
2691 | 2706 | (let [spec (if (plot-spec? spec-or-data) |
@@ -2966,7 +2981,6 @@ iris |
2966 | 2981 | :=plottype :bar |
2967 | 2982 | :=bins :sturges} |
2968 | 2983 | (update-keys opts-or-spec =key))] |
2969 | | - (validate! Layer result) |
2970 | 2984 | {:=layers [result]}))) |
2971 | 2985 | ([spec opts] |
2972 | 2986 | (=* spec (histogram opts)))) |
|
0 commit comments