In this notebook, we’ll investigate time series plotting in R to
achieve a visually pleasing and reusable high quality plot template for
other plots in the future.
Here, we look at engine oil temperature on a relatively hot day
running through periods of b-road and motorway driving. We also include
coasting (off/on) as an example of a discrete signal plotting. The
notebook is about plot visuals - in order to properly assess oil
temperature, at least engine load should be considered as a factor.
Preparing data
coasting <- read_csv("hot.ChannelGroup_0_CAN1_-_message_dsg_10hz_0x359.csv")
Rows: 21501 Columns: 2── Column specification ─────────────────────────────────────────────────────────────────
Delimiter: ","
dbl (2): timestamps, CAN1.dsg_10hz.coasting
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
oil_temp <- read_csv("hot.ChannelGroup_3_CAN2_-_message_engine_7_50hz_0x588.csv")
Rows: 21501 Columns: 2── Column specification ─────────────────────────────────────────────────────────────────
Delimiter: ","
dbl (2): timestamps, CAN2.engine_7_50hz.oil_temperature
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
speed <- read_csv("hot.ChannelGroup_1_CAN1_-_message_kombi_1_40hz_0x320.csv")
Rows: 21501 Columns: 2── Column specification ─────────────────────────────────────────────────────────────────
Delimiter: ","
dbl (2): timestamps, CAN1.kombi_1_40hz.kombi_speed_actual
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
ambient_temperature <- read_csv("hot.ChannelGroup_2_CAN1_-_message_mfd_50hz_0x527.csv")
Rows: 21501 Columns: 2── Column specification ─────────────────────────────────────────────────────────────────
Delimiter: ","
dbl (2): timestamps, CAN1.mfd_50hz.ambient_temperature
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
Then, we’ll load the data CSVs. These are extracted from CAN dump
data and interpolated to 0.1s - each CSV will thus have same timestamps
and a single data column. The CSV names represent the originating
channel group and therefore the actual signal name is not present in the
CSV name. This boils down to how other tools export the data rather than
a conscious choice.
colnames(coasting)[2] = "coasting"
colnames(oil_temp)[2] = "t_oil"
colnames(ambient_temperature)[2] = "t_amb"
colnames(speed)[2] = "speed"
The raw export CSVs had a little bit verbose column names so let’s
rename the columns to more human readable format
df_list <- list(coasting, oil_temp, ambient_temperature, speed)
df <- df_list %>% reduce(full_join, by='timestamps')
head(df)
| | | | |
---|
0.0 | 0 | -59 | 28.5 | 0 |
0.1 | 0 | -59 | 28.5 | 0 |
0.2 | 0 | -59 | 28.5 | 0 |
0.3 | 0 | -59 | 28.5 | 0 |
0.4 | 0 | -59 | 28.5 | 0 |
0.5 | 0 | -59 | 28.5 | 0 |
As all CSVs had same timestamps and same number of samples we can
join all the data into a single dataframe for easier handling. To do
this, let’s first construct a list of all datasets and then pass that to
reduce
function to join the rows through common column
timestamps
. Showing the first few rows of the result
dataframe we’ll see oil temperature starts with a cool -59 value before
the ignition is on. Rather than fixing this in the data we’ll just
filter it out later by limiting the graph axis.
| | | | |
---|
2149.5 | 0 | 102 | 22.5 | 0 |
2149.6 | 0 | 102 | 22.5 | 0 |
2149.7 | 0 | 102 | 22.5 | 0 |
2149.8 | 0 | 102 | 22.5 | 0 |
2149.9 | 0 | 102 | 22.5 | 0 |
2150.0 | 0 | 102 | 22.5 | 0 |
By looking at the last few rows of the dataframe we see the trip
length was 2150 seconds. Let’s define a time range of interest as an
integer vector - this way we can later easily zoom the graphs to a
specific time frame.
Plotting
speed_plot <- df %>%
select(timestamps, speed) %>%
na.omit() %>%
ggplot() +
geom_line(aes(x = timestamps, y = speed), linewidth = 0.4, alpha = 0.75, color = "deepskyblue4") +
scale_x_continuous(breaks = seq(0, TIME_RANGE[2], by = 120), expand = c(0.01, 0.01), limits = TIME_RANGE) +
scale_y_continuous(breaks = seq(0, 130, by = 10), limits = c(0, 130)) +
ylab("Speed [km/h]") +
theme_minimal() +
theme(
axis.title.x = element_blank(), axis.text.x = element_blank(),
axis.title.y = element_text(margin = margin(t = 0, r = 10, b = 0, l = 0)),
plot.margin = margin(t = 5.5, r = 5.5, b = 0, l = 5.5))
Having dataframe prepared we can create the plots for each variable.
There’s a lot going on here so let’s break down a plot for vehicle
speed:
- Take all of a dataframe and select
timestamp
,
speed
tuples
- Feed the result of selection through omit filter that ignores
columns that have no specified value
- Feed the filtering result to a new
ggplot()
object
- Which consists of a line plot having timestamps as x axis and speed
as y axis
- X axis is a continuous signal with breaks every 120 seconds, limited
to the configured data range as opposed to all of data, and it has 1%
padding on both sides
- Y axis is a continuous signal with range 0..130 and a break every 10
km/h
- The Y axis label is
Speed [km/h]
- The plot uses a minimal theme, this basically resets background
colors etc
- But then we’ll override few theme settings
- x axis will not show title nor break texts
- y axis margins are adjusted to push title little bit further from
break texts
- overall plot margins are adjusted for stacking multiple plots on top
of each other
Note well start the x break sequence always from zero. This allows
time range to start from any second but breaks will still be zero
-based.
oil_temp_plot <- df %>%
select(timestamps, t_oil) %>%
na.omit() %>%
ggplot() +
geom_line(aes(x = timestamps, y = t_oil), linewidth = 0.4, alpha = 0.75, color = "firebrick4") +
scale_x_continuous(breaks = seq(0, TIME_RANGE[2], by = 120), expand = c(0.01, 0.01), limits = TIME_RANGE) +
scale_y_continuous(breaks = seq(70, 110, by = 5), limits = c(70, 110)) +
ylab("Oil Temperature [C]") +
theme_minimal() +
theme(
axis.title.x = element_blank(), axis.text.x = element_blank(),
axis.title.y = element_text(margin = margin(t = 0, r = 10, b = 0, l = 0)),
plot.margin = margin(t = 0, r = 5.5, b = 0, l = 5.5))
amb_temp_plot <- df %>%
select(timestamps, t_amb) %>%
na.omit() %>%
ggplot() +
geom_line(aes(x = timestamps, y = t_amb), linewidth = 0.4, alpha = 0.75, color = "darkgreen") +
scale_x_continuous(breaks = seq(0, TIME_RANGE[2], by = 120), expand = c(0.01, 0.01), limits = TIME_RANGE) +
scale_y_continuous(breaks = seq(22, 32, by = 1), limits = c(22, 32)) +
ylab("Ambient Temperature [C]") +
theme_minimal() +
theme(
axis.title.x = element_blank(), axis.text.x = element_blank(),
axis.title.y = element_text(margin = margin(t = 0, r = 10, b = 0, l = 0)),
plot.margin = margin(t = 0, r = 5.5, b = 0, l = 5.5))
The plots for oil temperature and ambient temperature will be
similar, only changing in color, y axis range, title and margins
coasting_plot <- df %>%
select(timestamps, coasting) %>%
na.omit() %>%
ggplot() +
geom_line(aes(x = timestamps, y = coasting), linewidth = 0.4, alpha = 0.75, color = "gray35") +
scale_x_continuous(breaks = seq(0, TIME_RANGE[2], by = 120), expand = c(0.01, 0.01), limits = TIME_RANGE) +
scale_y_discrete(breaks = seq(0, 1, by = 1), limits = c(0, 1), labels = c("off", "on")) +
labs(x = "Time [s]", y = "Coasting") +
theme_minimal() +
theme(
axis.title.x = element_text(margin = margin(t = 10, r = 0, b = 0, l = 0)),
axis.title.y = element_text(margin = margin(t = 0, r = 10, b = 0, l = 0)),
plot.margin = margin(t = 0, r = 5.5, b = 5.5, l = 5.5))
Warning: Continuous limits supplied to discrete scale.
ℹ Did you mean `limits = factor(...)` or `scale_*_continuous()`?
The plot for discrete coasting signal will be little bit different.
Firstly, we set the y axis scaling to discrete and provide labels for
the possible discrete values. Second, as this plot will be at the bottom
of the graph stack we let the x axis title and breaks to be drawn -
these will serve the entire plot
g1 <- ggplotGrob(speed_plot)
Warning: Removed 1 row containing missing values (`geom_line()`).
g2 <- ggplotGrob(oil_temp_plot)
Warning: Removed 15 rows containing missing values (`geom_line()`).
g3 <- ggplotGrob(amb_temp_plot)
Warning: Removed 1 row containing missing values (`geom_line()`).
g4 <- ggplotGrob(coasting_plot)
Warning: Removed 1 row containing missing values (`geom_line()`).
Our intention is to stack the plots on top of each other. We can use
a grid to do such layout for us - it could do also multicolumn plots but
here we just use one as a “stack”. In order to use grid, we’ll need to
convert the plots to grid graphical objects aka grobs.
maxWidth = grid::unit.pmax(g1$widths, g2$widths, g3$widths, g4$widths)
g1$widths <- as.list(maxWidth)
g2$widths <- as.list(maxWidth)
g3$widths <- as.list(maxWidth)
g4$widths <- as.list(maxWidth)
The grobs might get slightly differing widths. We want to have equal
widths in all grobs so that the breaks on time axis will align nicely
accross all plots. To do this, we can find the width of the widest grob
and force that to all grobs. Credit: https://gist.github.com/tomhopper/faa24797bb44addeba79
grid.arrange(
arrangeGrob(g1,g2,g3,g4,
ncol = 1,
heights = c(1,1,1,.5)
)
)

Now we can finally arrange the grobs to a grid, this will plot it
out. We can control the height of the each grob separately - here we
give 50% height to the coasting signal as it does not need as much
vertical real estate as the continuous signals do.
LS0tCnRpdGxlOiAiVGltZSBzZXJpZXMgZGF0YSBwbG90dGluZyIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKSW4gdGhpcyBub3RlYm9vaywgd2UnbGwgaW52ZXN0aWdhdGUgdGltZSBzZXJpZXMgcGxvdHRpbmcgaW4gUiB0byBhY2hpZXZlIGEgdmlzdWFsbHkgcGxlYXNpbmcgYW5kIHJldXNhYmxlIGhpZ2ggcXVhbGl0eSBwbG90IHRlbXBsYXRlIGZvciBvdGhlciBwbG90cyBpbiB0aGUgZnV0dXJlLiAKCkhlcmUsIHdlIGxvb2sgYXQgZW5naW5lIG9pbCB0ZW1wZXJhdHVyZSBvbiBhIHJlbGF0aXZlbHkgaG90IGRheSBydW5uaW5nIHRocm91Z2ggcGVyaW9kcyBvZiBiLXJvYWQgYW5kIG1vdG9yd2F5IGRyaXZpbmcuIFdlIGFsc28gaW5jbHVkZSBjb2FzdGluZyAob2ZmL29uKSBhcyBhbiBleGFtcGxlIG9mIGEgZGlzY3JldGUgc2lnbmFsIHBsb3R0aW5nLiBUaGUgbm90ZWJvb2sgaXMgYWJvdXQgcGxvdCB2aXN1YWxzIC0gaW4gb3JkZXIgdG8gcHJvcGVybHkgYXNzZXNzIG9pbCB0ZW1wZXJhdHVyZSwgYXQgbGVhc3QgZW5naW5lIGxvYWQgc2hvdWxkIGJlIGNvbnNpZGVyZWQgYXMgYSBmYWN0b3IuCgojIyBMaWJyYXJpZXMKYGBge3J9CmxpYnJhcnkodGlkeXZlcnNlKQpsaWJyYXJ5KGdyaWQpCmxpYnJhcnkoZ3JpZEV4dHJhKQpgYGAKRmlyc3QsIGxldCdzIGxvYWQgdGhlIGxpYnJhcmllcyB3ZSdyZSBnb2luZyB0byB1c2UuIFBsZWFzZSBub3RlIHRoYXQgcnVubmluZyB0aGUgbm90ZWJvb2sgaW4gUlN0dWRpbywgdGhlc2UgcGFja2FnZXMgaGF2ZSB0byBiZSBwcmVpbnN0YWxsZWQgd2l0aCBgcGFja2FnZXMuaW5zdGFsbCgiPHBhY2thZ2UtbmFtZT4iKWAuCgojIyBQcmVwYXJpbmcgZGF0YQpgYGB7cn0KY29hc3RpbmcgPC0gcmVhZF9jc3YoImhvdC5DaGFubmVsR3JvdXBfMF9DQU4xXy1fbWVzc2FnZV9kc2dfMTBoel8weDM1OS5jc3YiKQpvaWxfdGVtcCA8LSByZWFkX2NzdigiaG90LkNoYW5uZWxHcm91cF8zX0NBTjJfLV9tZXNzYWdlX2VuZ2luZV83XzUwaHpfMHg1ODguY3N2IikKc3BlZWQgPC0gcmVhZF9jc3YoImhvdC5DaGFubmVsR3JvdXBfMV9DQU4xXy1fbWVzc2FnZV9rb21iaV8xXzQwaHpfMHgzMjAuY3N2IikKYW1iaWVudF90ZW1wZXJhdHVyZSA8LSByZWFkX2NzdigiaG90LkNoYW5uZWxHcm91cF8yX0NBTjFfLV9tZXNzYWdlX21mZF81MGh6XzB4NTI3LmNzdiIpCmBgYApUaGVuLCB3ZSdsbCBsb2FkIHRoZSBkYXRhIENTVnMuIFRoZXNlIGFyZSBleHRyYWN0ZWQgZnJvbSBDQU4gZHVtcCBkYXRhIGFuZCBpbnRlcnBvbGF0ZWQgdG8gMC4xcyAtIGVhY2ggQ1NWIHdpbGwgdGh1cyBoYXZlIHNhbWUgdGltZXN0YW1wcyBhbmQgYSBzaW5nbGUgZGF0YSBjb2x1bW4uIFRoZSBDU1YgbmFtZXMgcmVwcmVzZW50IHRoZSBvcmlnaW5hdGluZyBjaGFubmVsIGdyb3VwIGFuZCB0aGVyZWZvcmUgdGhlIGFjdHVhbCBzaWduYWwgbmFtZSBpcyBub3QgcHJlc2VudCBpbiB0aGUgQ1NWIG5hbWUuIFRoaXMgYm9pbHMgZG93biB0byBob3cgb3RoZXIgdG9vbHMgZXhwb3J0IHRoZSBkYXRhIHJhdGhlciB0aGFuIGEgY29uc2Npb3VzIGNob2ljZS4gCgpgYGB7cn0KY29sbmFtZXMoY29hc3RpbmcpWzJdID0gImNvYXN0aW5nIgpjb2xuYW1lcyhvaWxfdGVtcClbMl0gPSAidF9vaWwiCmNvbG5hbWVzKGFtYmllbnRfdGVtcGVyYXR1cmUpWzJdID0gInRfYW1iIgpjb2xuYW1lcyhzcGVlZClbMl0gPSAic3BlZWQiCmBgYApUaGUgcmF3IGV4cG9ydCBDU1ZzIGhhZCBhIGxpdHRsZSBiaXQgdmVyYm9zZSBjb2x1bW4gbmFtZXMgc28gbGV0J3MgcmVuYW1lIHRoZSBjb2x1bW5zIHRvIG1vcmUgaHVtYW4gcmVhZGFibGUgZm9ybWF0CgpgYGB7cn0KZGZfbGlzdCA8LSBsaXN0KGNvYXN0aW5nLCBvaWxfdGVtcCwgYW1iaWVudF90ZW1wZXJhdHVyZSwgc3BlZWQpCmRmIDwtIGRmX2xpc3QgJT4lIHJlZHVjZShmdWxsX2pvaW4sIGJ5PSd0aW1lc3RhbXBzJykKaGVhZChkZikKYGBgCkFzIGFsbCBDU1ZzIGhhZCBzYW1lIHRpbWVzdGFtcHMgYW5kIHNhbWUgbnVtYmVyIG9mIHNhbXBsZXMgd2UgY2FuIGpvaW4gYWxsIHRoZSBkYXRhIGludG8gYSBzaW5nbGUgZGF0YWZyYW1lIGZvciBlYXNpZXIgaGFuZGxpbmcuIFRvIGRvIHRoaXMsIGxldCdzIGZpcnN0IGNvbnN0cnVjdCBhIGxpc3Qgb2YgYWxsIGRhdGFzZXRzIGFuZCB0aGVuIHBhc3MgdGhhdCB0byBgcmVkdWNlYCBmdW5jdGlvbiB0byBqb2luIHRoZSByb3dzIHRocm91Z2ggY29tbW9uIGNvbHVtbiBgdGltZXN0YW1wc2AuIFNob3dpbmcgdGhlIGZpcnN0IGZldyByb3dzIG9mIHRoZSByZXN1bHQgZGF0YWZyYW1lIHdlJ2xsIHNlZSBvaWwgdGVtcGVyYXR1cmUgc3RhcnRzIHdpdGggYSBjb29sIC01OSB2YWx1ZSBiZWZvcmUgdGhlIGlnbml0aW9uIGlzIG9uLiBSYXRoZXIgdGhhbiBmaXhpbmcgdGhpcyBpbiB0aGUgZGF0YSB3ZSdsbCBqdXN0IGZpbHRlciBpdCBvdXQgbGF0ZXIgYnkgbGltaXRpbmcgdGhlIGdyYXBoIGF4aXMuIAoKYGBge3J9CnRhaWwoZGYpClRJTUVfUkFOR0UgPSBjKDAsIDIxNTApCmBgYApCeSBsb29raW5nIGF0IHRoZSBsYXN0IGZldyByb3dzIG9mIHRoZSBkYXRhZnJhbWUgd2Ugc2VlIHRoZSB0cmlwIGxlbmd0aCB3YXMgMjE1MCBzZWNvbmRzLiBMZXQncyBkZWZpbmUgYSB0aW1lIHJhbmdlIG9mIGludGVyZXN0IGFzIGFuIGludGVnZXIgdmVjdG9yIC0gdGhpcyB3YXkgd2UgY2FuIGxhdGVyIGVhc2lseSB6b29tIHRoZSBncmFwaHMgdG8gYSBzcGVjaWZpYyB0aW1lIGZyYW1lLgoKIyMgUGxvdHRpbmcKCmBgYHtyfQpzcGVlZF9wbG90IDwtIGRmICU+JQogIHNlbGVjdCh0aW1lc3RhbXBzLCBzcGVlZCkgJT4lCiAgbmEub21pdCgpICU+JQogIGdncGxvdCgpICsKICBnZW9tX2xpbmUoYWVzKHggPSB0aW1lc3RhbXBzLCB5ID0gc3BlZWQpLCBsaW5ld2lkdGggPSAwLjQsIGFscGhhID0gMC43NSwgY29sb3IgPSAiZGVlcHNreWJsdWU0IikgKwogIHNjYWxlX3hfY29udGludW91cyhicmVha3MgPSBzZXEoMCwgVElNRV9SQU5HRVsyXSwgYnkgPSAxMjApLCBleHBhbmQgPSBjKDAuMDEsIDAuMDEpLCBsaW1pdHMgPSBUSU1FX1JBTkdFKSArCiAgc2NhbGVfeV9jb250aW51b3VzKGJyZWFrcyA9IHNlcSgwLCAxMzAsIGJ5ID0gMTApLCBsaW1pdHMgPSBjKDAsIDEzMCkpICsKICB5bGFiKCJTcGVlZCBba20vaF0iKSArCiAgdGhlbWVfbWluaW1hbCgpICsKICB0aGVtZSgKICAgIGF4aXMudGl0bGUueCA9IGVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0LnggPSBlbGVtZW50X2JsYW5rKCksCiAgICBheGlzLnRpdGxlLnkgPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAwLCByID0gMTAsIGIgPSAwLCBsID0gMCkpLAogICAgcGxvdC5tYXJnaW4gPSBtYXJnaW4odCA9IDUuNSwgciA9IDUuNSwgYiA9IDAsIGwgPSA1LjUpKQoKYGBgCkhhdmluZyBkYXRhZnJhbWUgcHJlcGFyZWQgd2UgY2FuIGNyZWF0ZSB0aGUgcGxvdHMgZm9yIGVhY2ggdmFyaWFibGUuIFRoZXJlJ3MgYSBsb3QgZ29pbmcgb24gaGVyZSBzbyBsZXQncyBicmVhayBkb3duIGEgcGxvdCBmb3IgdmVoaWNsZSBzcGVlZDoKCiogVGFrZSBhbGwgb2YgYSBkYXRhZnJhbWUgYW5kIHNlbGVjdCBgdGltZXN0YW1wYCwgYHNwZWVkYCB0dXBsZXMgCiogRmVlZCB0aGUgcmVzdWx0IG9mIHNlbGVjdGlvbiB0aHJvdWdoIG9taXQgZmlsdGVyIHRoYXQgaWdub3JlcyBjb2x1bW5zIHRoYXQgaGF2ZSBubyBzcGVjaWZpZWQgdmFsdWUKKiBGZWVkIHRoZSBmaWx0ZXJpbmcgcmVzdWx0IHRvIGEgbmV3IGBnZ3Bsb3QoKWAgb2JqZWN0CiogV2hpY2ggY29uc2lzdHMgb2YgYSBsaW5lIHBsb3QgaGF2aW5nIHRpbWVzdGFtcHMgYXMgeCBheGlzIGFuZCBzcGVlZCBhcyB5IGF4aXMgCiogWCBheGlzIGlzIGEgY29udGludW91cyBzaWduYWwgd2l0aCBicmVha3MgZXZlcnkgMTIwIHNlY29uZHMsIGxpbWl0ZWQgdG8gdGhlIGNvbmZpZ3VyZWQgZGF0YSByYW5nZSBhcyBvcHBvc2VkIHRvIGFsbCBvZiBkYXRhLCBhbmQgaXQgaGFzIDElIHBhZGRpbmcgb24gYm90aCBzaWRlcwoqIFkgYXhpcyBpcyBhIGNvbnRpbnVvdXMgc2lnbmFsIHdpdGggcmFuZ2UgMC4uMTMwIGFuZCBhIGJyZWFrIGV2ZXJ5IDEwIGttL2gKKiBUaGUgWSBheGlzIGxhYmVsIGlzIGBTcGVlZCBba20vaF1gCiogVGhlIHBsb3QgdXNlcyBhIG1pbmltYWwgdGhlbWUsIHRoaXMgYmFzaWNhbGx5IHJlc2V0cyBiYWNrZ3JvdW5kIGNvbG9ycyBldGMKKiBCdXQgdGhlbiB3ZSdsbCBvdmVycmlkZSBmZXcgdGhlbWUgc2V0dGluZ3MKICAqIHggYXhpcyB3aWxsIG5vdCBzaG93IHRpdGxlIG5vciBicmVhayB0ZXh0cyAKICAqIHkgYXhpcyBtYXJnaW5zIGFyZSBhZGp1c3RlZCB0byBwdXNoIHRpdGxlIGxpdHRsZSBiaXQgZnVydGhlciBmcm9tIGJyZWFrIHRleHRzCiAgKiBvdmVyYWxsIHBsb3QgbWFyZ2lucyBhcmUgYWRqdXN0ZWQgZm9yIHN0YWNraW5nIG11bHRpcGxlIHBsb3RzIG9uIHRvcCBvZiBlYWNoIG90aGVyCgpOb3RlIHdlbGwgc3RhcnQgdGhlIHggYnJlYWsgc2VxdWVuY2UgYWx3YXlzIGZyb20gemVyby4gVGhpcyBhbGxvd3MgdGltZSByYW5nZSB0byBzdGFydCBmcm9tIGFueSBzZWNvbmQgYnV0IGJyZWFrcyB3aWxsIHN0aWxsIGJlIHplcm8gLWJhc2VkLiAKCgpgYGB7cn0Kb2lsX3RlbXBfcGxvdCA8LSBkZiAlPiUKICBzZWxlY3QodGltZXN0YW1wcywgdF9vaWwpICU+JQogIG5hLm9taXQoKSAlPiUKICBnZ3Bsb3QoKSArCiAgZ2VvbV9saW5lKGFlcyh4ID0gdGltZXN0YW1wcywgeSA9IHRfb2lsKSwgbGluZXdpZHRoID0gMC40LCBhbHBoYSA9IDAuNzUsIGNvbG9yID0gImZpcmVicmljazQiKSArCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IHNlcSgwLCBUSU1FX1JBTkdFWzJdLCBieSA9IDEyMCksIGV4cGFuZCA9IGMoMC4wMSwgMC4wMSksIGxpbWl0cyA9IFRJTUVfUkFOR0UpICsKICBzY2FsZV95X2NvbnRpbnVvdXMoYnJlYWtzID0gc2VxKDcwLCAxMTAsIGJ5ID0gNSksIGxpbWl0cyA9IGMoNzAsIDExMCkpICsKICB5bGFiKCJPaWwgVGVtcGVyYXR1cmUgW0NdIikgKwogIHRoZW1lX21pbmltYWwoKSArCiAgdGhlbWUoCiAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X2JsYW5rKCksIGF4aXMudGV4dC54ID0gZWxlbWVudF9ibGFuaygpLAogICAgYXhpcy50aXRsZS55ID0gZWxlbWVudF90ZXh0KG1hcmdpbiA9IG1hcmdpbih0ID0gMCwgciA9IDEwLCBiID0gMCwgbCA9IDApKSwKICAgIHBsb3QubWFyZ2luID0gbWFyZ2luKHQgPSAwLCByID0gNS41LCBiID0gMCwgbCA9IDUuNSkpCmBgYAoKYGBge3J9CmFtYl90ZW1wX3Bsb3QgPC0gZGYgJT4lCiAgc2VsZWN0KHRpbWVzdGFtcHMsIHRfYW1iKSAlPiUKICBuYS5vbWl0KCkgJT4lCiAgZ2dwbG90KCkgKwogIGdlb21fbGluZShhZXMoeCA9IHRpbWVzdGFtcHMsIHkgPSB0X2FtYiksIGxpbmV3aWR0aCA9IDAuNCwgYWxwaGEgPSAwLjc1LCBjb2xvciA9ICJkYXJrZ3JlZW4iKSArCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IHNlcSgwLCBUSU1FX1JBTkdFWzJdLCBieSA9IDEyMCksIGV4cGFuZCA9IGMoMC4wMSwgMC4wMSksIGxpbWl0cyA9IFRJTUVfUkFOR0UpICsKICBzY2FsZV95X2NvbnRpbnVvdXMoYnJlYWtzID0gc2VxKDIyLCAzMiwgYnkgPSAxKSwgbGltaXRzID0gYygyMiwgMzIpKSArCiAgeWxhYigiQW1iaWVudCBUZW1wZXJhdHVyZSBbQ10iKSArCiAgdGhlbWVfbWluaW1hbCgpICsKICB0aGVtZSgKICAgIGF4aXMudGl0bGUueCA9IGVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0LnggPSBlbGVtZW50X2JsYW5rKCksCiAgICBheGlzLnRpdGxlLnkgPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAwLCByID0gMTAsIGIgPSAwLCBsID0gMCkpLAogICAgcGxvdC5tYXJnaW4gPSBtYXJnaW4odCA9IDAsIHIgPSA1LjUsIGIgPSAwLCBsID0gNS41KSkKYGBgClRoZSBwbG90cyBmb3Igb2lsIHRlbXBlcmF0dXJlIGFuZCBhbWJpZW50IHRlbXBlcmF0dXJlIHdpbGwgYmUgc2ltaWxhciwgb25seSBjaGFuZ2luZyBpbiBjb2xvciwgeSBheGlzIHJhbmdlLCB0aXRsZSBhbmQgbWFyZ2lucwoKCmBgYHtyfQpjb2FzdGluZ19wbG90IDwtIGRmICU+JQogIHNlbGVjdCh0aW1lc3RhbXBzLCBjb2FzdGluZykgJT4lCiAgbmEub21pdCgpICU+JQogIGdncGxvdCgpICsKICBnZW9tX2xpbmUoYWVzKHggPSB0aW1lc3RhbXBzLCB5ID0gY29hc3RpbmcpLCBsaW5ld2lkdGggPSAwLjQsIGFscGhhID0gMC43NSwgY29sb3IgPSAiZ3JheTM1IikgKwogIHNjYWxlX3hfY29udGludW91cyhicmVha3MgPSBzZXEoMCwgVElNRV9SQU5HRVsyXSwgYnkgPSAxMjApLCBleHBhbmQgPSBjKDAuMDEsIDAuMDEpLCBsaW1pdHMgPSBUSU1FX1JBTkdFKSArCiAgc2NhbGVfeV9kaXNjcmV0ZShicmVha3MgPSBzZXEoMCwgMSwgYnkgPSAxKSwgbGltaXRzID0gYygwLCAxKSwgbGFiZWxzID0gYygib2ZmIiwgIm9uIikpICsKICBsYWJzKHggPSAiVGltZSBbc10iLCB5ID0gIkNvYXN0aW5nIikgKwogIHRoZW1lX21pbmltYWwoKSArCiAgdGhlbWUoCiAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAxMCwgciA9IDAsIGIgPSAwLCBsID0gMCkpLAogICAgYXhpcy50aXRsZS55ID0gZWxlbWVudF90ZXh0KG1hcmdpbiA9IG1hcmdpbih0ID0gMCwgciA9IDEwLCBiID0gMCwgbCA9IDApKSwKICAgIHBsb3QubWFyZ2luID0gbWFyZ2luKHQgPSAwLCByID0gNS41LCBiID0gNS41LCBsID0gNS41KSkKYGBgClRoZSBwbG90IGZvciBkaXNjcmV0ZSBjb2FzdGluZyBzaWduYWwgd2lsbCBiZSBsaXR0bGUgYml0IGRpZmZlcmVudC4gRmlyc3RseSwgd2Ugc2V0IHRoZSB5IGF4aXMgc2NhbGluZyB0byBkaXNjcmV0ZSBhbmQgcHJvdmlkZSBsYWJlbHMgZm9yIHRoZSBwb3NzaWJsZSBkaXNjcmV0ZSB2YWx1ZXMuIFNlY29uZCwgYXMgdGhpcyBwbG90IHdpbGwgYmUgYXQgdGhlIGJvdHRvbSBvZiB0aGUgZ3JhcGggc3RhY2sgd2UgbGV0IHRoZSB4IGF4aXMgdGl0bGUgYW5kIGJyZWFrcyB0byBiZSBkcmF3biAtIHRoZXNlIHdpbGwgc2VydmUgdGhlIGVudGlyZSBwbG90CgoKYGBge3J9CmcxIDwtIGdncGxvdEdyb2Ioc3BlZWRfcGxvdCkKZzIgPC0gZ2dwbG90R3JvYihvaWxfdGVtcF9wbG90KQpnMyA8LSBnZ3Bsb3RHcm9iKGFtYl90ZW1wX3Bsb3QpCmc0IDwtIGdncGxvdEdyb2IoY29hc3RpbmdfcGxvdCkKYGBgCk91ciBpbnRlbnRpb24gaXMgdG8gc3RhY2sgdGhlIHBsb3RzIG9uIHRvcCBvZiBlYWNoIG90aGVyLiBXZSBjYW4gdXNlIGEgZ3JpZCB0byBkbyBzdWNoIGxheW91dCBmb3IgdXMgLSBpdCBjb3VsZCBkbyBhbHNvIG11bHRpY29sdW1uIHBsb3RzIGJ1dCBoZXJlIHdlIGp1c3QgdXNlIG9uZSBhcyBhICJzdGFjayIuIEluIG9yZGVyIHRvIHVzZSBncmlkLCB3ZSdsbCBuZWVkIHRvIGNvbnZlcnQgdGhlIHBsb3RzIHRvIGdyaWQgZ3JhcGhpY2FsIG9iamVjdHMgYWthIGdyb2JzLgoKCmBgYHtyfQptYXhXaWR0aCA9IGdyaWQ6OnVuaXQucG1heChnMSR3aWR0aHMsIGcyJHdpZHRocywgZzMkd2lkdGhzLCBnNCR3aWR0aHMpCmcxJHdpZHRocyA8LSBhcy5saXN0KG1heFdpZHRoKQpnMiR3aWR0aHMgPC0gYXMubGlzdChtYXhXaWR0aCkKZzMkd2lkdGhzIDwtIGFzLmxpc3QobWF4V2lkdGgpCmc0JHdpZHRocyA8LSBhcy5saXN0KG1heFdpZHRoKQpgYGAKVGhlIGdyb2JzIG1pZ2h0IGdldCBzbGlnaHRseSBkaWZmZXJpbmcgd2lkdGhzLiBXZSB3YW50IHRvIGhhdmUgZXF1YWwgd2lkdGhzIGluIGFsbCBncm9icyBzbyB0aGF0IHRoZSBicmVha3Mgb24gdGltZSBheGlzIHdpbGwgYWxpZ24gbmljZWx5IGFjY3Jvc3MgYWxsIHBsb3RzLiBUbyBkbyB0aGlzLCB3ZSBjYW4gZmluZCB0aGUgd2lkdGggb2YgdGhlIHdpZGVzdCBncm9iIGFuZCBmb3JjZSB0aGF0IHRvIGFsbCBncm9icy4gQ3JlZGl0OiBodHRwczovL2dpc3QuZ2l0aHViLmNvbS90b21ob3BwZXIvZmFhMjQ3OTdiYjQ0YWRkZWJhNzkKCgpgYGB7ciwgZmlnLmhlaWdodD04fQpncmlkLmFycmFuZ2UoCiAgYXJyYW5nZUdyb2IoZzEsZzIsZzMsZzQsCiAgICBuY29sID0gMSwKICAgIGhlaWdodHMgPSBjKDEsMSwxLC41KQogICkKKQpgYGAKTm93IHdlIGNhbiBmaW5hbGx5IGFycmFuZ2UgdGhlIGdyb2JzIHRvIGEgZ3JpZCwgdGhpcyB3aWxsIHBsb3QgaXQgb3V0LiBXZSBjYW4gY29udHJvbCB0aGUgaGVpZ2h0IG9mIHRoZSBlYWNoIGdyb2Igc2VwYXJhdGVseSAtIGhlcmUgd2UgZ2l2ZSA1MCUgaGVpZ2h0IHRvIHRoZSBjb2FzdGluZyBzaWduYWwgYXMgaXQgZG9lcyBub3QgbmVlZCBhcyBtdWNoIHZlcnRpY2FsIHJlYWwgZXN0YXRlIGFzIHRoZSBjb250aW51b3VzIHNpZ25hbHMgZG8uIAoKIyMgQ29uY2x1c2lvbnMKCiogVXNpbmcgZ2dwbG90MiB3aXRoIGdyaWRzIGNhbiB5aWVsZCBoaWdoIHF1YWxpdHkgdGltZSBzZXJpZXMgcGxvdHMgbWl4aW5nIGNvbnRpbnVvdXMgYW5kIGRpc2NyZXRlIHNpZ25hbHMKKiBJdCdzIGxpdHRsZSBiaXQgdGVkaW91cyB0byBnZXQgdGhlIHBsb3RzIHN0YWNrZWQsIGFsaWduZWQgYW5kIHNjYWxlZCAKKiBWZXJ0aWNhbCByZWFsIGVzdGF0ZSBpcyBzY2FyY2UgLSBtaXhpbmcgbW9yZSBzaWduYWxzIHdpbGwgY2F1c2Ugdmlld2VycyBoYXZpbmcgaGFyZCB0aW1lIHRvIHNjcm9sbCB0byBzZWUgdGltZSBheGlzCiogQXQgcHJlc2VudCwgbm8ga25vd24gZWFzeSB3YXkgdG8gb3ZlcmxheSBzZXZlcmFsIGRpc2NyZXRlIHNpZ25hbHMgb24gdG9wIG9mIGNvbnRpbnVvdXMgc2lnbmFscyAtIHRoaXMgd291bGQgc2F2ZSB2ZXJ0aWNhbCBzcGFjZSB3aXRob3V0IGEgcmVhbCBkYW5nZXIgb2YgY29uZnVzaW5nIHZpZXdlcnMKCgo=