├── .gitattributes
├── LICENSE
├── LookML.sublime-syntax
├── README.md
├── lookml-sublime-syntax.png
└── test_files
├── 01_order_items.view.lkml
├── 02_users.view.lkml
├── 03_inventory_items.view.lkml
├── 04_products.view.lkml
├── 05_distribution_centers.view.lkml
├── 11_order_facts.view.lkml
├── 12_user_order_facts.view.lkml
├── 13_repeat_purchase_facts.view.lkml
├── 22_affinity.view.lkml
├── 23_share_of_wallet.view.lkml
├── 24_inventory_snapshot.view.lkml
├── 25_trailing_sales_snapshot.view.lkml
├── 51_events.view.lkml
├── 52_sessions.view.lkml
├── 97_web_analytics_data_tool.view.lkml
├── 98_cohort_data_tool.view.lkml
├── 99_users_avatar.view.lkml
└── thelook.model.lkml
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Jon Walls
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/LookML.sublime-syntax:
--------------------------------------------------------------------------------
1 | %YAML 1.2
2 | ---
3 | # http://www.sublimetext.com/docs/3/syntax.html
4 | # https://www.sublimetext.com/docs/3/scope_naming.html#color_schemes
5 |
6 | name: LookML
7 | file_extensions:
8 | - lkml
9 | scope: source.looker
10 |
11 | contexts:
12 |
13 | # LOOKML
14 | main:
15 | - match: '#'
16 | scope: comment.looker
17 | push:
18 | - meta_scope: comment.looker
19 | - match: \n
20 | pop: true
21 |
22 | - include: keywords
23 | - include: numbers
24 | - include: variables
25 | - include: constants
26 | - include: double_quoted_strings
27 | - include: lists
28 |
29 | keywords:
30 | - match: '\b(explore|view|filter|parameter|dimension|dimension_group|measure|set)\b: ([A-Za-z0-9_-]+) ?({)'
31 | captures:
32 | 1: entity.name.looker
33 | 2: storage.type.looker
34 | 3: punctuation.definition.end.looker
35 |
36 | - match: '\b(sql_table_name|sql_on|sql_trigger|sql_latitude|sql_longitude|sql)\b(: ?)'
37 | captures:
38 | 1: variable.parameter.looker
39 | push: sql
40 |
41 | - match: '\b(html)\b(: ?)'
42 | captures:
43 | 1: variable.parameter.looker
44 | push: html
45 |
46 | - match: '\b(url|icon_url)\b(: ?)(")'
47 | captures:
48 | 1: variable.parameter.looker
49 | push: url
50 |
51 | - match: '\b([a-z0-9_]+)\b(: ?)([A-Za-z0-9_\-]+ ?)'
52 | captures:
53 | 1: variable.parameter.looker
54 | 3: storage.type.looker
55 |
56 | - match: '\b(?i)([a-z0-9_]+)\b(: ?)'
57 | captures:
58 | 1: variable.parameter.looker
59 |
60 | - match: '\b(string|unquoted|number|date|time|datetime|yesno)\b'
61 | scope: storage.type.looker
62 |
63 | - match: '\b(count|count_distinct|sum|average|median)\b'
64 | scope: storage.type.looker
65 |
66 | - match: '\b(gbp|gbp_0|usd|usd_0|percent_0|percent_1|percent_2)\b'
67 | scope: storage.type.looker
68 |
69 | - match: (})
70 | scope: punctuation.definition.end.looker
71 |
72 | double_quoted_strings:
73 | - match: '"'
74 | scope: punctuation.definition.string.begin.looker
75 | push:
76 | - meta_scope: string.quoted.double.looker
77 | - include: templates
78 | - match: '"'
79 | scope: punctuation.definition.string.end.looker
80 | pop: true
81 |
82 | single_quoted_strings:
83 | - match: "'"
84 | scope: punctuation.definition.string.begin.looker
85 | push:
86 | - meta_scope: string.quoted.single.looker
87 | - include: templates
88 | - match: "'"
89 | scope: punctuation.definition.string.end.looker
90 | pop: true
91 |
92 | variables:
93 | - match: '\$\{'
94 | scope: punctuation.definition.variable.begin.looker
95 | push:
96 | - meta_scope: storage.type.field_reference.looker
97 | - match: '\}'
98 | scope: punctuation.definition.variable.end.looker
99 | pop: true
100 |
101 | constants:
102 | - match: '\b(yes|no)\b'
103 | scope: constant.language.looker
104 |
105 | numbers:
106 | - match: '\b([0-9/.]+)'
107 | scope: constant.numeric.looker
108 |
109 | templates:
110 | - match: '\{\%'
111 | scope: punctuation.separator.looker
112 | push: liquid_control
113 |
114 | - match: '\{\{'
115 | scope: punctuation.separator.liquid.looker
116 | push: liquid_variable
117 |
118 | # LISTS
119 | lists:
120 | - match: '\['
121 | scope: punctuation.definition.begin.looker
122 | push:
123 | - meta_scope: string.list.looker
124 | - match: '#'
125 | scope: comment.looker
126 | push:
127 | - meta_scope: comment.looker
128 | - match: \n
129 | pop: true
130 | - match: '[a-zA-Z0-9_-]'
131 | scope: storage.type.list.looker
132 | - match: '\]'
133 | scope: punctuation.definition.end.looker
134 | pop: true
135 |
136 | # SQL
137 | sql:
138 | - clear_scopes: true
139 | - meta_scope: string.sql.looker
140 | - include: variables
141 | - include: numbers
142 | - include: double_quoted_strings
143 | - include: single_quoted_strings
144 | - include: templates
145 |
146 | - match: '\-\-'
147 | scope: comment.sql.looker
148 | push:
149 | - meta_scope: comment.sql.looker
150 | - match: '\n'
151 | pop: true
152 |
153 | - match: (\-|\+|\*|\/|%\/%|%%|\^|\|\|)
154 | scope: keyword.operator.arithmetic.looker
155 |
156 | - match: '\b(=|!=|<>|<|>|<=|>=|AND|OR|IS|NOT)\b'
157 | scope: keyword.operator.comparison.looker
158 |
159 | - match: '(?i)\b(SELECT|AS|FROM|WHERE|GROUP BY|ORDER BY|HAVING|LEFT|JOIN|ON)\b'
160 | scope: keyword.control.sql.looker
161 |
162 | - match: '(?i)\b(CASE|WHEN|IN|THEN|ELSE|END)\b'
163 | scope: keyword.control.sql.looker
164 |
165 | - match: '(?i)\b(ABS|ACOS|ASCII|ASIN|ATAN|ATAN2|AVG|CEILING|COALESCE|COS|COT|COUNT|COUNTD|DEGREES|DIV|EXP|FLOAT|FLOOR|HEXBINX|HEXBINY|INT|LN|LOG|MAX|MIN|NOW|NULLIF|OVER|PARTITION BY|POWER|RADIANS|ROUND|RUNNING_AVG|RUNNING_COUNT|RUNNING_MAX|RUNNING_MIN|RUNNING_SUM|SIGN|SIN|SQRT|SQUARE|STDEV|STDEVP|SUM|TAN|TOTAL|VAR|VARP|WINDOW_AVG|WINDOW_COUNT|WINDOW_MAX|WINDOW_MEDIAN|WINDOW_MIN|WINDOW_PERCENTILE|WINDOW_STDEV|WINDOW_STDEVP|WINDOW_SUM|WINDOW_VAR|WINDOW_VARP|YEAR|ATTR|CHAR|CONTAINS|ENDSWITH|EXCLUDE|FIND|FIRST|FIXED|FULLNAME|IFNULL|INCLUDE|INDEX|ISDATE|ISFULLNAME|ISMEMBEROF|ISNULL|ISUSERNAME|LAST|LEFT|LEN|LOOKUP|LOWER|LTRIM|MID|PREVIOUS_VALUE|RANK|RANK_DENSE|RANK_MODIFIED_RANK_PERCENTILE|RANK_UNIQUE|RAWSQLAGG_BOOL|RAWSQLAGG_DATE|RAWSQLAGG_DATETIME|RAWSQLAGG_INT|RAWSQLAGG_REAL|RAWSQLAGG_STR|RAWSQL_BOOL|RAWSQL_DATE|RAWSQL_DATETIME|RAWSQL_INT|RAWSQL_REAL|RAWSQL_STR|REPLACE|RIGHT|RTRIM|SCRIPT_BOOL|SCRIPT_INT|SCRIPT_REAL|SCRIPT_STR|SIZE|SPACE|SPLIT|STARTSWITH|STR|TODAY|TRIM|UPPER|USERDOMAIN|USERNAME|ZN)\b'
166 | scope: variable.function.sql.looker
167 |
168 | - match: '\b(?i)(TRUE|FALSE|NULL|PI)\b'
169 | scope: constant.language.sql.looker
170 |
171 | - match: '(?i)\b(CURRENT\_?DATE|GET\_?DATE|DATE\_?ADD|DATE\_?DIFF|DATE\_?NAME|DATE\_?PART|DATETIME|DATE\_?TRUNC|EXTRACT|FROM|TIMESTAMP|DATE_FROM_PARTS)\b'
172 | scope: variable.function.sql.looker
173 |
174 | - match: '\b(?i)(hour|day|dayofweek|dayofyear|week|month|year)\b'
175 | scope: constant.language.sql.looker
176 |
177 | - match: ';;'
178 | scope: comment.endofstatement.looker
179 | pop: true
180 |
181 | # HTML
182 | html:
183 | - clear_scopes: true
184 | - meta_scope: text.html.basic.looker
185 | - include: templates
186 |
187 | - match: '<(/?)([a-zA-Z]+)'
188 | captures:
189 | 1: punctuation.definition.tag.begin.html.looker
190 | 2: entity.name.tag.html.looker
191 | push: html_tag
192 |
193 | - match: ';;'
194 | scope: comment.endofstatement.hmtl.looker
195 | pop: true
196 |
197 | html_tag:
198 | - meta_scope: text.html.looker
199 | - include: numbers
200 |
201 | - match: '(src|href)(=\")'
202 | captures:
203 | 1: entity.name.tag.attribue.looker
204 | push: url
205 |
206 | - match: '[a-zA-Z]+'
207 | scope: entity.name.tag.attribute.looker
208 |
209 | - match: '\=\"'
210 | scope: punctuation.html.tag.looker
211 | push: html_tag_attribute_string
212 |
213 | - match: '\>'
214 | scope: punctuation.definition.tag.end.html.looker
215 | pop: true
216 |
217 | html_tag_attribute_string:
218 | - clear_scopes: true
219 | - include: numbers
220 | - include: templates
221 | - meta_scope: string.html.tag.attribute.value.looker
222 |
223 | - match: '([a-zA-Z\-]+):'
224 | captures:
225 | 1: meta.attribute-with-value.html.looker
226 |
227 | - match: '([a-zA-Z\-]+)'
228 | scope: constant.language.html.tag.attribute.value.looker
229 |
230 | - match: '"'
231 | scope: punctuation.html.tag.looker
232 | pop: true
233 |
234 | # URL
235 | url:
236 | - clear_scopes: true
237 | - meta_scope: string.url.looker
238 | - include: templates
239 |
240 | - match: (https?:\/\/)
241 | captures:
242 | 1: entity.name.tag.html.looker
243 |
244 | - match: '(\?|\&)([a-zA-Z0-9_]+)(\=)'
245 | captures:
246 | 2: storage.type.url_param.looker
247 |
248 | - match: '"'
249 | pop: true
250 |
251 | # LIQUID TEMPLATES
252 | liquid_control:
253 | - clear_scopes: true
254 | - include: numbers
255 | - meta_scope: variable.function.liquid.control.looker
256 |
257 | - match: \bassign|parameter\b
258 | scope: keyword.liquid.looker
259 |
260 | - match: \b(if|elsif|else|endif|unless|endunless|case|when|endcase|condition|endcondition)\b
261 | scope: keyword.control.liquid.looker
262 |
263 | - match: (==|!=|<>|<|>|<=|>=|or|and|contains)
264 | scope: keyword.operator.comparison.looker
265 |
266 | - match: \b([a-zA-Z0-9_]+)\b
267 | scope: storage.type.liquid.looker
268 |
269 | - include: double_quoted_strings
270 | - include: single_quoted_strings
271 |
272 | - match: '\%\}'
273 | scope: punctuation.terminator.liquid.looker
274 | pop: true
275 |
276 | liquid_variable:
277 | - clear_scopes: true
278 | - meta_scope: variable.function.liquid.variable.looker
279 | - match: '\}\}'
280 | scope: punctuation.terminator.liquid.looker
281 | pop: true
282 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # lookml-sublime-syntax – Looker Syntax Highlighting for Sublime Text 3
2 |
3 | This file can be added to your Sublime Text 3 configuration to display LookML files with syntax highlighting. This applies to both LookML and embedded Liquid, HTML, URLs and SQL code.
4 |
5 | ## Status and Support
6 |
7 | The sublime-syntax format for syntax definitions is supported by Sublime Text 3 only; prior versions will not work.
8 |
9 | This syntax definition is NOT supported or warranteed by Looker in any way. Please do not contact Looker for support. Issues can be logged via https://github.com/ContrastingSounds/lookml-sublime-syntax/issues
10 |
11 | ## Installation
12 |
13 | 1. Download the file, either by cloning the git repository (which includes a directory of test LookML files), or saving this link directly to your computer: https://raw.githubusercontent.com/ContrastingSounds/lookml-sublime-syntax/master/LookML.sublime-syntax
14 | 2. On Mac:
15 | - Go to Sublime Text -> Preferences -> Browse Packages
16 | - This will open up Sublime's Packages folder in a Finder window
17 | - Drop the 'LookML.sublime-syntax' file in to the folder
18 | 3. On Windows:
19 | - Go to Preferences -> Browse Packages
20 | - This will open up Sublime's Packages folder in a File Explorer window
21 | - Drop the 'LookML.sublime-syntax' file in to the folder
22 |
23 | ## Usage
24 |
25 | Once installed, Sublime Text 3 will now display any .lkml file with full highlighting for LookML syntax:
26 |
27 | 
28 |
--------------------------------------------------------------------------------
/lookml-sublime-syntax.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ContrastingSounds/lookml-sublime-syntax/e8328d20f882a6f5db591d0957d7eb0d1fe460e5/lookml-sublime-syntax.png
--------------------------------------------------------------------------------
/test_files/01_order_items.view.lkml:
--------------------------------------------------------------------------------
1 | view: order_items {
2 | sql_table_name: order_items ;;
3 | ########## IDs, Foreign Keys, Counts ###########
4 |
5 | dimension: id {
6 | primary_key: yes
7 | type: number
8 | sql: ${TABLE}.id ;;
9 | }
10 |
11 | dimension: inventory_item_id {
12 | type: number
13 | hidden: yes
14 | sql: ${TABLE}.inventory_item_id ;;
15 | }
16 |
17 | dimension: user_id {
18 | type: number
19 | hidden: yes
20 | sql: ${TABLE}.user_id ;;
21 | }
22 |
23 | measure: count {
24 | type: count_distinct
25 | sql: ${id} ;;
26 | drill_fields: [detail*]
27 | }
28 |
29 | measure: order_count {
30 | view_label: "Orders"
31 | type: count_distinct
32 | drill_fields: [detail*]
33 | sql: ${order_id} ;;
34 | }
35 |
36 |
37 | measure: count_last_28d {
38 | label: "Count Sold in Trailing 28 Days"
39 | type: count_distinct
40 | sql: ${id} ;;
41 | hidden: yes
42 | filters:
43 | {field:created_date
44 | value: "28 days"
45 | }}
46 |
47 | dimension: order_id {
48 | type: number
49 | sql: ${TABLE}.order_id ;;
50 |
51 |
52 | action: {
53 | label: "Send this to slack channel"
54 | url: "https://hooks.zapier.com/hooks/catch/1662138/tvc3zj/"
55 |
56 | param: {
57 | name: "user_dash_link"
58 | value: "https://demo.looker.com/dashboards/160?Email={{ users.email._value}}"
59 | }
60 |
61 | form_param: {
62 | name: "Message"
63 | type: textarea
64 | default: "Hey,
65 | Could you check out order #{{value}}. It's saying its {{status._value}},
66 | but the customer is reaching out to us about it.
67 | ~{{ _user_attributes.first_name}}"
68 | }
69 |
70 | form_param: {
71 | name: "Recipient"
72 | type: select
73 | default: "zevl"
74 | option: {
75 | name: "zevl"
76 | label: "Zev"
77 | }
78 | option: {
79 | name: "slackdemo"
80 | label: "Slack Demo User"
81 | }
82 |
83 | }
84 |
85 | form_param: {
86 | name: "Channel"
87 | type: select
88 | default: "cs"
89 | option: {
90 | name: "cs"
91 | label: "Customer Support"
92 | }
93 | option: {
94 | name: "general"
95 | label: "General"
96 | }
97 |
98 | }
99 |
100 |
101 | }
102 |
103 |
104 |
105 | }
106 |
107 | ########## Time Dimensions ##########
108 |
109 | dimension_group: returned {
110 | type: time
111 | timeframes: [time, date, week, month, raw]
112 | sql: ${TABLE}.returned_at ;;
113 | }
114 |
115 | dimension_group: shipped {
116 | type: time
117 | timeframes: [date, week, month, raw]
118 | sql: ${TABLE}.shipped_at ;;
119 | }
120 |
121 | dimension_group: delivered {
122 | type: time
123 | timeframes: [date, week, month, raw]
124 | sql: ${TABLE}.delivered_at ;;
125 | }
126 |
127 | dimension_group: created {
128 | #X# group_label:"Order Date"
129 | type: time
130 | timeframes: [time, hour, date, week, month, year, hour_of_day, day_of_week, month_num, raw, week_of_year]
131 | sql: ${TABLE}.created_at ;;
132 | }
133 |
134 | dimension: reporting_period {
135 | group_label: "Order Date"
136 | sql: CASE
137 | WHEN date_part('year',${created_raw}) = date_part('year',current_date)
138 | AND ${created_raw} < CURRENT_DATE
139 | THEN 'This Year to Date'
140 |
141 | WHEN date_part('year',${created_raw}) + 1 = date_part('year',current_date)
142 | AND date_part('dayofyear',${created_raw}) <= date_part('dayofyear',current_date)
143 | THEN 'Last Year to Date'
144 |
145 | END
146 | ;;
147 | }
148 |
149 | dimension: days_since_sold {
150 | hidden: yes
151 | sql: datediff('day',${created_raw},CURRENT_DATE) ;;
152 | }
153 |
154 | dimension: months_since_signup {
155 | view_label: "Orders"
156 | type: number
157 | sql: DATEDIFF('month',${users.created_raw},${created_raw}) ;;
158 | }
159 |
160 | ########## Logistics ##########
161 |
162 | dimension: status {
163 | sql: ${TABLE}.status ;;
164 | }
165 |
166 | dimension: days_to_process {
167 | type: number
168 | sql: CASE
169 | WHEN ${status} = 'Processing' THEN DATEDIFF('day',${created_raw},GETDATE())*1.0
170 | WHEN ${status} IN ('Shipped', 'Complete', 'Returned') THEN DATEDIFF('day',${created_raw},${shipped_raw})*1.0
171 | WHEN ${status} = 'Cancelled' THEN NULL
172 | END
173 | ;;
174 | }
175 |
176 | dimension: shipping_time {
177 | type: number
178 | sql: datediff('day',${shipped_raw},${delivered_raw})*1.0 ;;
179 | }
180 |
181 | measure: average_days_to_process {
182 | type: average
183 | value_format_name: decimal_2
184 | sql: ${days_to_process} ;;
185 | }
186 |
187 | measure: average_shipping_time {
188 | type: average
189 | value_format_name: decimal_2
190 | sql: ${shipping_time} ;;
191 | }
192 |
193 | ########## Financial Information ##########
194 |
195 | dimension: sale_price {
196 | type: number
197 | value_format_name: usd
198 | sql: ${TABLE}.sale_price ;;
199 | }
200 |
201 | dimension: gross_margin {
202 | type: number
203 | value_format_name: usd
204 | sql: ${sale_price} - ${inventory_items.cost} ;;
205 | }
206 |
207 | dimension: item_gross_margin_percentage {
208 | type: number
209 | value_format_name: percent_2
210 | sql: 1.0 * ${gross_margin}/NULLIF(${sale_price},0) ;;
211 | }
212 |
213 | dimension: item_gross_margin_percentage_tier {
214 | type: tier
215 | sql: 100*${item_gross_margin_percentage} ;;
216 | tiers: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
217 | style: interval
218 | }
219 |
220 | measure: total_sale_price {
221 | type: sum
222 | value_format_name: usd
223 | sql: ${sale_price} ;;
224 | drill_fields: [detail*]
225 | }
226 |
227 | measure: total_gross_margin {
228 | type: sum
229 | value_format_name: usd
230 | sql: ${gross_margin} ;;
231 | drill_fields: [detail*]
232 | }
233 |
234 | measure: average_sale_price {
235 | type: average
236 | value_format_name: usd
237 | sql: ${sale_price} ;;
238 | drill_fields: [detail*]
239 | }
240 |
241 | measure: median_sale_price {
242 | type: median
243 | value_format_name: usd
244 | sql: ${sale_price} ;;
245 | drill_fields: [detail*]
246 | }
247 |
248 | measure: average_gross_margin {
249 | type: average
250 | value_format_name: usd
251 | sql: ${gross_margin} ;;
252 | drill_fields: [detail*]
253 | }
254 |
255 | measure: total_gross_margin_percentage {
256 | type: number
257 | value_format_name: percent_2
258 | sql: 1.0 * ${total_gross_margin}/ NULLIF(${total_sale_price},0) ;;
259 | }
260 |
261 | measure: average_spend_per_user {
262 | type: number
263 | value_format_name: usd
264 | sql: 1.0 * ${total_sale_price} / NULLIF(${users.count},0) ;;
265 | drill_fields: [detail*]
266 | }
267 |
268 | ########## Return Information ##########
269 |
270 | dimension: is_returned {
271 | type: yesno
272 | sql: ${returned_raw} IS NOT NULL ;;
273 | }
274 |
275 | measure: returned_count {
276 | type: count_distinct
277 | sql: ${id} ;;
278 | filters: {
279 | field: is_returned
280 | value: "yes"
281 | }
282 | drill_fields: [detail*]
283 | }
284 |
285 | measure: returned_total_sale_price {
286 | type: sum
287 | value_format_name: usd
288 | sql: ${sale_price} ;;
289 | filters: {
290 | field: is_returned
291 | value: "yes"
292 | }
293 | }
294 |
295 | measure: return_rate {
296 | type: number
297 | value_format_name: percent_2
298 | sql: 1.0 * ${returned_count} / nullif(${count},0) ;;
299 | }
300 |
301 |
302 | ########## Repeat Purchase Facts ##########
303 |
304 | dimension: days_until_next_order {
305 | type: number
306 | view_label: "Repeat Purchase Facts"
307 | sql: DATEDIFF('day',${created_raw},${repeat_purchase_facts.next_order_raw}) ;;
308 | }
309 |
310 | dimension: repeat_orders_within_30d {
311 | type: yesno
312 | view_label: "Repeat Purchase Facts"
313 | sql: ${days_until_next_order} <= 30 ;;
314 | }
315 |
316 | measure: count_with_repeat_purchase_within_30d {
317 | type: count_distinct
318 | sql: ${id} ;;
319 | view_label: "Repeat Purchase Facts"
320 |
321 | filters: {
322 | field: repeat_orders_within_30d
323 | value: "Yes"
324 | }
325 | }
326 |
327 | measure: 30_day_repeat_purchase_rate {
328 | description: "The percentage of customers who purchase again within 30 days"
329 | view_label: "Repeat Purchase Facts"
330 | type: number
331 | value_format_name: percent_1
332 | sql: 1.0 * ${count_with_repeat_purchase_within_30d} / NULLIF(${count},0) ;;
333 | drill_fields: [products.brand, order_count, count_with_repeat_purchase_within_30d]
334 | }
335 |
336 | measure: first_purchase_count {
337 | view_label: "Orders"
338 | type: count_distinct
339 | sql: ${order_id} ;;
340 |
341 | filters: {
342 | field: order_facts.is_first_purchase
343 | value: "Yes"
344 | }
345 | # customized drill path for first_purchase_count
346 | drill_fields: [user_id, order_id, created_date, users.traffic_source]
347 | link: {
348 | label: "New User's Behavior by Traffic Source"
349 | url: "
350 | {% assign vis_config = '{
351 | \"type\": \"looker_column\",
352 | \"show_value_labels\": true,
353 | \"y_axis_gridlines\": true,
354 | \"show_view_names\": false,
355 | \"y_axis_combined\": false,
356 | \"show_y_axis_labels\": true,
357 | \"show_y_axis_ticks\": true,
358 | \"show_x_axis_label\": false,
359 | \"value_labels\": \"legend\",
360 | \"label_type\": \"labPer\",
361 | \"font_size\": \"13\",
362 | \"colors\": [
363 | \"#1ea8df\",
364 | \"#a2dcf3\",
365 | \"#929292\"
366 | ],
367 | \"hide_legend\": false,
368 | \"y_axis_orientation\": [
369 | \"left\",
370 | \"right\"
371 | ],
372 | \"y_axis_labels\": [
373 | \"Average Sale Price ($)\"
374 | ]
375 | }' %}
376 | {{ hidden_first_purchase_visualization_link._link }}&vis_config={{ vis_config | encode_uri }}&sorts=users.average_lifetime_orders+descc&toggle=dat,pik,vis&limit=5000"
377 | }
378 | }
379 |
380 | ########## Dynamic Sales Cohort App ##########
381 |
382 | filter: cohort_by {
383 | type: string
384 | hidden: yes
385 | suggestions: ["Week", "Month", "Quarter", "Year"]
386 | }
387 |
388 | filter: metric {
389 | type: string
390 | hidden: yes
391 | suggestions: ["Order Count", "Gross Margin", "Total Sales", "Unique Users"]
392 | }
393 |
394 | dimension_group: first_order_period {
395 | type: time
396 | timeframes: [date]
397 | hidden: yes
398 | sql: CAST(DATE_TRUNC({% parameter cohort_by %}, ${user_order_facts.first_order_date}) AS DATE)
399 | ;;
400 | }
401 |
402 | dimension: periods_as_customer {
403 | type: number
404 | hidden: yes
405 | sql: DATEDIFF({% parameter cohort_by %}, ${user_order_facts.first_order_date}, ${user_order_facts.latest_order_date})
406 | ;;
407 | }
408 |
409 | measure: cohort_values_0 {
410 | type: count_distinct
411 | hidden: yes
412 | sql: CASE WHEN {% parameter metric %} = 'Order Count' THEN ${id}
413 | WHEN {% parameter metric %} = 'Unique Users' THEN ${users.id}
414 | ELSE null
415 | END
416 | ;;
417 | }
418 |
419 | measure: cohort_values_1 {
420 | type: sum
421 | hidden: yes
422 | sql: CASE WHEN {% parameter metric %} = 'Gross Margin' THEN ${gross_margin}
423 | WHEN {% parameter metric %} = 'Total Sales' THEN ${sale_price}
424 | ELSE 0
425 | END
426 | ;;
427 | }
428 |
429 | measure: values {
430 | type: number
431 | hidden: yes
432 | sql: ${cohort_values_0} + ${cohort_values_1} ;;
433 | }
434 |
435 | measure: hidden_first_purchase_visualization_link {
436 | hidden: yes
437 | view_label: "Orders"
438 | type: count_distinct
439 | sql: ${order_id} ;;
440 |
441 | filters: {
442 | field: order_facts.is_first_purchase
443 | value: "Yes"
444 | }
445 | drill_fields: [users.traffic_source, user_order_facts.average_lifetime_revenue, user_order_facts.average_lifetime_orders]
446 | }
447 |
448 |
449 |
450 |
451 | ########## Sets ##########
452 |
453 | set: detail {
454 | fields: [id, order_id, status, created_date, sale_price, products.brand, products.item_name, users.portrait, users.name, users.email]
455 | }
456 | set: return_detail {
457 | fields: [id, order_id, status, created_date, returned_date, sale_price, products.brand, products.item_name, users.portrait, users.name, users.email]
458 | }
459 | }
460 |
--------------------------------------------------------------------------------
/test_files/02_users.view.lkml:
--------------------------------------------------------------------------------
1 | view: users {
2 | sql_table_name: users ;;
3 | ## Demographics ##
4 |
5 | dimension: id {
6 | primary_key: yes
7 | type: number
8 | sql: ${TABLE}.id ;;
9 | tags: ["user_id"]
10 | }
11 |
12 | dimension: first_name {
13 | hidden: yes
14 | sql: INITCAP(${TABLE}.first_name) ;;
15 | }
16 |
17 | dimension: last_name {
18 | hidden: yes
19 | sql: INITCAP(${TABLE}.last_name) ;;
20 | }
21 |
22 | dimension: name {
23 | sql: ${first_name} || ' ' || ${last_name} ;;
24 | }
25 |
26 | dimension: age {
27 | type: number
28 | sql: ${TABLE}.age ;;
29 | }
30 |
31 | dimension: age_tier {
32 | type: tier
33 | tiers: [0, 10, 20, 30, 40, 50, 60, 70]
34 | style: integer
35 | sql: ${age} ;;
36 | }
37 |
38 | dimension: gender {
39 | sql: ${TABLE}.gender ;;
40 | }
41 |
42 | dimension: gender_short {
43 | sql: LOWER(LEFT(${gender},1)) ;;
44 | }
45 |
46 | dimension: user_image {
47 | sql: ${image_file} ;;
48 | html: ;;
49 | }
50 |
51 | dimension: email {
52 | sql: ${TABLE}.email ;;
53 | tags: ["email"]
54 |
55 | link: {
56 | label: "User Lookup Dashboard"
57 | url: "http://demo.looker.com/dashboards/160?Email={{ value | encode_uri }}"
58 | icon_url: "http://www.looker.com/favicon.ico"
59 | }
60 | action: {
61 | label: "Email Promotion to Customer"
62 | url: "https://desolate-refuge-53336.herokuapp.com/posts"
63 | icon_url: "https://sendgrid.com/favicon.ico"
64 | param: {
65 | name: "some_auth_code"
66 | value: "abc123456"
67 | }
68 | form_param: {
69 | name: "Subject"
70 | required: yes
71 | default: "Thank you {{ users.name._value }}"
72 | }
73 | form_param: {
74 | name: "Body"
75 | type: textarea
76 | required: yes
77 | default:
78 | "Dear {{ users.first_name._value }},
79 |
80 | Thanks for your loyalty to the Look. We'd like to offer you a 10% discount
81 | on your next purchase! Just use the code LOYAL when checking out!
82 |
83 | Your friends at the Look"
84 | }
85 | }
86 | required_fields: [name, first_name]
87 | }
88 |
89 | dimension: image_file {
90 | hidden: yes
91 | sql: ('https://docs.looker.com/assets/images/'||${gender_short}||'.jpg') ;;
92 | }
93 |
94 | ## Demographics ##
95 |
96 | dimension: city {
97 | sql: ${TABLE}.city ;;
98 | drill_fields: [zip]
99 | }
100 |
101 | dimension: state {
102 | sql: ${TABLE}.state ;;
103 | map_layer_name: us_states
104 | drill_fields: [zip, city]
105 | }
106 |
107 | dimension: zip {
108 | type: zipcode
109 | sql: ${TABLE}.zip ;;
110 | }
111 |
112 | dimension: uk_postcode {
113 | label: "UK Postcode"
114 | sql: CASE WHEN ${TABLE}.country = 'UK' THEN TRANSLATE(LEFT(${zip},2),'0123456789','') END ;;
115 | map_layer_name: uk_postcode_areas
116 | drill_fields: [city, zip]
117 | }
118 |
119 | dimension: country {
120 | map_layer_name: countries
121 | drill_fields: [state, city]
122 | sql: CASE WHEN ${TABLE}.country = 'UK' THEN 'United Kingdom'
123 | ELSE ${TABLE}.country
124 | END
125 | ;;
126 | }
127 |
128 | dimension: location {
129 | type: location
130 | sql_latitude: ${TABLE}.latitude ;;
131 | sql_longitude: ${TABLE}.longitude ;;
132 | }
133 |
134 | dimension: approx_location {
135 | type: location
136 | drill_fields: [location]
137 | sql_latitude: round(${TABLE}.latitude,1) ;;
138 | sql_longitude: round(${TABLE}.longitude,1) ;;
139 | }
140 |
141 | ## Other User Information ##
142 |
143 | dimension_group: created {
144 | type: time
145 | # timeframes: [time, date, week, month, raw]
146 | sql: ${TABLE}.created_at ;;
147 | }
148 |
149 | dimension: history {
150 | sql: ${TABLE}.id ;;
151 | html: Order History
152 | ;;
153 | }
154 |
155 | dimension: traffic_source {
156 | sql: ${TABLE}.traffic_source ;;
157 | }
158 |
159 | dimension: ssn {
160 | # dummy field used in next dim
161 | hidden: yes
162 | type: number
163 | sql: lpad(cast(round(random() * 10000, 0) as char(4)), 4, '0') ;;
164 | }
165 |
166 | dimension: ssn_last_4 {
167 | label: "SSN Last 4"
168 | description: "Only users with sufficient permissions will see this data"
169 | type: string
170 | sql:
171 | CASE WHEN '{{_user_attributes["can_see_sensitive_data"]}}' = 'yes'
172 | THEN ${ssn}
173 | ELSE MD5(${ssn}||'salt')
174 | END;;
175 | html:
176 | {% if _user_attributes["can_see_sensitive_data"] == 'yes' %}
177 | {{ value }}
178 | {% else %}
179 | ####
180 | {% endif %} ;;
181 | }
182 |
183 | ## MEASURES ##
184 |
185 | measure: count {
186 | type: count
187 | drill_fields: [detail*]
188 | }
189 |
190 | measure: count_percent_of_total {
191 | label: "Count (Percent of Total)"
192 | type: percent_of_total
193 | sql: ${count} ;;
194 | drill_fields: [detail*]
195 | }
196 |
197 | measure: average_age {
198 | type: average
199 | value_format_name: decimal_2
200 | sql: ${age} ;;
201 | drill_fields: [detail*]
202 | }
203 |
204 | set: detail {
205 | fields: [id, name, email, age, created_date, orders.count, order_items.count]
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/test_files/03_inventory_items.view.lkml:
--------------------------------------------------------------------------------
1 | view: inventory_items {
2 | sql_table_name: inventory_items ;;
3 | ## DIMENSIONS ##
4 |
5 | dimension: id {
6 | primary_key: yes
7 | type: number
8 | sql: ${TABLE}.id ;;
9 | }
10 |
11 | dimension: cost {
12 | type: number
13 | value_format_name: usd
14 | sql: ${TABLE}.cost ;;
15 | }
16 |
17 | dimension_group: created {
18 | type: time
19 | timeframes: [time, date, week, month, raw]
20 | sql: ${TABLE}.created_at ;;
21 | }
22 |
23 | dimension: product_id {
24 | type: number
25 | hidden: yes
26 | sql: ${TABLE}.product_id ;;
27 | }
28 |
29 | dimension_group: sold {
30 | type: time
31 | timeframes: [time, date, week, month, raw]
32 | sql: ${TABLE}.sold_at ;;
33 | }
34 |
35 | dimension: is_sold {
36 | type: yesno
37 | sql: ${sold_raw} is not null ;;
38 | }
39 |
40 | dimension: days_in_inventory {
41 | description: "days between created and sold date"
42 | type: number
43 | sql: DATEDIFF('day', ${created_raw}, coalesce(${sold_raw},CURRENT_DATE)) ;;
44 | }
45 |
46 | dimension: days_in_inventory_tier {
47 | type: tier
48 | sql: ${days_in_inventory} ;;
49 | style: integer
50 | tiers: [0, 5, 10, 20, 40, 80, 160, 360]
51 | }
52 |
53 | dimension: days_since_arrival {
54 | description: "days since created - useful when filtering on sold yesno for items still in inventory"
55 | type: number
56 | sql: DATEDIFF('day', ${created_date}, GETDATE()) ;;
57 | }
58 |
59 | dimension: days_since_arrival_tier {
60 | type: tier
61 | sql: ${days_since_arrival} ;;
62 | style: integer
63 | tiers: [0, 5, 10, 20, 40, 80, 160, 360]
64 | }
65 |
66 | dimension: product_distribution_center_id {
67 | hidden: yes
68 | sql: ${TABLE}.product_distribution_center_id ;;
69 | }
70 |
71 | ## MEASURES ##
72 |
73 | measure: sold_count {
74 | type: count
75 | drill_fields: [detail*]
76 |
77 | filters: {
78 | field: is_sold
79 | value: "Yes"
80 | }
81 | }
82 |
83 | measure: sold_percent {
84 | type: number
85 | value_format_name: percent_2
86 | sql: 1.0 * ${sold_count}/NULLIF(${count},0) ;;
87 | }
88 |
89 | measure: total_cost {
90 | type: sum
91 | value_format_name: usd
92 | sql: ${cost} ;;
93 | }
94 |
95 | measure: average_cost {
96 | type: average
97 | value_format_name: usd
98 | sql: ${cost} ;;
99 | }
100 |
101 | measure: count {
102 | type: count
103 | drill_fields: [detail*]
104 | }
105 |
106 | measure: number_on_hand {
107 | type: count
108 | drill_fields: [detail*]
109 |
110 | filters: {
111 | field: is_sold
112 | value: "No"
113 | }
114 | }
115 |
116 | measure: stock_coverage_ratio {
117 | type: number
118 | description: "Stock on Hand vs Trailing 28d Sales Ratio"
119 | sql: 1.0 * ${number_on_hand} / nullif(${order_items.count_last_28d},0) ;;
120 | value_format_name: decimal_2
121 | html:
{{ rendered_value }}
;; 122 | } 123 | 124 | set: detail { 125 | fields: [id, products.item_name, products.category, products.brand, products.department, cost, created_time, sold_time] 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /test_files/04_products.view.lkml: -------------------------------------------------------------------------------- 1 | view: products { 2 | sql_table_name: products ;; 3 | 4 | dimension: id { 5 | primary_key: yes 6 | type: number 7 | sql: ${TABLE}.id ;; 8 | } 9 | 10 | dimension: category { 11 | sql: TRIM(${TABLE}.category) ;; 12 | drill_fields: [item_name] 13 | } 14 | 15 | dimension: item_name { 16 | sql: TRIM(${TABLE}.name) ;; 17 | } 18 | 19 | dimension: brand { 20 | sql: TRIM(${TABLE}.brand) ;; 21 | 22 | link: { 23 | label: "Website" 24 | url: "http://www.google.com/search?q={{ value | encode_uri }}+clothes&btnI" 25 | icon_url: "http://www.google.com/s2/favicons?domain=www.{{ value | encode_uri }}.com" 26 | } 27 | 28 | link: { 29 | label: "Facebook" 30 | url: "http://www.google.com/search?q=site:facebook.com+{{ value | encode_uri }}+clothes&btnI" 31 | icon_url: "https://static.xx.fbcdn.net/rsrc.php/yl/r/H3nktOa7ZMg.ico" 32 | } 33 | 34 | link: { 35 | label: "{{value}} Analytics Dashboard" 36 | url: "/dashboards/8?Brand%20Name={{ value | encode_uri }}" 37 | icon_url: "http://www.looker.com/favicon.ico" 38 | } 39 | 40 | drill_fields: [category, item_name] 41 | } 42 | 43 | dimension: retail_price { 44 | type: number 45 | sql: ${TABLE}.retail_price ;; 46 | } 47 | 48 | dimension: department { 49 | sql: TRIM(${TABLE}.department) ;; 50 | } 51 | 52 | dimension: sku { 53 | sql: ${TABLE}.sku ;; 54 | } 55 | 56 | dimension: distribution_center_id { 57 | type: number 58 | sql: ${TABLE}.distribution_center_id ;; 59 | } 60 | 61 | ## MEASURES ## 62 | 63 | measure: count { 64 | type: count 65 | drill_fields: [detail*] 66 | } 67 | 68 | measure: brand_count { 69 | type: count_distinct 70 | sql: ${brand} ;; 71 | drill_fields: [brand, detail2*, -brand_count] # show the brand, a bunch of counts (see the set below), don't show the brand count, because it will always be 1 72 | } 73 | 74 | measure: category_count { 75 | alias: [category.count] 76 | type: count_distinct 77 | sql: ${category} ;; 78 | drill_fields: [category, detail2*, -category_count] # don't show because it will always be 1 79 | } 80 | 81 | measure: department_count { 82 | alias: [department.count] 83 | type: count_distinct 84 | sql: ${department} ;; 85 | drill_fields: [department, detail2*, -department_count] # don't show because it will always be 1 86 | } 87 | 88 | set: detail { 89 | fields: [id, item_name, brand, category, department, retail_price, customers.count, orders.count, order_items.count, inventory_items.count] 90 | } 91 | 92 | set: detail2 { 93 | fields: [category_count, brand_count, department_count, count, customers.count, orders.count, order_items.count, inventory_items.count, products.count] 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test_files/05_distribution_centers.view.lkml: -------------------------------------------------------------------------------- 1 | view: distribution_centers { 2 | dimension: location { 3 | type: location 4 | sql_latitude: ${TABLE}.latitude ;; 5 | sql_longitude: ${TABLE}.longitude ;; 6 | } 7 | 8 | dimension: id { 9 | type: number 10 | primary_key: yes 11 | sql: ${TABLE}.id ;; 12 | } 13 | 14 | dimension: name { 15 | sql: ${TABLE}.name ;; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test_files/11_order_facts.view.lkml: -------------------------------------------------------------------------------- 1 | include: "thelook.model.lkml" 2 | view: order_facts { 3 | derived_table: { 4 | explore_source: order_items { 5 | column: order_id {} 6 | column: items_in_order { field: order_items.count } 7 | column: order_amount { field: order_items.total_sale_price } 8 | column: order_cost { field: inventory_items.total_cost } 9 | column: user_id {field: order_items.user_id } 10 | column: created_at {field: order_items.created_raw} 11 | column: order_gross_margin {field: order_items.total_gross_margin} 12 | derived_column: order_sequence_number { 13 | sql: RANK() OVER (PARTITION BY user_id ORDER BY created_at) ;; 14 | } 15 | } 16 | sortkeys: ["order_id"] 17 | distribution: "order_id" 18 | datagroup_trigger: ecommerce_etl 19 | } 20 | dimension: order_id { 21 | type: number 22 | hidden: yes 23 | primary_key: yes 24 | sql: ${TABLE}.order_id ;; 25 | } 26 | 27 | dimension: items_in_order { 28 | type: number 29 | sql: ${TABLE}.items_in_order ;; 30 | } 31 | 32 | dimension: order_amount { 33 | type: number 34 | value_format_name: usd 35 | sql: ${TABLE}.order_amount ;; 36 | } 37 | 38 | dimension: order_cost { 39 | type: number 40 | value_format_name: usd 41 | sql: ${TABLE}.order_cost ;; 42 | } 43 | 44 | dimension: order_gross_margin { 45 | type: number 46 | value_format_name: usd 47 | } 48 | 49 | 50 | dimension: order_sequence_number { 51 | type: number 52 | sql: ${TABLE}.order_sequence_number ;; 53 | } 54 | 55 | dimension: is_first_purchase { 56 | type: yesno 57 | sql: ${order_sequence_number} = 1 ;; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test_files/12_user_order_facts.view.lkml: -------------------------------------------------------------------------------- 1 | view: user_order_facts { 2 | derived_table: { 3 | sql: SELECT 4 | user_id 5 | , COUNT(DISTINCT order_id) AS lifetime_orders 6 | , SUM(sale_price) AS lifetime_revenue 7 | , MIN(NULLIF(created_at,0)) AS first_order 8 | , MAX(NULLIF(created_at,0)) AS latest_order 9 | , COUNT(DISTINCT DATE_TRUNC('month', NULLIF(created_at,0))) AS number_of_distinct_months_with_orders 10 | FROM order_items 11 | GROUP BY user_id 12 | ;; 13 | sortkeys: ["user_id"] 14 | distribution: "user_id" 15 | datagroup_trigger: ecommerce_etl 16 | } 17 | 18 | dimension: user_id { 19 | primary_key: yes 20 | hidden: yes 21 | sql: ${TABLE}.user_id ;; 22 | } 23 | 24 | ##### Time and Cohort Fields ###### 25 | 26 | dimension_group: first_order { 27 | type: time 28 | timeframes: [date, week, month, year] 29 | sql: ${TABLE}.first_order ;; 30 | } 31 | 32 | dimension_group: latest_order { 33 | type: time 34 | timeframes: [date, week, month, year] 35 | sql: ${TABLE}.latest_order ;; 36 | } 37 | 38 | dimension: days_as_customer { 39 | description: "Days between first and latest order" 40 | type: number 41 | sql: DATEDIFF('day', ${TABLE}.first_order, ${TABLE}.latest_order)+1 ;; 42 | } 43 | 44 | dimension: days_as_customer_tiered { 45 | type: tier 46 | tiers: [0, 1, 7, 14, 21, 28, 30, 60, 90, 120] 47 | sql: ${days_as_customer} ;; 48 | style: integer 49 | } 50 | 51 | ##### Lifetime Behavior - Order Counts ###### 52 | 53 | dimension: lifetime_orders { 54 | type: number 55 | sql: ${TABLE}.lifetime_orders ;; 56 | } 57 | 58 | dimension: repeat_customer { 59 | description: "Lifetime Count of Orders > 1" 60 | type: yesno 61 | sql: ${lifetime_orders} > 1 ;; 62 | } 63 | 64 | dimension: lifetime_orders_tier { 65 | type: tier 66 | tiers: [0, 1, 2, 3, 5, 10] 67 | sql: ${lifetime_orders} ;; 68 | style: integer 69 | } 70 | 71 | measure: average_lifetime_orders { 72 | type: average 73 | value_format_name: decimal_2 74 | sql: ${lifetime_orders} ;; 75 | } 76 | 77 | dimension: distinct_months_with_orders { 78 | type: number 79 | sql: ${TABLE}.number_of_distinct_months_with_orders ;; 80 | } 81 | 82 | ##### Lifetime Behavior - Revenue ###### 83 | 84 | dimension: lifetime_revenue { 85 | type: number 86 | value_format_name: usd 87 | sql: ${TABLE}.lifetime_revenue ;; 88 | } 89 | 90 | dimension: lifetime_revenue_tier { 91 | type: tier 92 | tiers: [0, 25, 50, 100, 200, 500, 1000] 93 | sql: ${lifetime_revenue} ;; 94 | style: integer 95 | } 96 | 97 | measure: average_lifetime_revenue { 98 | type: average 99 | value_format_name: usd 100 | sql: ${lifetime_revenue} ;; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test_files/13_repeat_purchase_facts.view.lkml: -------------------------------------------------------------------------------- 1 | view: repeat_purchase_facts { 2 | derived_table: { 3 | sql: SELECT 4 | order_items.order_id 5 | , COUNT(DISTINCT repeat_order_items.id) AS number_subsequent_orders 6 | , MIN(repeat_order_items.created_at) AS next_order_date 7 | , MIN(repeat_order_items.order_id) AS next_order_id 8 | FROM order_items 9 | LEFT JOIN order_items repeat_order_items 10 | ON order_items.user_id = repeat_order_items.user_id 11 | AND order_items.created_at < repeat_order_items.created_at 12 | GROUP BY 1 13 | ;; 14 | sortkeys: ["order_id"] 15 | distribution: "order_id" 16 | datagroup_trigger: ecommerce_etl 17 | } 18 | 19 | dimension: order_id { 20 | type: number 21 | hidden: yes 22 | primary_key: yes 23 | sql: ${TABLE}.order_id ;; 24 | } 25 | 26 | dimension: next_order_id { 27 | type: number 28 | hidden: yes 29 | sql: ${TABLE}.next_order_id ;; 30 | } 31 | 32 | dimension: has_subsequent_order { 33 | type: yesno 34 | sql: ${next_order_id} > 0 ;; 35 | } 36 | 37 | dimension: number_subsequent_orders { 38 | type: number 39 | sql: ${TABLE}.number_subsequent_orders ;; 40 | } 41 | 42 | dimension_group: next_order { 43 | type: time 44 | timeframes: [raw, date] 45 | hidden: yes 46 | sql: ${TABLE}.next_order_date ;; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test_files/22_affinity.view.lkml: -------------------------------------------------------------------------------- 1 | view: affinity { 2 | derived_table: { 3 | datagroup_trigger: ecommerce_etl 4 | distribution: "product_a_id" 5 | sortkeys: ["product_a_id", "product_b_id"] 6 | sql: SELECT 7 | product_a_id 8 | , product_b_id 9 | , joint_user_freq 10 | , joint_order_freq 11 | , top1.prod_freq AS product_a_freq 12 | , top2.prod_freq AS product_b_freq 13 | 14 | FROM 15 | ( 16 | SELECT 17 | up1.prod_id AS product_a_id 18 | , up2.prod_id AS product_b_id 19 | , COUNT(*) AS joint_user_freq 20 | FROM ${user_order_product.SQL_TABLE_NAME} AS up1 21 | LEFT JOIN ${user_order_product.SQL_TABLE_NAME} AS up2 22 | ON up1.user_id = up2.user_id 23 | AND up1.prod_id <> up2.prod_id 24 | GROUP BY product_a_id, product_b_id 25 | ) AS juf 26 | 27 | LEFT JOIN 28 | ( 29 | SELECT 30 | op1.prod_id AS oproduct_a_id 31 | , op2.prod_id AS oproduct_b_id 32 | , COUNT(*) AS joint_order_freq 33 | FROM ${user_order_product.SQL_TABLE_NAME} op1 34 | LEFT JOIN ${user_order_product.SQL_TABLE_NAME} op2 35 | ON op1.order_id = op2.order_id 36 | AND op1.prod_id <> op2.prod_id 37 | GROUP BY oproduct_a_id, oproduct_b_id 38 | ) AS jof 39 | 40 | ON jof.oproduct_a_id = juf.product_a_id 41 | AND jof.oproduct_b_id = juf.product_b_id 42 | LEFT JOIN ${total_order_product.SQL_TABLE_NAME} top1 43 | ON top1.prod_id = juf.product_a_id 44 | LEFT JOIN ${total_order_product.SQL_TABLE_NAME} top2 45 | ON top2.prod_id = juf.product_b_id 46 | ;; 47 | } 48 | 49 | measure: count { 50 | type: count 51 | drill_fields: [detail*] 52 | } 53 | 54 | dimension: product_a_id { 55 | sql: ${TABLE}.product_a_id ;; 56 | } 57 | 58 | dimension: product_b_id { 59 | sql: ${TABLE}.product_b_id ;; 60 | } 61 | 62 | dimension: joint_user_freq { 63 | description: "The number of users who have purchased both product a and product b" 64 | type: number 65 | sql: ${TABLE}.joint_user_freq ;; 66 | } 67 | 68 | dimension: joint_order_freq { 69 | description: "The number of orders that include both product a and product b" 70 | type: number 71 | sql: ${TABLE}.joint_order_freq ;; 72 | } 73 | 74 | dimension: product_a_freq { 75 | description: "The total number of times product a has been purchased" 76 | type: number 77 | sql: ${TABLE}.product_a_freq ;; 78 | } 79 | 80 | dimension: product_b_freq { 81 | description: "The total number of times product b has been purchased" 82 | type: number 83 | sql: ${TABLE}.product_b_freq ;; 84 | } 85 | 86 | dimension: user_affinity { 87 | hidden: yes 88 | type: number 89 | sql: 1.0*${joint_user_freq}/NULLIF((${product_a_freq}+${product_b_freq})-(${joint_user_freq}),0) ;; 90 | value_format_name: percent_2 91 | } 92 | 93 | dimension: order_affinity { 94 | hidden: yes 95 | type: number 96 | sql: 1.0*${joint_order_freq}/NULLIF((${product_a_freq}+${product_b_freq})-(${joint_order_freq}),0) ;; 97 | value_format_name: percent_2 98 | } 99 | 100 | measure: avg_user_affinity { 101 | label: "Affinity Score (by User History)" 102 | description: "Percentage of users that bought both products weighted by how many times each product sold individually" 103 | type: average 104 | sql: 100.0 * ${user_affinity} ;; 105 | value_format_name: decimal_2 106 | } 107 | 108 | measure: avg_order_affinity { 109 | label: "Affinity Score (by Order Basket)" 110 | description: "Percentage of orders that contained both products weighted by how many times each product sold individually" 111 | type: average 112 | sql: 100.0 * ${order_affinity} ;; 113 | value_format_name: decimal_2 114 | } 115 | 116 | measure: combined_affinity { 117 | type: number 118 | sql: ${avg_user_affinity} + ${avg_order_affinity} ;; 119 | } 120 | 121 | set: detail { 122 | fields: [product_a_id,product_b_id,user_affinity,order_affinity] 123 | } 124 | } 125 | 126 | 127 | 128 | ############################################# 129 | #Table that aggregates the products purchased by user and order id 130 | view: user_order_product { 131 | derived_table: { 132 | datagroup_trigger: ecommerce_etl 133 | distribution: "prod_id" 134 | sortkeys: ["prod_id", "user_id", "order_id"] 135 | sql: SELECT 136 | oi.user_id AS user_id 137 | , p.id AS prod_id 138 | , oi.order_id AS order_id 139 | FROM order_items oi 140 | LEFT JOIN inventory_items ii 141 | ON oi.inventory_item_id = ii.id 142 | LEFT JOIN products p 143 | ON ii.product_id = p.id 144 | GROUP BY 1,2,3 145 | ;; 146 | } 147 | 148 | # measure: count { 149 | # type: count 150 | # drill_fields: [detail*] 151 | # } 152 | 153 | dimension: user_id { 154 | type: number 155 | sql: ${TABLE}.user_id ;; 156 | } 157 | 158 | dimension: prod_id { 159 | type: number 160 | sql: ${TABLE}.prod_id ;; 161 | } 162 | 163 | dimension: order_id { 164 | type: number 165 | sql: ${TABLE}.order_id ;; 166 | } 167 | } 168 | 169 | ################################################# 170 | #Table to count the total times a product id has been purchased 171 | view: total_order_product { 172 | derived_table: { 173 | datagroup_trigger: ecommerce_etl 174 | distribution: "prod_id" 175 | sortkeys: ["prod_id"] 176 | sql: SELECT 177 | p.id AS prod_id 178 | , COUNT(*) AS prod_freq 179 | FROM order_items oi 180 | LEFT JOIN inventory_items 181 | ON oi.inventory_item_id = inventory_items.id 182 | LEFT JOIN products p 183 | ON inventory_items.product_id = p.id 184 | GROUP BY p.id 185 | ;; 186 | } 187 | 188 | # measure: count { 189 | # type: count 190 | # drill_fields: [detail*] 191 | # } 192 | 193 | dimension: prod_id { 194 | sql: ${TABLE}.prod_id ;; 195 | } 196 | 197 | dimension: prod_freq { 198 | type: number 199 | sql: ${TABLE}.prod_freq ;; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /test_files/23_share_of_wallet.view.lkml: -------------------------------------------------------------------------------- 1 | view: order_items_share_of_wallet { 2 | view_label: "Share of Wallet" 3 | # 4 | # - measure: total_sale_price 5 | # type: sum 6 | # value_format: '$#,###' 7 | # sql: ${sale_price} 8 | # 9 | 10 | 11 | ########## Comparison for Share of Wallet ########## 12 | 13 | filter: item_name { 14 | view_label: "Share of Wallet (Item Level)" 15 | suggest_dimension: products.item_name 16 | suggest_explore: orders_with_share_of_wallet_application 17 | } 18 | 19 | filter: brand { 20 | view_label: "Share of Wallet (Brand Level)" 21 | suggest_dimension: products.brand 22 | suggest_explore: orders_with_share_of_wallet_application 23 | } 24 | 25 | dimension: primary_key { 26 | sql: ${order_items.id} ;; 27 | primary_key: yes 28 | hidden: yes 29 | } 30 | 31 | dimension: item_comparison { 32 | view_label: "Share of Wallet (Item Level)" 33 | description: "Compare a selected item vs. other items in the brand vs. all other brands" 34 | sql: CASE 35 | WHEN {% condition item_name %} trim(products.item_name) {% endcondition %} 36 | THEN '(1) '||${products.item_name} 37 | WHEN {% condition brand %} trim(products.brand) {% endcondition %} 38 | THEN '(2) Rest of '||${products.brand} 39 | ELSE '(3) Rest of Population' 40 | END 41 | ;; 42 | } 43 | 44 | dimension: brand_comparison { 45 | view_label: "Share of Wallet (Brand Level)" 46 | description: "Compare a selected brand vs. all other brands" 47 | sql: CASE 48 | WHEN {% condition brand %} trim(products.brand) {% endcondition %} 49 | THEN '(1) '||${products.brand} 50 | ELSE '(2) Rest of Population' 51 | END 52 | ;; 53 | } 54 | 55 | measure: total_sale_price_this_item { 56 | view_label: "Share of Wallet (Item Level)" 57 | type: sum 58 | hidden: yes 59 | sql: ${order_items.sale_price} ;; 60 | value_format_name: usd 61 | 62 | filters: { 63 | field: order_items_share_of_wallet.item_comparison 64 | value: "(1)%" 65 | } 66 | } 67 | 68 | measure: total_sale_price_this_brand { 69 | view_label: "Share of Wallet (Item Level)" 70 | type: sum 71 | hidden: yes 72 | value_format_name: usd 73 | sql: ${order_items.sale_price} ;; 74 | 75 | filters: { 76 | field: order_items_share_of_wallet.item_comparison 77 | value: "(2)%,(1)%" 78 | } 79 | } 80 | 81 | measure: total_sale_price_brand_v2 { 82 | view_label: "Share of Wallet (Brand Level)" 83 | label: "Total Sales - This Brand" 84 | type: sum 85 | value_format_name: usd 86 | sql: ${order_items.sale_price} ;; 87 | 88 | filters: { 89 | field: order_items_share_of_wallet.brand_comparison 90 | value: "(1)%" 91 | } 92 | } 93 | 94 | measure: item_share_of_wallet_within_brand { 95 | view_label: "Share of Wallet (Item Level)" 96 | type: number 97 | description: "This item sales over all sales for same brand" 98 | # view_label: 'Share of Wallet' 99 | value_format_name: percent_2 100 | sql: ${total_sale_price_this_item}*1.0 / nullif(${total_sale_price_this_brand},0) ;; 101 | } 102 | 103 | measure: item_share_of_wallet_within_company { 104 | view_label: "Share of Wallet (Item Level)" 105 | description: "This item sales over all sales across website" 106 | value_format_name: percent_2 107 | # view_label: 'Share of Wallet' 108 | type: number 109 | sql: ${total_sale_price_this_item}*1.0 / nullif(${order_items.total_sale_price},0) ;; 110 | } 111 | 112 | measure: brand_share_of_wallet_within_company { 113 | view_label: "Share of Wallet (Brand Level)" 114 | description: "This brand's sales over all sales across website" 115 | value_format_name: percent_2 116 | # view_label: 'Share of Wallet' 117 | type: number 118 | sql: ${total_sale_price_brand_v2}*1.0 / nullif(${order_items.total_sale_price},0) ;; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /test_files/24_inventory_snapshot.view.lkml: -------------------------------------------------------------------------------- 1 | view: inventory_snapshot { 2 | derived_table: { 3 | datagroup_trigger: ecommerce_etl 4 | sortkeys: ["snapshot_date"] 5 | distribution: "product_id" 6 | sql: with calendar as 7 | (select distinct date(created_at) as snapshot_date 8 | from inventory_items 9 | -- where dateadd('day',90,created_at)>=current_date 10 | ) 11 | 12 | select 13 | 14 | inventory_items.product_id 15 | ,calendar.snapshot_date 16 | ,count(*) as number_in_stock 17 | 18 | from inventory_items 19 | left join calendar 20 | on inventory_items.created_at <= calendar.snapshot_date 21 | and (inventory_items.sold_at >= calendar.snapshot_date OR inventory_items.sold_at is null) 22 | -- where dateadd('day',90,calendar.snapshot_date)>=current_date 23 | group by 1,2 24 | 25 | ;; 26 | } 27 | 28 | 29 | 30 | dimension: product_id { 31 | type: number 32 | sql: ${TABLE}.product_id ;; 33 | } 34 | 35 | dimension: snapshot_date { 36 | type: date 37 | sql: ${TABLE}.snapshot_date ;; 38 | } 39 | 40 | dimension: number_in_stock { 41 | type: number 42 | hidden: yes 43 | sql: ${TABLE}.number_in_stock ;; 44 | } 45 | 46 | measure: total_in_stock { 47 | type: sum 48 | sql: ${number_in_stock} ;; 49 | } 50 | 51 | measure: stock_coverage_ratio { 52 | type: number 53 | sql: 1.0 * ${total_in_stock} / nullif(${trailing_sales_snapshot.sum_trailing_28d_sales},0) ;; 54 | value_format_name: decimal_2 55 | } 56 | 57 | measure: sum_stock_yesterday { 58 | type: sum 59 | hidden: yes 60 | sql: ${number_in_stock} ;; 61 | filters: { 62 | field: snapshot_date 63 | value: "yesterday" 64 | } 65 | } 66 | 67 | 68 | measure: sum_stock_last_wk { 69 | type: sum 70 | hidden: yes 71 | sql: ${number_in_stock} ;; 72 | filters: { 73 | field: snapshot_date 74 | value: "8 days ago for 1 day" 75 | } 76 | } 77 | 78 | measure: stock_coverage_ratio_yday { 79 | type: number 80 | view_label: "Stock Ratio Changes" 81 | sql: 1.0 * ${sum_stock_yesterday} / nullif(${trailing_sales_snapshot.sum_trailing_28d_sales_yesterday},0) ;; 82 | value_format_name: decimal_2 83 | } 84 | 85 | 86 | measure: stock_coverage_ratio_last_wk { 87 | type: number 88 | view_label: "Stock Ratio Changes" 89 | sql: 1.0 * ${sum_stock_last_wk} / nullif(${trailing_sales_snapshot.sum_trailing_28d_sales_last_wk},0) ;; 90 | value_format_name: decimal_2 91 | } 92 | 93 | measure: wk_to_wk_change_coverage { 94 | label: "WoW Change - Coverage Ratio" 95 | view_label: "Stock Ratio Changes" 96 | sql: round(100*(${stock_coverage_ratio_yday}-${stock_coverage_ratio_last_wk}),1) ;; 97 | value_format_name: decimal_1 98 | # value_format: "# 'bp'" 99 | } 100 | 101 | 102 | set: detail { 103 | fields: [product_id, snapshot_date, number_in_stock] 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test_files/25_trailing_sales_snapshot.view.lkml: -------------------------------------------------------------------------------- 1 | view: trailing_sales_snapshot { 2 | derived_table: { 3 | datagroup_trigger: ecommerce_etl 4 | sortkeys: ["product_id"] 5 | distribution: "product_id" 6 | sql: with calendar as 7 | (select distinct date(created_at) as snapshot_date 8 | from inventory_items 9 | -- where dateadd('day',90,created_at)>=current_date 10 | ) 11 | 12 | select 13 | 14 | 15 | inventory_items.product_id 16 | ,date(order_items.created_at) as snapshot_date 17 | ,count(*) as trailing_28d_sales 18 | 19 | from order_items 20 | left join inventory_items on order_items.inventory_item_id = inventory_items.id 21 | left join calendar 22 | on order_items.created_at <= dateadd('day',28,calendar.snapshot_date) 23 | and order_items.created_at >= calendar.snapshot_date 24 | -- where dateadd('day',90,calendar.snapshot_date)>=current_date 25 | group by 1,2 26 | 27 | ;; 28 | } 29 | 30 | # measure: count { 31 | # type: count 32 | # drill_fields: [detail*] 33 | # } 34 | 35 | dimension: product_id { 36 | type: number 37 | sql: ${TABLE}.product_id ;; 38 | } 39 | 40 | dimension: snapshot_date { 41 | type: date 42 | sql: ${TABLE}.snapshot_date ;; 43 | } 44 | 45 | dimension: trailing_28d_sales { 46 | type: number 47 | hidden: yes 48 | sql: ${TABLE}.trailing_28d_sales ;; 49 | } 50 | 51 | measure: sum_trailing_28d_sales { 52 | type: sum 53 | sql: ${trailing_28d_sales} ;; 54 | } 55 | 56 | measure: sum_trailing_28d_sales_yesterday { 57 | type: sum 58 | hidden: yes 59 | sql: ${trailing_28d_sales} ;; 60 | filters: { 61 | field: snapshot_date 62 | value: "yesterday" 63 | } 64 | } 65 | 66 | 67 | measure: sum_trailing_28d_sales_last_wk { 68 | type: sum 69 | hidden: yes 70 | sql: ${trailing_28d_sales} ;; 71 | filters: { 72 | field: snapshot_date 73 | value: "8 days ago for 1 day" 74 | } 75 | } 76 | 77 | set: detail { 78 | fields: [product_id, snapshot_date, trailing_28d_sales] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test_files/51_events.view.lkml: -------------------------------------------------------------------------------- 1 | view: events { 2 | sql_table_name: events ;; 3 | 4 | dimension: event_id { 5 | type: number 6 | primary_key: yes 7 | sql: ${TABLE}.id ;; 8 | } 9 | 10 | dimension: session_id { 11 | type: number 12 | hidden: yes 13 | sql: ${TABLE}.session_id ;; 14 | } 15 | 16 | dimension: ip { 17 | label: "IP Address" 18 | view_label: "Visitors" 19 | sql: ${TABLE}.ip_address ;; 20 | } 21 | 22 | dimension: user_id { 23 | sql: ${TABLE}.user_id ;; 24 | } 25 | 26 | dimension_group: event { 27 | type: time 28 | # timeframes: [time, date, hour, time_of_day, hour_of_day, week, day_of_week_index, day_of_week] 29 | sql: ${TABLE}.created_at ;; 30 | } 31 | 32 | dimension: sequence_number { 33 | type: number 34 | description: "Within a given session, what order did the events take place in? 1=First, 2=Second, etc" 35 | sql: ${TABLE}.sequence_number ;; 36 | } 37 | 38 | dimension: is_entry_event { 39 | type: yesno 40 | description: "Yes indicates this was the entry point / landing page of the session" 41 | sql: ${sequence_number} = 1 ;; 42 | } 43 | 44 | dimension: is_exit_event { 45 | type: yesno 46 | label: "UTM Source" 47 | sql: ${sequence_number} = ${sessions.number_of_events_in_session} ;; 48 | description: "Yes indicates this was the exit point / bounce page of the session" 49 | } 50 | 51 | measure: count_bounces { 52 | type: count 53 | description: "Count of events where those events were the bounce page for the session" 54 | 55 | filters: { 56 | field: is_exit_event 57 | value: "Yes" 58 | } 59 | } 60 | 61 | measure: bounce_rate { 62 | type: number 63 | value_format_name: percent_2 64 | description: "Percent of events where those events were the bounce page for the session, out of all events" 65 | sql: ${count_bounces}*1.0 / nullif(${count}*1.0,0) ;; 66 | } 67 | 68 | dimension: full_page_url { 69 | sql: ${TABLE}.uri ;; 70 | } 71 | 72 | dimension: viewed_product_id { 73 | type: number 74 | sql: CASE 75 | WHEN ${event_type} = 'Product' THEN right(${full_page_url},len(${full_page_url})-9) 76 | END 77 | ;; 78 | } 79 | 80 | dimension: event_type { 81 | sql: ${TABLE}.event_type ;; 82 | } 83 | 84 | dimension: funnel_step { 85 | description: "Login -> Browse -> Add to Cart -> Checkout" 86 | sql: CASE 87 | WHEN ${event_type} IN ('Login', 'Home') THEN '(1) Land' 88 | WHEN ${event_type} IN ('Category', 'Brand') THEN '(2) Browse Inventory' 89 | WHEN ${event_type} = 'Product' THEN '(3) View Product' 90 | WHEN ${event_type} = 'Cart' THEN '(4) Add Item to Cart' 91 | WHEN ${event_type} = 'Purchase' THEN '(5) Purchase' 92 | END 93 | ;; 94 | } 95 | 96 | measure: unique_visitors { 97 | type: count_distinct 98 | description: "Uniqueness determined by IP Address and User Login" 99 | view_label: "Visitors" 100 | sql: ${ip} ;; 101 | drill_fields: [visitors*] 102 | } 103 | 104 | dimension: location { 105 | type: location 106 | view_label: "Visitors" 107 | sql_latitude: ${TABLE}.latitude ;; 108 | sql_longitude: ${TABLE}.longitude ;; 109 | } 110 | 111 | dimension: approx_location { 112 | type: location 113 | view_label: "Visitors" 114 | sql_latitude: round(${TABLE}.latitude,1) ;; 115 | sql_longitude: round(${TABLE}.longitude,1) ;; 116 | } 117 | 118 | dimension: has_user_id { 119 | type: yesno 120 | view_label: "Visitors" 121 | description: "Did the visitor sign in as a website user?" 122 | sql: ${users.id} > 0 ;; 123 | } 124 | 125 | dimension: browser { 126 | view_label: "Visitors" 127 | sql: ${TABLE}.browser ;; 128 | } 129 | 130 | dimension: os { 131 | label: "Operating System" 132 | view_label: "Visitors" 133 | sql: ${TABLE}.os ;; 134 | } 135 | 136 | measure: count { 137 | type: count 138 | drill_fields: [simple_page_info*] 139 | } 140 | 141 | measure: sessions_count { 142 | type: count_distinct 143 | sql: ${session_id} ;; 144 | } 145 | 146 | measure: count_m { 147 | label: "Count (MM)" 148 | type: number 149 | hidden: yes 150 | sql: ${count}/1000000.0 ;; 151 | drill_fields: [simple_page_info*] 152 | value_format: "#.### \"M\"" 153 | } 154 | 155 | measure: unique_visitors_m { 156 | label: "Unique Visitors (MM)" 157 | view_label: "Visitors" 158 | type: number 159 | sql: count (distinct ${ip}) / 1000000.0 ;; 160 | description: "Uniqueness determined by IP Address and User Login" 161 | value_format: "#.### \"M\"" 162 | hidden: yes 163 | drill_fields: [visitors*] 164 | } 165 | 166 | measure: unique_visitors_k { 167 | label: "Unique Visitors (k)" 168 | view_label: "Visitors" 169 | type: number 170 | hidden: yes 171 | description: "Uniqueness determined by IP Address and User Login" 172 | sql: count (distinct ${ip}) / 1000.0 ;; 173 | value_format: "#.### \"k\"" 174 | drill_fields: [visitors*] 175 | } 176 | 177 | set: simple_page_info { 178 | fields: [event_id, event_time, event_type, 179 | # - os 180 | # - browser 181 | full_page_url, user_id, funnel_step] 182 | } 183 | 184 | set: visitors { 185 | fields: [ip, os, browser, user_id, count] 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /test_files/52_sessions.view.lkml: -------------------------------------------------------------------------------- 1 | view: sessions { 2 | derived_table: { 3 | sortkeys: ["session_id"] 4 | distribution: "session_id" 5 | datagroup_trigger: ecommerce_etl 6 | sql: SELECT 7 | session_id 8 | , MIN(created_at) AS session_start 9 | , MAX(created_at) AS session_end 10 | , COUNT(*) AS number_of_events_in_session 11 | , SUM(CASE WHEN event_type IN ('Category','Brand') THEN 1 END) AS browse_events 12 | , SUM(CASE WHEN event_type = 'Product' THEN 1 END) AS product_events 13 | , SUM(CASE WHEN event_type = 'Cart' THEN 1 END) AS cart_events 14 | , SUM(CASE WHEN event_type = 'Purchase' THEN 1 end) AS purchase_events 15 | , MAX(user_id) AS session_user_id 16 | , MIN(id) AS landing_event_id 17 | , MAX(id) AS bounce_event_id 18 | FROM events 19 | GROUP BY session_id 20 | ;; 21 | } 22 | 23 | ##### Basic Web Info ######## 24 | 25 | measure: count { 26 | type: count 27 | drill_fields: [detail*] 28 | } 29 | 30 | dimension: session_id { 31 | type: string 32 | primary_key: yes 33 | sql: ${TABLE}.session_id ;; 34 | } 35 | 36 | dimension: session_user_id { 37 | sql: ${TABLE}.session_user_id ;; 38 | } 39 | 40 | dimension: landing_event_id { 41 | sql: ${TABLE}.landing_event_id ;; 42 | } 43 | 44 | dimension: bounce_event_id { 45 | sql: ${TABLE}.bounce_event_id ;; 46 | } 47 | 48 | dimension_group: session_start { 49 | type: time 50 | # timeframes: [time, date, week, month, hour_of_day, day_of_week] 51 | sql: ${TABLE}.session_start ;; 52 | } 53 | 54 | dimension_group: session_end { 55 | type: time 56 | timeframes: [raw, time, date, week, month] 57 | sql: ${TABLE}.session_end ;; 58 | } 59 | 60 | dimension: duration { 61 | label: "Duration (sec)" 62 | type: number 63 | sql: DATEDIFF('second', ${session_start_raw}, ${session_end_raw}) ;; 64 | } 65 | 66 | measure: average_duration { 67 | label: "Average Duration (sec)" 68 | type: average 69 | value_format_name: decimal_2 70 | sql: ${duration} ;; 71 | } 72 | 73 | dimension: duration_seconds_tier { 74 | label: "Duration Tier (sec)" 75 | type: tier 76 | tiers: [10, 30, 60, 120, 300] 77 | style: integer 78 | sql: ${duration} ;; 79 | } 80 | 81 | ##### Bounce Information ######## 82 | 83 | dimension: is_bounce_session { 84 | type: yesno 85 | sql: ${number_of_events_in_session} = 1 ;; 86 | } 87 | 88 | measure: count_bounce_sessions { 89 | type: count 90 | 91 | filters: { 92 | field: is_bounce_session 93 | value: "Yes" 94 | } 95 | 96 | drill_fields: [detail*] 97 | } 98 | 99 | measure: percent_bounce_sessions { 100 | type: number 101 | value_format_name: percent_2 102 | sql: 1.0 * ${count_bounce_sessions} / nullif(${count},0) ;; 103 | } 104 | 105 | ####### Session by event types included ######## 106 | 107 | dimension: number_of_browse_events_in_session { 108 | type: number 109 | hidden: yes 110 | sql: ${TABLE}.browse_events ;; 111 | } 112 | 113 | dimension: number_of_product_events_in_session { 114 | type: number 115 | hidden: yes 116 | sql: ${TABLE}.product_events ;; 117 | } 118 | 119 | dimension: number_of_cart_events_in_session { 120 | type: number 121 | hidden: yes 122 | sql: ${TABLE}.cart_events ;; 123 | } 124 | 125 | dimension: number_of_purchase_events_in_session { 126 | type: number 127 | hidden: yes 128 | sql: ${TABLE}.purchase_events ;; 129 | } 130 | 131 | dimension: includes_browse { 132 | type: yesno 133 | sql: ${number_of_browse_events_in_session} > 0 ;; 134 | } 135 | 136 | dimension: includes_product { 137 | type: yesno 138 | sql: ${number_of_product_events_in_session} > 0 ;; 139 | } 140 | 141 | dimension: includes_cart { 142 | type: yesno 143 | sql: ${number_of_cart_events_in_session} > 0 ;; 144 | } 145 | 146 | dimension: includes_purchase { 147 | type: yesno 148 | sql: ${number_of_purchase_events_in_session} > 0 ;; 149 | } 150 | 151 | measure: count_with_cart { 152 | type: count 153 | 154 | filters: { 155 | field: includes_cart 156 | value: "Yes" 157 | } 158 | 159 | drill_fields: [detail*] 160 | } 161 | 162 | measure: count_with_purchase { 163 | type: count 164 | 165 | filters: { 166 | field: includes_purchase 167 | value: "Yes" 168 | } 169 | 170 | drill_fields: [detail*] 171 | } 172 | 173 | dimension: number_of_events_in_session { 174 | type: number 175 | sql: ${TABLE}.number_of_events_in_session ;; 176 | } 177 | 178 | ####### Linear Funnel ######## 179 | 180 | dimension: furthest_funnel_step { 181 | sql: CASE 182 | WHEN ${number_of_purchase_events_in_session} > 0 THEN '(5) Purchase' 183 | WHEN ${number_of_cart_events_in_session} > 0 THEN '(4) Add to Cart' 184 | WHEN ${number_of_product_events_in_session} > 0 THEN '(3) View Product' 185 | WHEN ${number_of_browse_events_in_session} > 0 THEN '(2) Browse' 186 | ELSE '(1) Land' 187 | END 188 | ;; 189 | } 190 | 191 | measure: all_sessions { 192 | view_label: "Funnel View" 193 | label: "(1) All Sessions" 194 | type: count 195 | drill_fields: [detail*] 196 | } 197 | 198 | measure: count_browse_or_later { 199 | view_label: "Funnel View" 200 | label: "(2) Browse or later" 201 | type: count 202 | 203 | filters: { 204 | field: furthest_funnel_step 205 | value: "(2) Browse,(3) View Product,(4) Add to Cart,(5) Purchase 206 | " 207 | } 208 | 209 | drill_fields: [detail*] 210 | } 211 | 212 | measure: count_product_or_later { 213 | view_label: "Funnel View" 214 | label: "(3) View Product or later" 215 | type: count 216 | 217 | filters: { 218 | field: furthest_funnel_step 219 | value: "(3) View Product,(4) Add to Cart,(5) Purchase 220 | " 221 | } 222 | 223 | drill_fields: [detail*] 224 | } 225 | 226 | measure: count_cart_or_later { 227 | view_label: "Funnel View" 228 | label: "(4) Add to Cart or later" 229 | type: count 230 | 231 | filters: { 232 | field: furthest_funnel_step 233 | value: "(4) Add to Cart,(5) Purchase 234 | " 235 | } 236 | 237 | drill_fields: [detail*] 238 | } 239 | 240 | measure: count_purchase { 241 | view_label: "Funnel View" 242 | label: "(5) Purchase" 243 | type: count 244 | 245 | filters: { 246 | field: furthest_funnel_step 247 | value: "(5) Purchase 248 | " 249 | } 250 | 251 | drill_fields: [detail*] 252 | } 253 | 254 | measure: cart_to_checkout_conversion { 255 | view_label: "Funnel View" 256 | type: number 257 | value_format_name: percent_2 258 | sql: 1.0 * ${count_purchase} / nullif(${count_cart_or_later},0) ;; 259 | } 260 | 261 | measure: overall_conversion { 262 | view_label: "Funnel View" 263 | type: number 264 | value_format_name: percent_2 265 | sql: 1.0 * ${count_purchase} / nullif(${count},0) ;; 266 | } 267 | 268 | set: detail { 269 | fields: [session_id, session_start_time, session_end_time, number_of_events_in_session, duration, number_of_purchase_events_in_session, number_of_cart_events_in_session] 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /test_files/97_web_analytics_data_tool.view.lkml: -------------------------------------------------------------------------------- 1 | include: "*.view" # include all the views 2 | 3 | 4 | ######################################################################################## 5 | # Web Analytics Tool Explore 6 | ######################################################################################## 7 | explore: data_tool { 8 | hidden: yes 9 | label: "Web Analytics Data Tool" 10 | 11 | join: sessions { 12 | from: sessions_webanalytics 13 | fields: [sessions.session_id, sessions.session_start_date, sessions.is_bounce_session, sessions.count_purchase, session_end_date, sessions.duration, sessions.duration_seconds_tier, sessions.average_duration, sessions.count, sessions.count_bounce_sessions, sessions.percent_bounce_sessions, sessions.overall_conversion, sessions.furthest_funnel_step] 14 | sql_on: ${data_tool.session_id} = ${sessions.session_id} ;; 15 | relationship: many_to_one 16 | } 17 | 18 | join: users { 19 | sql_on: ${sessions.session_user_id} = ${users.id} ;; 20 | relationship: many_to_one 21 | } 22 | 23 | join: user_order_facts { 24 | sql_on: ${users.id} = ${user_order_facts.user_id} ;; 25 | relationship: one_to_one 26 | view_label: "Users" 27 | } 28 | } 29 | 30 | 31 | 32 | #################################################################################### 33 | # Dynamic Fields 34 | # https://looker.com/platform/blocks/data-tool/web-analytics 35 | #################################################################################### 36 | 37 | view: data_tool { 38 | extends: [events] 39 | view_label: "Events" 40 | 41 | 42 | parameter: timeframe_filter { 43 | view_label: "Data Tool" 44 | allowed_value: { value: "Date" } 45 | allowed_value: { value: "Week" } 46 | allowed_value: { value: "Month" } 47 | } 48 | 49 | dimension: timeframe { 50 | view_label: "Data Tool" 51 | sql: CASE 52 | WHEN {% parameter timeframe_filter %} = 'Date' THEN ${event_date}::varchar 53 | WHEN {% parameter timeframe_filter %} = 'Week' THEN ${event_week} 54 | WHEN {% parameter timeframe_filter %} = 'Month' THEN ${event_month} 55 | END ;; 56 | label_from_parameter: timeframe_filter 57 | drill_fields: [os, browser, event_type] 58 | } 59 | 60 | 61 | parameter: primary_metric_filter { 62 | view_label: "Data Tool" 63 | allowed_value: { value: "Users" } 64 | allowed_value: { value: "Visitors" } 65 | allowed_value: { value: "Sessions" } 66 | allowed_value: { value: "Orders" } 67 | default_value: "Users" 68 | } 69 | 70 | measure: primary_metric { 71 | type: number 72 | view_label: "Data Tool" 73 | sql: CASE 74 | WHEN {% parameter primary_metric_filter %} = 'Users' THEN ${users.count} 75 | WHEN {% parameter primary_metric_filter %} = 'Visitors' THEN ${unique_visitors} 76 | WHEN {% parameter primary_metric_filter %} = 'Sessions' THEN ${sessions.count} 77 | WHEN {% parameter primary_metric_filter %} = 'Orders' THEN ${sessions.count_purchase} 78 | END ;; 79 | label_from_parameter: primary_metric_filter 80 | drill_fields: [detail*] 81 | } 82 | 83 | parameter: second_metric_filter { 84 | view_label: "Data Tool" 85 | allowed_value: { value: "Bounces" } 86 | allowed_value: { value: "Bounce Rate" } 87 | allowed_value: { value: "Conversion Rate" } 88 | default_value: "Conversion Rate" 89 | } 90 | 91 | measure: second_metric { 92 | type: number 93 | view_label: "Data Tool" 94 | sql: CASE 95 | WHEN {% parameter second_metric_filter %} = 'Bounces' THEN ${sessions.count_bounce_sessions} 96 | WHEN {% parameter second_metric_filter %} = 'Bounce Rate' THEN round((100.0 * ${sessions.percent_bounce_sessions}),2) 97 | WHEN {% parameter second_metric_filter %} = 'Conversion Rate' THEN round((100.0 * ${sessions.conversion_rate}),2) 98 | END ;; 99 | html: {% if metric_name._value contains 'Rate' or metric_name._value contains 'Users' %} 100 | {{ linked_value }}{{ format_symbol._value }} 101 | {% else %} 102 | {{ format_symbol._value }}{{ linked_value }} 103 | {% endif %} ;; 104 | label_from_parameter: second_metric_filter 105 | drill_fields: [detail*] 106 | } 107 | 108 | 109 | set: detail { 110 | fields: [ 111 | event_date, 112 | users.count, 113 | unique_visitors, 114 | sessions.count, 115 | sessions.count_bounce_sessions, 116 | sessions.percent_bounce_sessions, 117 | sessions.conversion_rate, 118 | sessions.orders 119 | ] 120 | } 121 | 122 | 123 | 124 | 125 | ################################################################ 126 | # Used for dynamically applying a format to the metric parameter 127 | ################################################################ 128 | dimension: metric_name { 129 | hidden: yes 130 | type: string 131 | sql: CASE 132 | WHEN {% parameter second_metric_filter %} = 'Bounces' THEN 'Bounces' 133 | WHEN {% parameter second_metric_filter %} = 'Bounce Rate' THEN 'Bounce Rate' 134 | WHEN {% parameter second_metric_filter %} = 'Conversion Rate' THEN 'Conversion Rate' 135 | WHEN {% parameter second_metric_filter %} = '% New Users' THEN '% New Users' 136 | ELSE NULL 137 | END ;; 138 | } 139 | 140 | dimension: format_symbol { 141 | hidden: yes 142 | sql: 143 | CASE 144 | WHEN ${metric_name} IN ('Bounce Rate','Conversion Rate','% New Users') THEN '%' 145 | END ;; 146 | } 147 | 148 | 149 | 150 | } 151 | 152 | 153 | 154 | 155 | 156 | 157 | ################################################################ 158 | # Change view labels for a few fields 159 | ################################################################ 160 | 161 | view: sessions_webanalytics { 162 | extends: [sessions] 163 | 164 | measure: conversion_rate { 165 | view_label: "Sessions" 166 | type: number 167 | value_format_name: percent_2 168 | sql: 1.0 * ${count_purchase} / nullif(${count},0) ;; 169 | drill_fields: [count_purchase, count, overall_conversion] 170 | } 171 | 172 | measure: count_purchase { 173 | view_label: "Sessions" 174 | label: "Orders" 175 | # hidden: yes 176 | type: count 177 | filters: { 178 | field: furthest_funnel_step 179 | value: "(5) Purchase" 180 | } 181 | drill_fields: [detail*] 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /test_files/98_cohort_data_tool.view.lkml: -------------------------------------------------------------------------------- 1 | include: "*.view" # include all the views 2 | 3 | 4 | ######################################################################################## 5 | # Cohort Data Tool Explore 6 | # dynamic join on cohort size depending on what cohort is selected by the user 7 | ######################################################################################## 8 | explore: cohorts { 9 | hidden: yes 10 | label: "Cohort Data Tool" 11 | view_name: cohort_size 12 | join: users { 13 | fields: [users.id, users.name, users.first_name, users.last_name, users.email, users.age, users.created_date, users.gender, users.traffic_source, users.count] 14 | type: left_outer 15 | relationship: many_to_one 16 | sql_on: 17 | CASE 18 | WHEN {% parameter cohort_size.cohort_filter %} = 'User Signup Month' 19 | THEN ${users.created_month} = ${cohort_size.cohort} 20 | WHEN {% parameter cohort_size.cohort_filter %} = 'Gender' 21 | THEN ${users.gender} = ${cohort_size.cohort} 22 | WHEN {% parameter cohort_size.cohort_filter %} = 'Age Group' 23 | THEN ${users.age_tier} = ${cohort_size.cohort} 24 | WHEN {% parameter cohort_size.cohort_filter %} = 'Traffic Source' 25 | THEN ${users.traffic_source} = ${cohort_size.cohort} 26 | ELSE ${users.created_month} = ${cohort_size.cohort} 27 | END 28 | ;; 29 | } 30 | join: order_items { 31 | from: order_items_cohorts 32 | fields: [order_items.id, order_items.order_id, order_items.created_date, order_items.total_sale_price, order_items.months_since_signup] 33 | type: left_outer 34 | relationship: one_to_many 35 | sql_on: ${users.id} = ${order_items.user_id} ;; 36 | } 37 | } 38 | 39 | 40 | ######################################################################################## 41 | # Define Cohort Size 42 | # dynamically swap grouping for user signup month, gender, age group, traffic source 43 | ######################################################################################## 44 | 45 | view: cohort_size { 46 | view_label: "Cohort" 47 | derived_table: { 48 | sql: SELECT 49 | CASE 50 | WHEN {% parameter cohort_filter %} = 'User Signup Month' 51 | THEN TO_CHAR(DATE_TRUNC('month', CONVERT_TIMEZONE('UTC', 'America/Los_Angeles', users.created_at )), 'YYYY-MM') 52 | WHEN {% parameter cohort_filter %} = 'Gender' 53 | THEN users.gender 54 | WHEN {% parameter cohort_filter %} = 'Traffic Source' 55 | THEN users.traffic_source 56 | WHEN {% parameter cohort_filter %} = 'Age Group' 57 | THEN 58 | (CASE 59 | WHEN users.age < 0 THEN 'Below 0' 60 | WHEN users.age >= 0 AND users.age < 10 THEN '0 to 9' 61 | WHEN users.age >= 10 AND users.age < 20 THEN '10 to 19' 62 | WHEN users.age >= 20 AND users.age < 30 THEN '20 to 29' 63 | WHEN users.age >= 30 AND users.age < 40 THEN '30 to 39' 64 | WHEN users.age >= 40 AND users.age < 50 THEN '40 to 49' 65 | WHEN users.age >= 50 AND users.age < 60 THEN '50 to 59' 66 | WHEN users.age >= 60 AND users.age < 70 THEN '60 to 69' 67 | WHEN users.age >= 70 THEN '70 or Above' 68 | ELSE 'Undefined' 69 | END) 70 | ELSE TO_CHAR(DATE_TRUNC('month', CONVERT_TIMEZONE('UTC', 'America/Los_Angeles', users.created_at )), 'YYYY-MM') 71 | END AS cohort, 72 | COUNT(DISTINCT users.id ) AS cohort_size 73 | FROM users AS users 74 | GROUP BY 1 75 | ;; 76 | } 77 | 78 | 79 | parameter: cohort_filter { 80 | label: "Cohort Picker" 81 | description: "Choose a cohort" 82 | allowed_value: { value: "User Signup Month" } 83 | allowed_value: { value: "Gender" } 84 | allowed_value: { value: "Age Group" } 85 | allowed_value: { value: "Traffic Source" } 86 | } 87 | 88 | parameter: metric_filter { 89 | label: "Metric Picker" 90 | description: "Choose a metric" 91 | allowed_value: { value: "User Retention" } 92 | allowed_value: { value: "Average Orders per User" } 93 | allowed_value: { value: "Average Spend per User" } 94 | # allowed_value: { value: "Cumulative Spend" } 95 | } 96 | 97 | measure: cohort_size { 98 | type: sum 99 | sql: ${TABLE}.cohort_size ;; 100 | drill_fields: [users.id, users.name, users.email, users.age, users.created_date, users.count] 101 | } 102 | 103 | dimension: cohort { 104 | description: "Use in conjuction with the Cohort Picker" 105 | primary_key: yes 106 | type: string 107 | sql: ${TABLE}.cohort ;; 108 | label_from_parameter: cohort_filter 109 | } 110 | 111 | measure: metric { 112 | type: number 113 | description: "Use in conjuction with the Metric Picker" 114 | sql: CASE 115 | WHEN {% parameter metric_filter %} = 'User Retention' THEN trunc(${percent_user_retention}) 116 | WHEN {% parameter metric_filter %} = 'Average Orders per User' THEN round(${average_orders_per_user},2) 117 | WHEN {% parameter metric_filter %} = 'Average Spend per User' THEN trunc(${average_spend_per_user}) 118 | ELSE NULL 119 | END ;; 120 | html: {% if metric_name._value contains 'User Retention' %} 121 | {{ linked_value }}{{ format_symbol._value }} 122 | {% else %} 123 | {{ format_symbol._value }}{{ linked_value }} 124 | {% endif %} ;; 125 | drill_fields: [cohort_size, percent_user_retention, users.count, average_orders_per_user, average_spend_per_user] 126 | label_from_parameter: metric_filter 127 | } 128 | 129 | # WHEN {% parameter metric_filter %} = 'Cumulative Spend' THEN trunc(${cumulative_spend}) 130 | 131 | 132 | measure: percent_user_retention { 133 | view_label: "Users" 134 | description: "number of active users / number of users in the cohort (use with months since signup dimension)" 135 | type: number 136 | sql: round(100.0 * ${users.count} / nullif(${cohort_size},0),2) ;; 137 | value_format: "0.00\%" 138 | drill_fields: [users.count, cohort_size, percent_user_retention] 139 | } 140 | 141 | measure: average_spend_per_user { 142 | view_label: "Order Items" 143 | type: number 144 | value_format_name: usd 145 | sql: 1.0 * ${order_items.total_sale_price} / NULLIF(${users.count},0) ;; 146 | drill_fields: [order_items.id, order_items.order_id, order_items.created_date, order_items.total_sale_price, users.email] 147 | } 148 | 149 | measure: average_orders_per_user { 150 | view_label: "Order Items" 151 | type: number 152 | value_format_name: usd 153 | sql: 1.0 * ${order_items.order_count} / NULLIF(${users.count},0) ;; 154 | drill_fields: [users.email, order_items.order_count] 155 | } 156 | 157 | # measure: cumulative_spend { 158 | # view_label: "Order Items" 159 | # type: running_total 160 | # direction: "column" 161 | # value_format_name: usd 162 | # sql: ${order_items.total_sale_price} ;; 163 | # drill_fields: [order_items.id, order_items.order_id, order_items.created_date, order_items.total_sale_price, users.email] 164 | # } 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | ################################################################ 199 | # Used for dynamically applying a format to the metric parameter 200 | ################################################################ 201 | 202 | dimension: metric_name { 203 | hidden: yes 204 | type: string 205 | sql: CASE 206 | WHEN {% parameter metric_filter %} = 'User Retention' THEN 'User Retention' 207 | WHEN {% parameter metric_filter %} = 'Average Orders per User' THEN 'Average Orders per User' 208 | WHEN {% parameter metric_filter %} = 'Average Spend per User' THEN 'Average Spend per User' 209 | WHEN {% parameter metric_filter %} = 'Cumulative Spend' THEN 'Cumulative Spend' 210 | ELSE NULL 211 | END ;; 212 | } 213 | 214 | dimension: format_symbol { 215 | hidden: yes 216 | sql: 217 | CASE 218 | WHEN ${metric_name} = 'User Retention' THEN '%' 219 | WHEN ${metric_name} = 'Average Spend per User' THEN '$' 220 | WHEN ${metric_name} = 'Cumulative Spend' THEN '$' 221 | END ;; 222 | } 223 | 224 | 225 | 226 | } 227 | 228 | 229 | ################################################################ 230 | # Group 'months since signup' under Order Items 231 | ################################################################ 232 | 233 | view: order_items_cohorts { 234 | extends: [order_items] 235 | 236 | dimension: months_since_signup { 237 | view_label: "Order Items" 238 | description: "Months an order occurred since the user first signed up" 239 | type: number 240 | sql: DATEDIFF('month',${users.created_raw},${created_raw}) ;; 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /test_files/99_users_avatar.view.lkml: -------------------------------------------------------------------------------- 1 | include: "01_order_items.view.lkml" 2 | include: "02_users.view.lkml" 3 | ## kittens for certain demos 4 | 5 | view: kitten_users { 6 | extends: [users] 7 | 8 | dimension: portrait { 9 | label: "Kitten Portrait" 10 | sql: GREATEST(MOD(${id}*97,867),MOD(${id}*31,881),MOD(${id}*72,893)) ;; 11 | type: number 12 | html: