(ns metabase.lib.binning (:require [clojure.set :as set] [clojure.string :as str] [metabase.lib.binning.util :as lib.binning.util] [metabase.lib.dispatch :as lib.dispatch] [metabase.lib.hierarchy :as lib.hierarchy] [metabase.lib.metadata.calculation :as lib.metadata.calculation] [metabase.lib.schema :as lib.schema] [metabase.lib.schema.binning :as lib.schema.binning] [metabase.lib.schema.common :as lib.schema.common] [metabase.lib.schema.metadata :as lib.schema.metadata] [metabase.lib.util :as lib.util] [metabase.util :as u] [metabase.util.formatting.numbers :as fmt.num] [metabase.util.i18n :as i18n] [metabase.util.malli :as mu])) | |
Implementation for [[with-binning]]. Implement this to tell [[with-binning]] how to add binning to a particular MBQL clause. | (defmulti with-binning-method {:arglists '([x binning])} (fn [x _binning] (lib.dispatch/dispatch-value x)) :hierarchy lib.hierarchy/hierarchy) |
Add binning to an MBQL clause or something that can be converted to an MBQL clause.
Eg. for a Field or Field metadata or (with-binning some-field (bin-by :num-bins 4)) => [:field {:binning {:strategy :num-bins :num-bins 4}} 1] Pass | (mu/defn with-binning {:style/indent [:form]} [x binning :- [:maybe [:or ::lib.schema.binning/binning ::lib.schema.binning/binning-option]]] (with-binning-method x (if (contains? binning :mbql) (:mbql binning) binning))) |
Implementation of [[binning]]. Return the current binning options associated with | (defmulti binning-method {:arglists '([x])} lib.dispatch/dispatch-value :hierarchy lib.hierarchy/hierarchy) |
(defmethod binning-method :default [_x] nil) | |
(mu/defn binning :- [:maybe ::lib.schema.binning/binning] "Get the current binning options associated with `x`, if any." [x] (binning-method x)) | |
Implementation for [[available-binning-strategies]]. Return a set of binning strategies from
| (defmulti available-binning-strategies-method {:arglists '([query stage-number x])} (fn [_query _stage-number x] (lib.dispatch/dispatch-value x)) :hierarchy lib.hierarchy/hierarchy) |
(defmethod available-binning-strategies-method :default [_query _stage-number _x] nil) | |
(mu/defn available-binning-strategies :- [:sequential [:ref ::lib.schema.binning/binning-option]] "Get a set of available binning strategies for `x`. Returns nil if none are available." ([query x] (available-binning-strategies query -1 x)) ([query :- ::lib.schema/query stage-number :- :int x] (available-binning-strategies-method query stage-number x))) | |
(mu/defn default-auto-bin :- ::lib.schema.binning/binning-option "Returns the basic auto-binning strategy. Public because it's used directly by some drill-thrus." [] {:lib/type :option/binning :display-name (i18n/tru "Auto bin") :default true :mbql {:strategy :default}}) | |
(defn- with-binning-option-type [m] (assoc m :lib/type :option/binning)) | |
(mu/defn numeric-binning-strategies :- [:sequential ::lib.schema.binning/binning-option] "List of binning options for numeric fields. These split the data evenly into a fixed number of bins." [] (mapv with-binning-option-type [(default-auto-bin) {:display-name (i18n/tru "10 bins") :mbql {:strategy :num-bins :num-bins 10}} {:display-name (i18n/tru "50 bins") :mbql {:strategy :num-bins :num-bins 50}} {:display-name (i18n/tru "100 bins") :mbql {:strategy :num-bins :num-bins 100}}])) | |
(mu/defn coordinate-binning-strategies :- [:sequential ::lib.schema.binning/binning-option] "List of binning options for coordinate fields (ie. latitude and longitude). These split the data into as many ranges as necessary, with each range being a certain number of degrees wide." [] (mapv with-binning-option-type [(default-auto-bin) {:display-name (i18n/tru "Bin every 0.1 degrees") :mbql {:strategy :bin-width :bin-width 0.1}} {:display-name (i18n/tru "Bin every 1 degree") :mbql {:strategy :bin-width :bin-width 1.0}} {:display-name (i18n/tru "Bin every 10 degrees") :mbql {:strategy :bin-width :bin-width 10.0}} {:display-name (i18n/tru "Bin every 20 degrees") :mbql {:strategy :bin-width :bin-width 20.0}} {:display-name (i18n/tru "Bin every 0.05 degrees") :mbql {:strategy :bin-width :bin-width 0.05}} {:display-name (i18n/tru "Bin every 0.01 degrees") :mbql {:strategy :bin-width :bin-width 0.01}} {:display-name (i18n/tru "Bin every 0.005 degrees") :mbql {:strategy :bin-width :bin-width 0.005}}])) | |
(mu/defn binning-display-name :- ::lib.schema.common/non-blank-string "This is implemented outside of [[lib.metadata.calculation/display-name]] because it needs access to the field type. It's called directly by `:field` or `:metadata/column`'s [[lib.metadata.calculation/display-name]]." [{:keys [bin-width num-bins strategy] :as binning-options} :- ::lib.schema.binning/binning x :- [:maybe [:or ::lib.schema.metadata/column ::lib.schema.common/semantic-or-relation-type]]] (when binning-options (let [semantic-type (cond-> x (and (map? x) (= :metadata/column (:lib/type x))) :semantic-type)] (case strategy :num-bins (i18n/trun "{0} bin" "{0} bins" num-bins) :bin-width (str (fmt.num/format-number bin-width {}) (when (isa? semantic-type :type/Coordinate) "°")) :default (i18n/tru "Auto binned"))))) | |
(defmethod lib.metadata.calculation/display-info-method :option/binning [_query _stage-number binning-option] (select-keys binning-option [:display-name :default :selected])) | |
(defmethod lib.metadata.calculation/display-info-method ::binning [query stage-number binning-value] (let [field-metadata ((:metadata-fn binning-value) query stage-number)] (merge {:display-name (binning-display-name binning-value field-metadata)} (when (= :default (:strategy binning-value)) {:default true})))) | |
(mu/defn binning= :- boolean? "Given binning values (as returned by [[binning]]), check if they match." [x :- [:maybe ::lib.schema.binning/binning] y :- [:maybe ::lib.schema.binning/binning]] (let [binning-keys (case (:strategy x) :num-bins [:strategy :num-bins] :bin-width [:strategy :bin-width] [:strategy])] (= (select-keys x binning-keys) (select-keys y binning-keys)))) | |
(mu/defn strategy= :- boolean? "Given a binning option (as returned by [[available-binning-strategies]]) and the binning value (possibly nil) from a column, check if they match." [binning-option :- ::lib.schema.binning/binning-option column-binning :- [:maybe ::lib.schema.binning/binning]] (binning= (:mbql binning-option) column-binning)) | |
(mu/defn resolve-bin-width :- [:maybe [:map [:bin-width ::lib.schema.binning/bin-width] [:min-value number?] [:max-value number?]]] "If a `column` is binned, resolve the actual bin width that will be used when a query is processed as well as min and max values." [metadata-providerable :- ::lib.schema.metadata/metadata-providerable column-metadata :- ::lib.schema.metadata/column value :- number?] ;; TODO: I think this function is taking the wrong approach. It uses the (global) :fingerprint for all cases, and if ;; we're looking at nested bins (eg. bin a query, then zoom in on one of those bins) we have tighter min and max ;; bounds on the column's own `binning-options`. We should be using those bounds everywhere if they exist, and falling ;; back on the fingerprint only if they're not defined. (when-let [binning-options (binning column-metadata)] (case (:strategy binning-options) :num-bins ;; If the column is already binned, compute the width of this single bin based on its bounds and width. (or (when-let [bin-width (:bin-width binning-options)] {:bin-width bin-width :min-value value :max-value (+ value bin-width)}) ;; Otherwise use the fingerprint. (when-let [{min-value :min, max-value :max, :as _number-fingerprint} (get-in column-metadata [:fingerprint :type :type/Number])] (let [{:keys [num-bins]} binning-options bin-width (lib.binning.util/nicer-bin-width min-value max-value num-bins)] {:bin-width bin-width :min-value value :max-value (+ value bin-width)}))) :bin-width (let [{:keys [bin-width]} binning-options] (assert (number? bin-width)) {:bin-width bin-width :min-value value :max-value (+ value bin-width)}) :default (when-let [{min-value :min, max-value :max, :as _number-fingerprint} (get-in column-metadata [:fingerprint :type :type/Number])] (when-let [[_strategy {:keys [bin-width]}] (lib.binning.util/resolve-options metadata-providerable :default nil column-metadata min-value max-value)] {:bin-width bin-width :min-value value :max-value (+ value bin-width)}))))) | |
(defn- binning_info->binning-options [binning_info] (-> binning_info u/normalize-map (set/rename-keys {:binning-strategy :strategy}))) | |
Decide whether string | (defn ends-with-binning? [s binning-options semantic-type] (str/ends-with? s (str ": " (binning-display-name binning-options semantic-type)))) |
Ensure that | (defn ensure-ends-with-binning [s binning-options semantic-type] (if (or (not (string? s)) (not (:strategy binning-options)) (ends-with-binning? s binning-options semantic-type)) s (lib.util/format "%s: %s" s (binning-display-name binning-options semantic-type)))) |
Update results column so binning is contained in its display_name. | (defn ensure-binning-in-display-name [column] (if (:binning_info column) (update column :display_name #(ensure-ends-with-binning %1 (binning_info->binning-options (:binning_info column)) (:semantic_type column))) column)) |