"Breakout by" transform. Entry points:
Requirements:
For different query types/shapes different breakout columns are allowed:
Query transformation is similar to
Question transformation:
Other functions:
| (ns metabase.lib.drill-thru.pivot (:require [metabase.lib.aggregation :as lib.aggregation] [metabase.lib.breakout :as lib.breakout] [metabase.lib.drill-thru.common :as lib.drill-thru.common] [metabase.lib.filter :as lib.filter] [metabase.lib.metadata.calculation :as lib.metadata.calculation] [metabase.lib.schema :as lib.schema] [metabase.lib.schema.drill-thru :as lib.schema.drill-thru] [metabase.lib.schema.metadata :as lib.schema.metadata] [metabase.lib.types.isa :as lib.types.isa] [metabase.lib.underlying :as lib.underlying] [metabase.util.malli :as mu])) |
(mu/defn- pivot-drill-pred :- [:sequential ::lib.schema.metadata/column]
"Implementation for pivoting on various kinds of fields.
Don't call this directly; call [[pivot-drill]]."
[query :- ::lib.schema/query
stage-number :- :int
{:keys [column value]} :- ::lib.schema.drill-thru/context
field-pred :- [:=> [:cat [:schema ::lib.schema.metadata/column]] boolean?]]
(when (and (lib.drill-thru.common/mbql-stage? query stage-number)
column
(some? value)
(lib.underlying/aggregation-sourced? query column))
(->> (lib.breakout/breakoutable-columns query stage-number)
(filter field-pred)))) | |
(def ^:private pivot-type-predicates
{:category (every-pred lib.types.isa/category?
(complement lib.types.isa/address?))
:location lib.types.isa/address?
:time lib.types.isa/temporal?}) | |
(defn- breakout-type [query stage-number breakout]
(let [column (lib.metadata.calculation/metadata query stage-number breakout)]
(cond
(lib.types.isa/temporal? column) :date
(lib.types.isa/address? column) :address
(lib.types.isa/category? column) :category))) | |
(mu/defn- permitted-pivot-types :- [:maybe [:set ::lib.schema.drill-thru/pivot-types]]
"This captures some complex conditions formerly encoded by `visualizations/click-actions/Mode/*` in the FE.
See [here](https://github.com/metabase/metabase/blob/f4415fec8563353615ef600f52de871507a052ec/frontend/src/metabase/visualizations/click-actions/Mode/utils.ts#L15)
for the original logic. (It returns `MODE_TYPE_*` enums, which are referenced below.)
Pivot drills are only available in certain conditions, like all drills: structured queries with aggregation(s), when
clicking a specific cell.
- No breakouts: any pivot is permitted. (`metric` mode)
- Exactly one date breakout, with an optional category breakout: no `:time` pivot. (`timeseries` mode)
- Exactly one breakout and it's an address: no `:location` pivot. (`geo` mode)
- One or two category breakouts: no `:location` pivot. (`pivot` mode)
- If all these conditions fail, no pivots are allowed and the pivot drill should not be returned.
This function encodes all these rules, returning a (possibly emtpy) set of permitted types."
[query :- ::lib.schema/query
stage-number :- :int]
(case (->> (lib.breakout/breakouts query stage-number)
(map #(breakout-type query stage-number %))
frequencies)
({:date 1}
{:date 1, :category 1})
#{:category :location}
{:address 1}
#{:category :time}
{}
#{:category :location :time}
({:category 1} {:category 2})
#{:category :time}
;; If there are breakouts but none of those conditions matched, no pivots are permitted.
#{})) | |
(mu/defn pivot-drill :- [:maybe ::lib.schema.drill-thru/drill-thru.pivot]
"Return all possible pivoting options on the given column and value.
See `:pivots` key, which holds a map `{t [breakouts...]}` where `t` is `:category`, `:location`, or `:time`.
If a key is missing, there are no breakouts of that kind."
[query :- ::lib.schema/query
_stage-number :- :int
{:keys [column dimensions value] :as context} :- ::lib.schema.drill-thru/context]
(let [stage-number (lib.underlying/top-level-stage-number query)]
(when (and (lib.drill-thru.common/mbql-stage? query stage-number)
column
(some? value)
(lib.underlying/aggregation-sourced? query column)
(-> (lib.aggregation/aggregations query stage-number) count pos?))
(let [breakout-pivot-types (permitted-pivot-types query stage-number)
pivots (into {} (for [pivot-type breakout-pivot-types
:let [pred (get pivot-type-predicates pivot-type)
columns (pivot-drill-pred query stage-number context pred)]
:when (not-empty columns)]
[pivot-type columns]))]
(when-not (empty? pivots)
{:lib/type :metabase.lib.drill-thru/drill-thru
:type :drill-thru/pivot
:dimensions dimensions
:pivots pivots
:stage-number stage-number}))))) | |
(defmethod lib.drill-thru.common/drill-thru-info-method :drill-thru/pivot [_query _stage-number drill-thru] (select-keys drill-thru [:many-pks? :object-id :type])) | |
Note that pivot drills have specific public functions for accessing the nested pivoting options. | |
(mu/defn pivot-types :- [:sequential ::lib.schema.drill-thru/pivot-types]
"A helper for the FE. Returns the set of pivot types (category, location, time) that apply to this drill-thru."
[drill-thru :- [:and ::lib.schema.drill-thru/drill-thru
[:map [:type [:= :drill-thru/pivot]]]]]
(-> drill-thru :pivots keys sort)) | |
(mu/defn pivot-columns-for-type :- [:sequential ::lib.schema.metadata/column]
"A helper for the FE. Returns all the columns of the given type which can be used to pivot the query."
[drill-thru :- [:and ::lib.schema.drill-thru/drill-thru
[:map [:type [:= :drill-thru/pivot]]]]
pivot-type :- ::lib.schema.drill-thru/pivot-types]
(get-in drill-thru [:pivots pivot-type])) | |
(defn- breakouts->filters [query stage-number {:keys [column value] :as _dimension}]
(let [resolved-column (lib.drill-thru.common/breakout->resolved-column query stage-number column)]
(-> query
(lib.breakout/remove-existing-breakouts-for-column stage-number column)
(lib.filter/filter stage-number (lib.filter/= resolved-column value))))) | |
Pivot drills are in play when clicking an aggregation cell. Pivoting is applied by: 1. For each "dimension", ie. the specific values for all breakouts at the originally clicked cell: a. Filter the query to have the dimension's column = the dimension's value at that cell. b. Go through the breakouts, and remove any that match this dimension from the query. 2. Add a new breakout for the selected column. | (defmethod lib.drill-thru.common/drill-thru-method :drill-thru/pivot
[query
_stage-number
{:keys [stage-number dimensions] :as _drill-thru}
& [column]]
(let [filtered (reduce #(breakouts->filters %1 stage-number %2) query dimensions)]
(lib.breakout/breakout filtered stage-number column))) |