How to Use — LookML Period-over-Period (PoP)¶
Period-over-Period (PoP) is a Looker pattern that lets dashboard users compare any metric across two time windows — this month vs last month, this year vs last year, and so on — without hardcoding the comparison into the view. Droughty generates the full LookML layer needed to enable this in a single command.
Note
PoP generation is currently supported for BigQuery and Snowflake. The generated LookML is identical; only the SQL dialect inside each dimension differs.
Where the config goes¶
The lookml_pop block lives inside droughty_project.yaml, which sits at the root of your git repository alongside your dbt project files:
my-analytics-repo/
├── droughty_project.yaml ← lookml_pop goes here
├── dbt_project.yml
├── models/
└── lookml/
Add it at the top level of the file, alongside your other droughty settings:
profile: my_profile
lookml_path: lookml/base
lookml_pop:
views:
fct_orders:
- order_date
The lookml_pop key is optional. If it is absent, droughty skips PoP generation entirely.
Where the generated file lands¶
The PoP layer is written to the same directory as your other base LookML files, controlled by lookml_path in droughty_project.yaml.
With the recommended folder structure, that is lookml/base/:
lookml/
└── base/
├── _base.layer.lkml ← base views (droughty lookml)
├── _aggregate.layer.lkml ← aggregate views (droughty lookml)
└── _base_parameters.layer.lkml ← PoP layer (droughty lookml, if configured)
The default filename is _base_parameters.layer and cannot be changed via config — lookml_base_filename controls the base views filename only.
Include all three files in your model:
# my_project.model.lkml
include: "/lookml/base/_base.layer.lkml"
include: "/lookml/base/_aggregate.layer.lkml"
include: "/lookml/base/_base_parameters.layer.lkml"
What gets generated¶
For each view you configure, droughty writes a Looker refinement (+view_name) into a single .lkml file. The refinement adds:
Four user-facing parameters
Parameter |
What it does |
|---|---|
|
Picks the comparison grain: day, week, month, quarter, year, or YTD. Default: month. |
|
Picks what to compare against: the previous period of the same length, or the same period one year ago. Default: previous period. |
|
Yesno. When Yes, restricts both periods to rows that fall on or before today’s equivalent position in the period (e.g. “month to date”). Default: No. |
|
Optional date picker. Overrides “today” with any date — useful for comparing historical periods. |
Per-date-field dimensions (one set for each date field you list in the config)
Dimension |
Purpose |
|---|---|
|
Hidden dimension_group casting the field to date, with all standard timeframes. |
|
Yesno. True if the row falls within the current partial period (respects |
|
The x-axis label for a chart — returns the right grain (e.g. “2024-03” for months) driven by the timeframe parameter. |
|
The position within the period (day of month, day of week, month of year, etc.) — use on the x-axis when you want both periods to share the same axis. |
|
String dimension returning the timeframe label for rows in either window, null for all others. Use this as the series breakdown on a chart. |
|
Hidden. Returns |
Paired measures (auto-generated from your table schema)
For every _pk and _fk column, droughty generates a count_distinct pair. For every numeric column it generates a sum_distinct pair:
Measure name pattern |
Filters on |
|---|---|
|
|
|
|
|
|
|
|
The label on the reference measures changes dynamically: it reads Current when no reference date is set and Reference when one is.
Step 1 — Add the config to droughty_project.yaml¶
Under lookml_pop, list each view you want PoP on and the date/timestamp columns in that table that should drive the comparison:
lookml_pop:
views:
fct_orders:
- order_date
- shipped_date
The key (
fct_orders) must match your LookML view name exactly.The list items (
order_date,shipped_date) are column names in the underlying table. You can list as many as you like.
Tip
You only need to list the columns you actually want to pivot on in a dashboard. One date field per view is the most common setup.
Step 2 — Run droughty lookml¶
droughty lookml
Droughty generates the PoP layer alongside the base views and explores. The output file is written to:
{lookml_path}/_base_parameters.layer.lkml
The filename is always _base_parameters.layer.lkml — it is not affected by lookml_base_filename, which controls only the base views output.
With the default paths this looks like:
lookml/
└── base/
└── _base_parameters.layer.lkml ← PoP refinements live here
Step 3 — Include the file in your LookML project¶
The generated file expects to be included from a manifest or model file. Add an include at the top of your model:
# my_model.model.lkml
include: "/lookml/base/_base_parameters.layer.lkml"
Because the file uses view refinements (view: +fct_orders { ... }), the base view must already be defined elsewhere — typically in the _base.layer.lkml that droughty also generates. The PoP file includes that automatically:
include: "/lookml/base/_base.layer.lkml"
Example — fct_orders with order_date¶
Given this config:
lookml_pop:
views:
fct_orders:
- order_date
Droughty generates a refinement that looks like this (abbreviated):
include: "/lookml/base/_base.layer.lkml"
view: +fct_orders {
# --- User-facing parameters ---
parameter: select_timeframe_advanced {
label: "select timeframe"
type: unquoted
default_value: "month"
allowed_value: { value: "year" label: "years" }
allowed_value: { value: "quarter" label: "quarters" }
allowed_value: { value: "month" label: "months" }
allowed_value: { value: "week" label: "weeks" }
allowed_value: { value: "day" label: "days" }
allowed_value: { value: "ytd" label: "ytd" }
}
parameter: select_comparison {
label: "select comparison type"
type: unquoted
default_value: "period"
allowed_value: { label: "previous year" value: "year" }
allowed_value: { label: "previous period" value: "period" }
}
parameter: apply_to_date_filter_advanced {
label: "apply to date filter advanced"
type: yesno
default_value: "false"
}
parameter: select_reference_date_advanced {
label: "select reference date"
type: date
convert_tz: no
}
# --- Hidden date infrastructure ---
dimension_group: current_timestamp_advanced {
type: time
hidden: yes
timeframes: [raw, hour, date, week, month, month_name, month_num,
year, hour_of_day, day_of_week_index, day_of_month, day_of_year]
sql: current_timestamp() ;;
}
# --- Per-field dimensions for order_date ---
dimension_group: order_date_date {
label: "date surrogate"
hidden: yes
type: time
datatype: datetime
timeframes: [time, hour_of_day, raw, date, day_of_month, day_of_week,
day_of_week_index, day_of_year, week, week_of_year,
month, month_name, month_num, quarter, quarter_of_year, year]
sql: cast(${TABLE}.order_date as date) ;;
}
dimension: is_to_date_advanced {
hidden: yes
type: yesno
sql:
{% if apply_to_date_filter_advanced._parameter_value == 'true' %}
{% if select_timeframe_advanced._parameter_value == 'week' %}
${fct_orders.order_date_date_day_of_week_index} <= ${current_timestamp_advanced_day_of_week_index}
{% elsif select_timeframe_advanced._parameter_value == 'month' %}
${fct_orders.order_date_date_day_of_month} <= ${current_timestamp_advanced_day_of_month}
{% elsif select_timeframe_advanced._parameter_value == 'year' %}
${fct_orders.order_date_date_day_of_year} <= ${current_timestamp_advanced_day_of_year}
{% else %} true
{% endif %}
{% else %} true
{% endif %} ;;
}
dimension: selected_dynamic_timeframe_advanced {
hidden: yes
type: string
label_from_parameter: select_timeframe_advanced
sql:
{% if select_timeframe_advanced._parameter_value == 'day' %} ${fct_orders.order_date_date_date}
{% elsif select_timeframe_advanced._parameter_value == 'week' %} ${fct_orders.order_date_date_week}
{% elsif select_timeframe_advanced._parameter_value == 'year' %} ${fct_orders.order_date_date_year}
{% elsif select_timeframe_advanced._parameter_value == 'quarter' %} ${fct_orders.order_date_date_quarter}
{% else %} ${fct_orders.order_date_date_month}
{% endif %} ;;
}
dimension: current_vs_previous_period_advanced {
label: "current vs previous period"
type: string
sql:
{% if select_comparison._parameter_value == 'year' %}
case
when date_trunc(${fct_orders.order_date_date_raw}, {% parameter select_timeframe_advanced %})
= date_trunc(${selected_reference_date_default_today_advanced_raw}, {% parameter select_timeframe_advanced %})
then ${selected_dynamic_timeframe_advanced}
when date_trunc(${fct_orders.order_date_date_raw}, {% parameter select_timeframe_advanced %})
= date_trunc(date_sub(${selected_reference_date_default_today_advanced_raw}, interval 1 year), {% parameter select_timeframe_advanced %})
then ${selected_dynamic_timeframe_advanced}
else null
end
{% elsif select_comparison._parameter_value == 'period' %}
case
when date_trunc(${fct_orders.order_date_date_raw}, {% parameter select_timeframe_advanced %})
= date_trunc(${selected_reference_date_default_today_advanced_raw}, {% parameter select_timeframe_advanced %})
then ${selected_dynamic_timeframe_advanced}
when date_trunc(${fct_orders.order_date_date_raw}, {% parameter select_timeframe_advanced %})
= date_trunc(date_sub(${selected_reference_date_default_today_advanced_raw}, interval 1 {% parameter select_timeframe_advanced %}), {% parameter select_timeframe_advanced %})
then ${selected_dynamic_timeframe_advanced}
else null
end
{% endif %} ;;
}
dimension: current_vs_previous_period_hidden_advanced {
hidden: yes
type: string
sql: /* same logic as above but returns 'reference' / 'comparison' instead of the date label */ ;;
}
# --- Auto-generated paired measures (from schema inspection) ---
measure: count_of_order_pk_reference_advanced {
label: "order_pk count ({% if select_reference_date_advanced._is_filtered %}Reference{% else %}Current{% endif %} {% parameter fct_orders.select_timeframe_advanced %})"
type: count_distinct
sql: ${TABLE}.order_pk ;;
filters: [current_vs_previous_period_hidden_advanced: "reference"]
}
measure: count_of_order_pk_previous_advanced {
label: "order_pk count (Previous {% parameter fct_orders.select_timeframe_advanced %})"
type: count_distinct
sql: ${TABLE}.order_pk ;;
filters: [current_vs_previous_period_hidden_advanced: "comparison"]
}
measure: sum_of_order_total_reference_advanced {
label: "order_total sum ({% if select_reference_date_advanced._is_filtered %}Reference{% else %}Current{% endif %} {% parameter fct_orders.select_timeframe_advanced %})"
type: sum_distinct
sql: ${TABLE}.order_total ;;
filters: [current_vs_previous_period_hidden_advanced: "reference"]
}
measure: sum_of_order_total_previous_advanced {
label: "order_total sum (Previous {% parameter fct_orders.select_timeframe_advanced %})"
type: sum_distinct
sql: ${TABLE}.order_total ;;
filters: [current_vs_previous_period_hidden_advanced: "comparison"]
}
}
Building a PoP dashboard in Looker¶
Once the LookML is loaded into your Looker project, follow these steps to build a period-comparison tile.
Step 1 — Add dashboard filters
Add four dashboard filters mapped to the PoP parameters on your explore:
Filter label |
Type |
Maps to parameter |
|---|---|---|
Timeframe |
String (dropdown) |
|
Comparison type |
String (dropdown) |
|
Apply to-date logic |
Boolean |
|
Reference date |
Date |
|
Step 2 — Build a line or bar chart
In the Looker Explore, add:
X-axis (Dimension):
selected_dynamic_day_of_advanced— this gives the position within the period (day 1, day 2, …) on a shared axis so both periods overlay correctlySeries breakdown (Pivot):
current_vs_previous_period_advanced— splits rows into “current” and “previous” seriesY-axis (Measure): one of the
_reference_advancedor_previous_advancedmeasures, e.g.count_of_order_pk_reference_advanced
Tip
Use selected_dynamic_day_of_advanced (positional) on the x-axis when overlaying two periods. Use selected_dynamic_timeframe_advanced (absolute date label) on the x-axis when showing both periods side by side in a bar chart.
Step 3 — Filter out null rows
The current_vs_previous_period_advanced dimension returns null for any row that doesn’t fall in either window. Add a filter on the explore to exclude nulls:
current_vs_previous_period_advanced is not null
This keeps only the two comparison windows in the result set.
How the comparison windows are calculated¶
The core logic lives in current_vs_previous_period_advanced and its hidden counterpart. Here is the decision tree in plain language:
┌─ YTD mode? ─────────────────────────────────────────────────────────┐
│ Reference window: Jan 1 of this year → today │
│ Comparison window: Jan 1 of last year → this day last year │
└───────────────────────────────────────────────────────────────────────┘
┌─ Comparison = "previous year" ──────────────────────────────────────┐
│ Reference window: the full {timeframe} containing the reference date │
│ Comparison window: the same {timeframe} one year earlier │
│ e.g. timeframe=month, date=2024-05-14 → │
│ reference: May 2024 │
│ comparison: May 2023 │
└───────────────────────────────────────────────────────────────────────┘
┌─ Comparison = "previous period" ────────────────────────────────────┐
│ Reference window: the full {timeframe} containing the reference date │
│ Comparison window: the immediately preceding {timeframe} │
│ e.g. timeframe=month, date=2024-05-14 → │
│ reference: May 2024 │
│ comparison: April 2024 │
└───────────────────────────────────────────────────────────────────────┘
When apply_to_date_filter_advanced is Yes, both windows are truncated at today’s equivalent position — so “May 2024” only includes days 1–14 if today is the 14th.
When select_reference_date_advanced is set, “today” is replaced by that date throughout the calculation.
Multiple date fields on one view¶
You can list multiple date columns for a single view:
lookml_pop:
views:
fct_orders:
- order_date
- shipped_date
Each field in the list gets its own full set of dimensions (order_date_date, shipped_date_date, is_to_date_advanced, etc.) and the paired measures are generated once using the table’s schema. Use the order_date-based dimensions for a “placed orders” comparison, and the shipped_date-based dimensions for a “shipped orders” comparison.
Warning
The parameters (select_timeframe_advanced, select_comparison, etc.) are shared across all date fields — they are added once to the view, not once per field. Both field sets will respond to the same dashboard filter.
Multiple views¶
Each view listed under lookml_pop.views gets its own independent set of parameters and dimensions:
lookml_pop:
views:
fct_orders:
- order_date
fct_sessions:
- session_start_date
Parameters on fct_orders are entirely separate from those on fct_sessions, so dashboard filters need to be mapped to the correct explore.