Custom Visualizations
Omni supports the most common visualization types and settings out of the box, but can also be heavily customized! We use VegaLite under the hood, which means you can edit and customize the visualization however you'd like by using the Advanced Editor. Check out the full VegaLite spec as well as examples.
Approaching the Vega Editor
Omni defines Vega specification behind all charts. By reaching into the configuration, charts can be tuned to unlock anything available in Vega. Extensive examples are available here.
To reference Omni fields in the Vega spec, periods and brackets need to be escaped. This requires a double forward-slash as follows:
- Omni object --> Vega object
users.id
-->users\\.id
users.age
-->users\\.age
users.created_at
-->users\\.created_at
users.created_at[date]
-->users\\.created_at\\[date\\]
users.created_at[month]
-->users\\.created_at\\[month\\]
id
-->id
note 'id' would only occur from a raw SQL query, as Omni will alias with the view
Saving Custom Visualization
To avoid jumpiness on the page, custom visualization need to save explicitly to update the URL (think of edits to the spec as drafts). To save a custom visualization for sharing, simply hit save and the specification will be coupled to the query URL.

Visualization Spec Library
Below, we've included several custom examples of Vega specs in Omni, eventually we'll add links to live examples.
US Map Example (Lat, Long)

Query Fields:
users.zip
users.zip_first_digit
users.latitude_average
users.longitude_average
users.user_count
{
"layer": [
{
"data": {
"url": "https://vega.github.io/editor/data/us-10m.json",
"format": {
"type": "topojson",
"feature": "states"
}
},
"mark": {
"fill": "lightgray",
"type": "geoshape",
"stroke": "white"
}
},
{
"mark": {
"type": "circle",
"tooltip": true
},
"encoding": {
"size": {
"value": 5
},
"color": {
"type": "nominal",
"field": "users\\.zip_first_digit",
"scale": {
"scheme": "magma"
},
"legend": null
},
"latitude": {
"type": "quantitative",
"field": "users\\.latitude_average"
},
"longitude": {
"type": "quantitative",
"field": "users\\.longitude_average"
}
}
}
],
"width": "container",
"height": "container",
"projection": {
"type": "albersUsa"
}
}
US Map Example (Zip Code Chloropleth)

Query Fields:
users.zip
users.user_count
{
"layer": [
{
"data": {
"url": "https://vega.github.io/editor/data/us-10m.json",
"format": {
"type": "topojson",
"feature": "states"
}
},
"mark": {
"fill": "white",
"type": "geoshape",
"stroke": "black"
}
},
{
"mark": "geoshape",
"width": "container",
"height": "container",
"encoding": {
"color": {
"type": "quantitative",
"field": "users\\.count",
"scale": {
"domain": [
0,
20
],
"scheme": "blues"
},
"legend": null
},
"shape": {
"type": "geojson",
"field": "geo"
},
"tooltip": [
{
"field": "users\\.zip"
},
{
"type": "quantitative",
"field": "users\\.count",
"title": "Users Count"
}
]
}
}
],
"transform": [
{
"as": "geo",
"from": {
"key": "properties.zip",
"data": {
"url": "https://gist.githubusercontent.com/jefffriesen/6892860/raw/e1f82336dde8de0539a7bac7b8bc60a23d0ad788/zips_us_topo.json",
"format": {
"type": "topojson",
"feature": "zip_codes_for_the_usa"
}
}
},
"lookup": "users\\.zip"
}
],
"projection": {
"type": "albersUsa"
}
}
Map Example (Zip Code Chloropleth, Washington DC)

Query Fields:
users.zip
users.user_count
{
"layer": [
{
"data": {
"url": "https://raw.githubusercontent.com/OpenDataDE/State-zip-code-GeoJSON/master/dc_district_of_columbia_zip_codes_geo.min.json",
"format": {
"property": "features"
}
},
"mark": {
"fill": "white",
"type": "geoshape",
"stroke": "black"
}
},
{
"mark": "geoshape",
"width": "container",
"height": "container",
"encoding": {
"color": {
"type": "quantitative",
"field": "users\\.count",
"scale": {
"domain": [
0,
20
],
"scheme": "blues"
},
"legend": null
},
"shape": {
"type": "geojson",
"field": "geo"
},
"tooltip": [
{
"field": "users\\.zip"
},
{
"type": "quantitative",
"field": "users\\.count",
"title": "Users Count"
}
]
}
}
],
"transform": [
{
"as": "geo",
"from": {
"key": "properties.ZCTA5CE10",
"data": {
"url": "https://raw.githubusercontent.com/OpenDataDE/State-zip-code-GeoJSON/master/dc_district_of_columbia_zip_codes_geo.min.json",
"format": {
"property": "features"
}
}
},
"lookup": "users\\.zip"
}
],
"projection": {
"type": "albersUsa"
}
}
Radial Chart

This layers in exploding pie slices using the square root of the value. Mostly of aesthetic style.
Query Fields:
products.category
order_items.sale_price_sum
{
"layer": [
{
"mark": {
"type": "arc",
"stroke": "#fff",
"innerRadius": 30
}
},
{
"mark": {
"dx": 4,
"type": "text",
"align": "center",
"radiusOffset": 30
},
"encoding": {
"text": {
"type": "nominal",
"field": "products\\.category"
}
}
}
],
"height": "container",
"width": "container",
"encoding": {
"color": {
"type": "nominal",
"field": "order_items\\.sale_price_sum",
"legend": null
},
"theta": {
"type": "quantitative",
"field": "order_items\\.sale_price_sum",
"stack": true
},
"radius": {
"field": "order_items\\.sale_price_sum",
"scale": {
"type": "sqrt",
"zero": true,
"rangeMin": 20
}
},
"tooltip": [
{
"type": "nominal",
"field": "products\\.category",
"title": "Category"
},
{
"type": "quantitative",
"field": "order_items\\.sale_price_sum",
"title": "Sales",
"format": ",.2f"
}
]
}
}
Cross Filtered Chart Pair

The visualization aggregates the top visualization over the highlight selection. Mainly a proof of concept for highly interactive vis. We essentially build two charts from the data table, stack them, and wire them together. Note also this vis uses pixel sizing, which is not ideal for use on dashboards (where "container" should be used for sizing).
Query Fields:
order_items.created_at[date]
filtered to 2021, the x-axisproducts.category
filtered to five products, forming the color facetsorder_items.sale_price_sum
the y-axisorder_items.count
the bubble size
{
"vconcat": [
{
"mark": "point",
"width": 600,
"height": 300,
"params": [
{
"name": "brush",
"select": {
"type": "interval",
"encodings": [
"x"
]
}
}
],
"encoding": {
"x": {
"type": "temporal",
"field": "order_items\\.created_at\\[date\\]__raw",
"title": "Date"
},
"y": {
"type": "quantitative",
"field": "order_items\\.sale_price_sum",
"title": "Total Sales"
},
"size": {
"type": "quantitative",
"field": "order_items\\.count",
"title": "Count of Sales"
},
"color": {
"value": "lightgray",
"condition": {
"type": "nominal",
"field": "products\\.category",
"param": "brush",
"scale": {
"range": [
"#e7ba52",
"#a7a7a7",
"#aec7e8",
"#1f77b4",
"#9467bd"
],
"domain": [
"Jeans",
"Accessories",
"Outerwear & Coats",
"Fashion Hoodies & Sweatshirts",
"Tops & Tees"
]
},
"title": "Category"
}
}
},
"transform": [
{
"filter": {
"param": "click"
}
}
]
},
{
"mark": "bar",
"width": 600,
"params": [
{
"name": "click",
"select": {
"type": "point",
"encodings": [
"color"
]
}
}
],
"encoding": {
"x": {
"field": "order_items\\.sale_price_sum",
"title": "Sales",
"aggregate": "sum"
},
"y": {
"field": "products\\.category",
"title": "Category"
},
"color": {
"value": "lightgray",
"condition": {
"field": "products\\.category",
"param": "click",
"scale": {
"range": [
"#e7ba52",
"#a7a7a7",
"#aec7e8",
"#1f77b4",
"#9467bd"
],
"domain": [
"Jeans",
"Accessories",
"Outerwear & Coats",
"Fashion Hoodies & Sweatshirts",
"Tops & Tees"
]
}
}
}
},
"transform": [
{
"filter": {
"param": "brush"
}
}
]
}
]
}
Flag Marks Scatterplot

The chart uses data about each country's economy, along with emojis representing the flag. There are several other measures available that are not visualized in the chart. Here we are simply swapping the mark for text (the emoji), and created a normal scatterplot otherwise. We also have some small transformations on the axes to use log-scale.

Query Fields:
flag
name
(country name)rank
gdp
growth
population
gdp_per_capita
gdp_percent_share
{
"mark": {
"type": "text",
"fontSize": 30
},
"width": "container",
"height": "container",
"encoding": {
"x": {
"type": "quantitative",
"field": "gdp_per_capita",
"scale": {
"type": "log",
"domain": [
200,
200000
]
}
},
"y": {
"axis": {
"labelOverlap": true
},
"type": "quantitative",
"field": "gdp",
"scale": {
"type": "log"
}
},
"text": {
"type": "nominal",
"field": "flag"
},
"tooltip": [
{
"sort": null,
"type": "nominal",
"field": "flag"
},
{
"sort": null,
"type": "nominal",
"field": "name",
"title": "country"
},
{
"sort": null,
"type": "quantitative",
"field": "gdp"
},
{
"type": "quantitative",
"field": "gdp_per_capita"
}
]
}
}
Waterfall

This chart requires both a custom visualization spec and some query munging. We can probably make this a bit simpler, but it's instructive for now on how flexible querying plus custom vis can be. Here we simulate some change data state by state and then append special bars for the start and finish values. This can probably become dynamic in the future. The data is mashed together via a simple SQL union. Note there is also quite a bit of calculation in the Vega spec, showing the ability to extend the basic data set to enhance the vis.

Query Fields:
label
value
Unioned Queries
- Begin row: start value, must be named 'Begin' for label, this can be replaced with the first value from the data set in the future
- Waterfall data set: usually easiest to build with UI and drop in the SQL or via a SQL block
- End row: value must be 0, must be named 'End', this can be replaced in the future since it's all implied
{
"layer": [
{
"mark": {
"size": 45,
"type": "bar"
},
"encoding": {
"y": {
"type": "quantitative",
"field": "previous_sum",
"title": "Amount"
},
"y2": {
"field": "sum"
},
"color": {
"value": "#93c4aa",
"condition": [
{
"test": "datum.label === 'Begin' || datum.label === 'End'",
"value": "#f7e0b6"
},
{
"test": "datum.sum < datum.previous_sum",
"value": "#f78a64"
}
]
}
}
},
{
"mark": {
"type": "rule",
"color": "#404040",
"opacity": 1,
"xOffset": -22.5,
"x2Offset": 22.5,
"strokeWidth": 2
},
"encoding": {
"y": {
"type": "quantitative",
"field": "sum"
},
"x2": {
"field": "lead"
}
}
},
{
"mark": {
"dy": -4,
"type": "text",
"baseline": "bottom"
},
"encoding": {
"y": {
"type": "quantitative",
"field": "sum_inc"
},
"text": {
"type": "nominal",
"field": "sum_inc"
}
}
},
{
"mark": {
"dy": 4,
"type": "text",
"baseline": "top"
},
"encoding": {
"y": {
"type": "quantitative",
"field": "sum_dec"
},
"text": {
"type": "nominal",
"field": "sum_dec"
}
}
},
{
"mark": {
"type": "text",
"baseline": "middle",
"fontWeight": "bold"
},
"encoding": {
"y": {
"type": "quantitative",
"field": "center"
},
"text": {
"type": "nominal",
"field": "text_amount"
},
"color": {
"value": "white",
"condition": [
{
"test": "datum.label === 'Begin' || datum.label === 'End'",
"value": "#725a30"
}
]
}
}
}
],
"width": "container",
"config": {
"text": {
"color": "#404040",
"fontWeight": "bold"
}
},
"height": "container",
"encoding": {
"x": {
"axis": {
"title": "Months",
"labelAngle": 0
},
"sort": null,
"type": "ordinal",
"field": "label"
}
},
"transform": [
{
"window": [
{
"as": "sum",
"op": "sum",
"field": "amount"
}
]
},
{
"window": [
{
"as": "lead",
"op": "lead",
"field": "label"
}
]
},
{
"as": "lead",
"calculate": "datum.lead === null ? datum.label : datum.lead"
},
{
"as": "previous_sum",
"calculate": "datum.label === 'End' ? 0 : datum.sum - datum.amount"
},
{
"as": "amount",
"calculate": "datum.label === 'End' ? datum.sum : datum.amount"
},
{
"as": "text_amount",
"calculate": "(datum.label !== 'Begin' && datum.label !== 'End' && datum.amount > 0 ? '+' : '') + datum.amount"
},
{
"as": "center",
"calculate": "(datum.sum + datum.previous_sum) / 2"
},
{
"as": "sum_dec",
"calculate": "datum.sum < datum.previous_sum ? datum.sum : ''"
},
{
"as": "sum_inc",
"calculate": "datum.sum > datum.previous_sum ? datum.sum : ''"
}
]
}
Boxplot

Boxplot is calculated using the distribution over a data set, so in this example, we actually upped the row limit to 50k results using SQL. Our example has several retail categories and the prices of goods in each category. The plot then build some summary statistics over the distribution of each category.
Query Fields:
category
retail_price
{
"mark": {
"type": "boxplot",
"extent": "min-max"
},
"width": "container",
"height": "container",
"encoding": {
"x": {
"axis": {
"title": "Brand"
},
"type": "ordinal",
"field": "products\\.category"
},
"y": {
"axis": {
"title": "Price"
},
"type": "quantitative",
"field": "products.retail_price"
},
"color": {
"type": "nominal",
"field": "products\\.category",
"legend": null
}
}
}
Heatmap

This chart is actually out of the box, but can be tricky, so we're including it here. To build this heatmap, simply add dimensions to both X and Y axes, using your measure in color. In this case week is on the X-axis, and day of week is on the Y-axis, with sales on color.
Query Fields:
week
day_of_week
sales_price_sum
Funnel Vis

This chart is for measuring a funnel with several filtered measures. It calculates both overall drop-off and step-by-step dropoff. The vis can be tweaked to more or fewer stages by editing the fold
section and then the subsequent steps below, removing the backticks - ie. users\\.count
then "measurename": "users.count"
.
Query Fields:
users.count
users.count_california_seniors
users.count_minors
users.count_california_minors
{
"layer": [
{
"mark": {
"type": "bar",
"color": "transparent"
},
"encoding": {
"x": {
"axis": "",
"type": "quantitative",
"field": "stagePos"
}
}
},
{
"mark": {
"type": "bar",
"tooltip": true
},
"encoding": {
"x": {
"axis": "",
"type": "quantitative",
"field": "negCount"
},
"color": {
"field": "stage",
"scale": {
"scheme": {
"name": "oranges",
"extent": [
0.8,
0
]
}
},
"legend": null
},
"tooltip": [
{
"type": "nominal",
"field": "stage",
"title": "Stage"
},
{
"type": "quantitative",
"field": "count",
"title": "Count"
}
]
}
},
{
"mark": {
"dx": {
"expr": "datum.labelLeft ? -4 : 4"
},
"type": "text",
"align": {
"expr": "datum.labelLeft ? 'right' : 'left'"
}
},
"encoding": {
"x": {
"axis": "",
"type": "quantitative",
"field": "negCount"
},
"text": {
"field": "count"
}
}
},
{
"mark": {
"dx": 4,
"type": "text",
"align": "left"
},
"encoding": {
"x": {
"axis": "",
"type": "quantitative",
"field": "stagePos"
},
"text": {
"field": "stage"
}
}
},
{
"mark": {
"type": "text",
"align": "center"
},
"encoding": {
"x": {
"axis": "",
"type": "quantitative",
"field": "cumulativePos"
},
"text": {
"field": "cumulativePct",
"format": ".1%"
}
}
},
{
"mark": {
"type": "text",
"align": "center"
},
"encoding": {
"x": {
"axis": "",
"type": "quantitative",
"field": "conversionPos"
},
"text": {
"field": "conversionPct",
"format": ".1%"
}
},
"transform": [
{
"filter": "isValid(datum.previousCount)"
}
]
},
{
"mark": {
"dx": {
"expr": "datum.dx"
},
"type": "text",
"align": {
"expr": "datum.align"
}
},
"encoding": {
"x": {
"axis": "",
"type": "quantitative",
"field": "pos"
},
"y": {
"axis": null,
"type": "nominal",
"datum": "0. Titles"
},
"text": {
"field": "caption"
}
},
"transform": [
{
"filter": "!isValid(datum.previousCount)"
},
{
"as": "zero",
"calculate": "0"
},
{
"as": [
"column",
"pos"
],
"fold": [
"stagePos",
"zero",
"cumulativePos",
"conversionPos"
]
},
{
"from": {
"key": "column",
"data": {
"values": [
{
"dx": 4,
"align": "left",
"column": "stagePos",
"caption": "Stage"
},
{
"dx": -4,
"align": "right",
"column": "zero",
"caption": "Count"
},
{
"dx": 0,
"align": "center",
"column": "cumulativePos",
"caption": "Overall"
},
{
"dx": 0,
"align": "center",
"column": "conversionPos",
"caption": "Previous"
}
]
},
"fields": [
"caption",
"align",
"dx"
]
},
"lookup": "column"
}
]
}
],
"width": "container",
"height": "container",
"encoding": {
"y": {
"axis": "",
"type": "nominal",
"field": "stage"
}
},
"transform": [
{
"as": [
"measurename",
"count"
],
"fold": [
"users\\.count",
"users\\.count_california_seniors",
"users\\.count_minors",
"users\\.count_california_minors"
]
},
{
"from": {
"key": "measurename",
"data": {
"values": [
{
"stage": "1. Users",
"measurename": "users.count"
},
{
"stage": "2. California ",
"measurename": "users.count_california_seniors"
},
{
"stage": "3. Minors",
"measurename": "users.count_minors"
},
{
"stage": "4. California Minors",
"measurename": "users.count_california_minors"
}
]
},
"fields": [
"stage"
]
},
"lookup": "measurename"
},
{
"joinaggregate": [
{
"as": "maxCount",
"op": "max",
"field": "count"
}
]
},
{
"sort": [
{
"field": "stage",
"order": "ascending"
}
],
"window": [
{
"as": "previousCount",
"op": "lag",
"field": "count"
}
]
},
{
"as": "cumulativePct",
"calculate": "datum.count / datum.maxCount"
},
{
"as": "conversionPct",
"calculate": "datum.count / datum.previousCount"
},
{
"as": "countPos",
"calculate": "datum.maxCount * 0.5"
},
{
"as": "cumulativePos",
"calculate": "datum.maxCount * 0.08"
},
{
"as": "conversionPos",
"calculate": "datum.maxCount * 0.16"
},
{
"as": "stagePos",
"calculate": "datum.maxCount * -1.2"
},
{
"as": "negCount",
"calculate": "-datum.count"
},
{
"as": "labelLeft",
"calculate": "datum.count < 0.1 * datum.maxCount"
}
]
}