In a culture where it seems like all we do is work, I thought it would be interesting to take a deep dive into the annual leave data offered by country. I guess I am not the only person interested in this topic, as I was lucky enough to win the the Tableau Public Viz of the Day recognition. Here is a little bit of insight into how I created this visualization.
Throughout your Tableau journey you will come to point where you will want to create a more advanced chart, which can often be intimidating. Especially when you start going through the rabbit hole of advanced charts across the internet.
One of the first advanced charts that I created was a radial bar chart using table calculations. I used this blog from the Data School to help me create the visualization below and I was super proud when I completed it!

I wanted to challenge myself and see if it was possible to use the Map Layers feature to create the radial bar chart. I read this great blog by the Flerlage twins before I began my journey and used the information from the Data School blog to help me build this chart.
The Data
First things first, creating this visualization required some data manipulation similar to the other blogs whereby I duplicated the data to create a path. I used R to read in my Annual Leave file, as well as the World Bank data on tourism that I appended to this file. One of the issues with trying to recreate radial bar charts in map layers is not being able to use table calculations. This blog from the Tableau Community states:
The new map layers feature introduced in v2020.4 only works when the fields used are dimensions (row level) or regular aggregates. Layers are not supported when the coordinates or geometries are table calculations, aggregates using ATTR(), nor data blends. In addition the MAKEPOINT() function doesn’t work on any of these calculations, it throws an error about not being able to be applied to table calculations nor fields from multiple data sources.
Lucky for us, the way to recreate table calculations is to use Tableau’s LOD (Level of Detail) Expressions. Below is the R script that I used to prep my data and knowing that I wouldn’t be able to use Table Calculations. I could create these in my datasource and also do the densification so I wouldn’t have to do any joins in Tableau itself. To be honest, it is always my preference to compute any joins or data manipulation before I bring it in Tableau.
#Annual Leave
library(tidyverse)
library(readxl)
library(dplyr)
setwd("_path_to_folder")
# Read in annual leave file
annual_leave <- read_excel("Annual Leave.xlsx", sheet = 1)
# Clean up dataframe
df <- annual_leave %>%
mutate(
`Paid Vacation (MAX)` = ifelse(is.na(`Paid Vacation (MAX)`), 0, `Paid Vacation (MAX)`),
`Paid Vacation (MIN)` = ifelse(is.na(`Paid Vacation (MIN)`), 0, `Paid Vacation (MIN)`),
`Paid Public Holidays (MAX)` = ifelse(is.na(`Paid Public Holidays (MAX)`), 0, `Paid Public Holidays (MAX)`),
`Paid Public Holidays (MIN)` = ifelse(is.na(`Paid Public Holidays (MIN)`), 0, `Paid Public Holidays (MIN)`)
) %>%
select(OrderN, Continent, Country, `Country Code`, `Minual Annual Leave`,
`Paid Vacation (MIN)`, `Paid Vacation (MAX)`, `Paid Public Holidays (MIN)`, `Paid Public Holidays (MAX)`,
`Total Paid Leave (MIN)`, `Total Paid Leave (MAX)`) %>%
mutate(`Total Paid Leave (MAX)` = coalesce(`Total Paid Leave (MAX)`, `Paid Vacation (MAX)` + `Paid Public Holidays (MAX)`),
`Total Paid Leave (MIN)` = coalesce(`Total Paid Leave (MIN)`, `Paid Vacation (MIN)` + `Paid Public Holidays (MIN)`)) %>%
# Remove countries/groups that have no data or is not needed
filter(!Country %in% c('European Union','Kiribati','Marshall Islands','Micronesia','Nauru','Palau', 'Tonga') ) %>%
group_by(Continent) %>%
# Creates Order for Radial bars to show correctly
arrange(Continent, Country, .by_group = TRUE) %>%
mutate(OrderNCont = row_number()) %>%
ungroup()
# Read in happiness data (not used in visual)
happy <- read_excel("happinessData.xls", sheet = 1) %>%
select(`Country name`, `Ladder score`)
# Read in tourism and population data from WorldBank
poptour <- read_csv("tourismPopData.csv")
poptour_rm_null <- poptour %>%
mutate(across(everything(), ~ ifelse(. == "..", NA, .)))
#Check and Clean data from world bank to change '..' to blanks/nulls
sum(poptour_rm_null == "..", na.rm = TRUE) # Should return 0
sum(is.na(poptour_rm_null)) # Should return a nonzero count if replacement worked
sum(poptour_tp$Value == "", na.rm = TRUE) # If >0, you have empty strings
# transpose the data
poptour_tp <- poptour_rm_null %>%
gather(key="Year", value = "Value",
-`Country Name`, -`Country Code`, -`Series Name`,-`Series Code`)
# remove null values and keep the most recent year of data for metrics
poptour_tp <- poptour_tp %>% drop_na(Value)
poptour_tp <- poptour_tp %>%
mutate(Year = substr(Year, 1, 4)) %>% # Extract first 4 characters
group_by(`Country Name`, `Series Name`) %>% # Group by country
slice_max(order_by = Year, n = 1) %>% # Keep the most recent year
ungroup()
# rename column names
poptour_tp_rn <- poptour_tp %>%
mutate(`Series Code` = case_when(
`Series Code` == "ST.INT.ARVL" ~ "Tourism Arrivals",
`Series Code` == "ST.INT.DPRT" ~ "Tourism Departures",
`Series Code` == "NY.GDP.MKTP.CD" ~ "GDP",
`Series Code` == "EN.POP.DNST" ~ "Population Density",
`Series Code` == "SP.POP.TOTL" ~ "Population Total",
TRUE ~ `Series Code` # Keep original if no match
))
# transpose data again
poptour_tp_wide <- poptour_tp_rn %>%
select(`Country Name`, `Country Code`, `Series Code`, Value) %>% # Keep Year
pivot_wider(
names_from = `Series Code`, # Make "Series Code" values into column names
values_from = Value # Fill new columns with "Value"
)
# transpose annual leave data and create path value for data densification
df_tp <- df %>%
gather(key="Statistic", value = "Value",
-OrderN, -OrderNCont, -Continent, -Country, -`Country Code`, -`Minual Annual Leave`) %>%
mutate(path=0)
df_tp2 <- df %>%
gather(key="Statistic", value = "Value",
-OrderN, -OrderNCont, -Continent, -Country, -`Country Code`, -`Minual Annual Leave`) %>%
mutate(path=1)
# stack data
df_final <-rbind(df_tp,df_tp2)
# join in happy and population/tourism data
df_join_happy <- merge(df_final, happy, by.x='Country', by.y='Country name', all.x=TRUE )
df_join_poptour <- merge(df_join_happy, poptour_tp_wide, by.x='Country Code', by.y='Country Code', all.x=TRUE )
# check # of players per season and average match played
df_check <- df_join_poptour %>%
group_by(path) %>%
summarise(
num_distinct_countries = n_distinct(Country) # Count distinct countries
)
#write files to csv ready to use
write.csv(df_join_poptour, file = "AnnualLeaveTP.csv")
Great, now my data is prepped and ready to go into Tableau. You could also, do this in Python or another tool of preference if R is not in your wheel house, or if you prefer another computing language.
Building in Tableau
Now because we did our data prep before, we have no joins or unions to do in Tableau. The calculations and instructions that follow will be somewhat similar to the radial bar chart blog from data school that was shared at the beginning of this blog.
So first lets create two parameters that allow us to customize the size of our circles. We will call these parameters ‘Inner’ and ‘Outer’ and they will be setup like below:


The ‘Inner’ parameter will control where our radial bars will start (0.5 to default) and the ‘Outer’ (1 to default) will determine where our maximum value for our radial bars will end.
Next, let’s create the most important calculation that allows our Radial Bar, Circle and Diamonds to align nicely in the view. This will be our Angle field:
// Map | Angle
[Order N Cont] * (1/{MAX({FIXED : COUNTD([Country])})} ) * 2 * PI() * 3
Lets break down this calculation:
- [Order N Cont ] – this is the order of our countries by continent. The order I decided to use here was alphabetical. So for our data Argentina, South America, and Albania, Europe, will both have values of 1 as they are alphabetically the first country in each continent. This was previously an INDEX() function, however we can’t use Table calculations for Map Layers.
- 1/{MAX({FIXED : COUNTD([Country])})} – this gives us a fractional value assigned to each country essentially 1 / total # of countries, which is 1/187 = 0.0053.
- 2*PI() – this part of the calculation allows us to transform the data structure to a circle and the times
- 3 – this value increases the space between our bars so they can breathe a little easier.
The below table you shows you how the Map | Angle calculation is working:

Our next important calculation is determining the length of the bars. For total paid leave my calculation is:
// Map | Length - Total Paid Leave
[Inner] + IIF([Path]=0, 0, [Total Paid Leave (MIN)] / {MAX([Total Paid Leave (MIN)])}
* ([Outer] - [Inner])
)
Lets break down this calculation:
- [Inner] – is where our bars will start from – this is defaulted at 0.5
- IIF([Path]=0, 0, [Total Paid Leave (MIN)] / {MAX([Total Paid Leave (MIN)])} – if our Path is 0 then we just give it a starting position of our inner circle which is 0.5. If the Path is 1 then we give it a normalized value of our metric Total Paid Leave. This multiplied by the ([Outer]-[Inner]) gives us our length of the bar.
- ([Outer] – [Inner]) – multiplier on our normalized value to keep it between 0.5 and 1.
The below table you shows you how the Map | Length – Total Paid Leave calculation is working:

Now we have our Angle and Length calculations we can now use the beauty of trigonometry to create our X and Y coordinates using the SIN() and COS() functions:
// Map | X - Paid Vacation
[Map | Length - Paid Vacation]*SIN([Map | Angle])
// Map | Y - Paid Vacation
[Map | Length - Paid Vacation]*COS([Map | Angle])
Lets see what happens when we drag these in to Tableau.

Great, we can now use these calculations as foundations for our other calculations. You can see the radial bar charts have been created, however we want to now use Tableau’s MAKEPOINT() function so we can convert these into spatial objects, allowing us to create multiple layers for our chart. This is what makes Map Layers such a powerful function and allows developers the freedom to make very customizable, detailed, and in my opinion better looking charts. So, let’s go ahead and create this MAKEPOINT() calculation, as well as two grid calculations. This allows us to layout the continents in a 3 x 2 grid so we can see it a little easier:
// Map | Points - Paid Vacation
MAKEPOINT([Map | Y - Paid Vacation], [Map | X - Paid Vacation])
// Grid - Rows
CASE [Continent]
WHEN IN('North America', 'South America') THEN 1
WHEN IN('Africa', 'Europe') THEN 2
WHEN IN('Asia', 'Oceania') THEN 3
END
// Grid - Columns
CASE [Continent]
WHEN IN('North America', 'Europe', 'Asia' ) THEN 1
WHEN IN('Africa', 'South America','Oceania' ) THEN 2
END

To build the chart above:
- Drag your ‘Map | Points – Paid Vacation’ to the view. This will automatically create Longitude (Columns shelf) and Latitude (Rows shelf) in your view.
- Drag ‘Grid – Columns’ to the Columns shelf and ‘Grid – Rows’ to the Rows shelf
- Drag Country and Continent to the detail mark.
- Update the Marks to Line
- Drag ‘Path’ to Path mark
With our first layer created, we can duplicate a lot of our calculations to create the lengths, x-coordinate and y-coordinate. One thing that does stay consistent here is the Angle that we are using:
// Map | Length - Total Paid Leave
[Inner] + IIF([Path]=0, 0, [Paid Vacation (MIN)] /
{MAX([Total Paid Leave (MIN)])} * ([Outer] - [Inner]))
// Map | X - Total Paid Leave
[Map | Length - Total Paid Leave]*SIN([Map | Angle])
// Map | Y - Total Paid Leave
[Map | Length - Total Paid Leave]*COS([Map | Angle])
// Map | Points - Total Paid Leave
MAKEPOINT([Map | Y - Total Paid Leave], [Map | X - Total Paid Leave])
// Map | Placement - Inbound Tourists
[Inner] + IIF([Path]=0, 0, 1.1* ([Outer] - [Inner]))
// Tourism Arrival % of Total
IF MIN([Path]) = 0 THEN
(IF SUM([Tourism Arrival | Country]) / MIN({SUM([Tourism Arrival | Country])}) = 0 THEN NULL ELSE SUM([Tourism Arrival | Country]) / MIN({SUM([Tourism Arrival | Country])}) END)
ELSE NULL END
// Map | X - Inbound Tourist
IF [Path] = 1 THEN [Map | Placement - Inbound Tourists]*SIN([Map | Angle]) ELSE NULL END
// Map | Y - Inbound Tourist
IF [Path] = 1 THEN [Map | Placement - Inbound Tourists]*COS([Map | Angle]) ELSE NULL END
// Map | Points - Inbound Tourist
MAKEPOINT([Map | Y - Inbound Tourist], [Map | X - Inbound Tourist])
// Map | Placement - Outbound Tourists
[Inner] + IIF([Path]=0, 0, .8* ([Outer] - [Inner]))
// Tourism Departure % of Total
IF MIN([Path]) = 0 THEN
IF SUM([Tourism Departure | Country]) / MIN({SUM([Tourism Departure | Country])}) = 0 THEN NULL
ELSE SUM([Tourism Departure | Country]) / MIN({SUM([Tourism Departure | Country])}) END
ELSE NULL END
// Map | X - Inbound Tourist
IF [Path] = 1 THEN [Map | Placement - Inbound Tourists]*SIN([Map | Angle]) ELSE NULL END
// Map | Y - Inbound Tourist
IF [Path] = 1 THEN [Map | Placement - Inbound Tourists]*COS([Map | Angle]) ELSE NULL END
// Map | Points - Inbound Tourist
MAKEPOINT([Map | Y - Inbound Tourist], [Map | X - Inbound Tourist])
One tip I would suggest when using Map Layers is dragging in at least two points when the Marks card is still automatically selected as Map. If you only bring in one point calculation into the view and then go to Map -> Background Maps -> None in the toolbar, Tableau then sometimes doesn’t work when bringing in the other layers. By bringing in the second layer straight away, leaving the map background, this should allow you to keep adding layers and remove the background with ease.
To add the continent and world average circle reference lines and continent center circles showing the three types of annual leave, I used the below calculations:
// REFERENCE LINES
// Map | Size Total Paid Cont Avg
[Inner] + IIF([Path]=0, NULL, {FIXED [Continent] : AVG([Total Paid Leave (MIN)])} / {FIXED : MAX([Total Paid Leave (MIN)])}
* ([Outer] - [Inner])
)
// Map | X - Cont Average
[Map | Size Total Paid Cont Avg]*SIN([Map | Angle])
// Map | Y - Cont Average
[Map | Size Total Paid Cont Avg]*COS([Map | Angle])
// Map | Points - Cont Average
MAKEPOINT([Map | Y - Cont Average], [Map | X - Cont Average])
// Map | Size Total Paid World Avg
[Inner] +
IIF([Path]=0, NULL, {FIXED : AVG([Total Paid Leave (MIN)])} / {FIXED : MAX([Total Paid Leave (MIN)])}
* ([Outer] - [Inner])
)
// Map | X - World Average
[Map | Size Total Paid World Avg]*SIN([Map | Angle])
// Map | Y - Cont Average
[Map | Size Total Paid World Avg]*COS([Map | Angle])
// Map | Points - World Average
MAKEPOINT([Map | Y - World Average], [Map | X - World Average])
// CENTER CIRCLES
// Map | Zero Point
MAKEPOINT(0,0)
Now all you have to do is build the chart! Feel free to download the workbook and reverse engineer the ‘Map Radial’ worksheet to see how each layer was built in more detail. Thank you for reading and I hope this helps demonstrate how using Map Layers in Tableau can allow you to do almost anything you put your mind to.

Note: when you look at this sheet you will see three layers that are hidden on this chart where I will potentially be adding a toggle to turn on and off the labels for the countries that are angled with the radial bars.

great viz, great walkthrough 🙂
Thank you so much Peter, let me know if you have any questions!