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.

Libraries

library(tidyverse)
── Attaching core tidyverse packages ───────────────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.2     ✔ readr     2.1.4
✔ forcats   1.0.0     ✔ stringr   1.5.0
✔ ggplot2   3.4.2     ✔ tibble    3.2.1
✔ lubridate 1.9.2     ✔ tidyr     1.3.0
✔ purrr     1.0.1     ── Conflicts ─────────────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the ]8;;http://conflicted.r-lib.org/conflicted package]8;; to force all conflicts to become errors
library(grid)
library(gridExtra)

Attaching package: ‘gridExtra’

The following object is masked from ‘package:dplyr’:

    combine

First, let’s load the libraries we’re going to use. Please note that running the notebook in RStudio, these packages have to be preinstalled with packages.install("<package-name>").

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)
ABCDEFGHIJ0123456789
timestamps
<dbl>
coasting
<dbl>
t_oil
<dbl>
t_amb
<dbl>
speed
<dbl>
0.00-5928.50
0.10-5928.50
0.20-5928.50
0.30-5928.50
0.40-5928.50
0.50-5928.50

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.

tail(df)
ABCDEFGHIJ0123456789
timestamps
<dbl>
coasting
<dbl>
t_oil
<dbl>
t_amb
<dbl>
speed
<dbl>
2149.5010222.50
2149.6010222.50
2149.7010222.50
2149.8010222.50
2149.9010222.50
2150.0010222.50
TIME_RANGE = c(0, 2150)

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:

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.

Conclusions

LS0tCnRpdGxlOiAiVGltZSBzZXJpZXMgZGF0YSBwbG90dGluZyIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKSW4gdGhpcyBub3RlYm9vaywgd2UnbGwgaW52ZXN0aWdhdGUgdGltZSBzZXJpZXMgcGxvdHRpbmcgaW4gUiB0byBhY2hpZXZlIGEgdmlzdWFsbHkgcGxlYXNpbmcgYW5kIHJldXNhYmxlIGhpZ2ggcXVhbGl0eSBwbG90IHRlbXBsYXRlIGZvciBvdGhlciBwbG90cyBpbiB0aGUgZnV0dXJlLiAKCkhlcmUsIHdlIGxvb2sgYXQgZW5naW5lIG9pbCB0ZW1wZXJhdHVyZSBvbiBhIHJlbGF0aXZlbHkgaG90IGRheSBydW5uaW5nIHRocm91Z2ggcGVyaW9kcyBvZiBiLXJvYWQgYW5kIG1vdG9yd2F5IGRyaXZpbmcuIFdlIGFsc28gaW5jbHVkZSBjb2FzdGluZyAob2ZmL29uKSBhcyBhbiBleGFtcGxlIG9mIGEgZGlzY3JldGUgc2lnbmFsIHBsb3R0aW5nLiBUaGUgbm90ZWJvb2sgaXMgYWJvdXQgcGxvdCB2aXN1YWxzIC0gaW4gb3JkZXIgdG8gcHJvcGVybHkgYXNzZXNzIG9pbCB0ZW1wZXJhdHVyZSwgYXQgbGVhc3QgZW5naW5lIGxvYWQgc2hvdWxkIGJlIGNvbnNpZGVyZWQgYXMgYSBmYWN0b3IuCgojIyBMaWJyYXJpZXMKYGBge3J9CmxpYnJhcnkodGlkeXZlcnNlKQpsaWJyYXJ5KGdyaWQpCmxpYnJhcnkoZ3JpZEV4dHJhKQpgYGAKRmlyc3QsIGxldCdzIGxvYWQgdGhlIGxpYnJhcmllcyB3ZSdyZSBnb2luZyB0byB1c2UuIFBsZWFzZSBub3RlIHRoYXQgcnVubmluZyB0aGUgbm90ZWJvb2sgaW4gUlN0dWRpbywgdGhlc2UgcGFja2FnZXMgaGF2ZSB0byBiZSBwcmVpbnN0YWxsZWQgd2l0aCBgcGFja2FnZXMuaW5zdGFsbCgiPHBhY2thZ2UtbmFtZT4iKWAuCgojIyBQcmVwYXJpbmcgZGF0YQpgYGB7cn0KY29hc3RpbmcgPC0gcmVhZF9jc3YoImhvdC5DaGFubmVsR3JvdXBfMF9DQU4xXy1fbWVzc2FnZV9kc2dfMTBoel8weDM1OS5jc3YiKQpvaWxfdGVtcCA8LSByZWFkX2NzdigiaG90LkNoYW5uZWxHcm91cF8zX0NBTjJfLV9tZXNzYWdlX2VuZ2luZV83XzUwaHpfMHg1ODguY3N2IikKc3BlZWQgPC0gcmVhZF9jc3YoImhvdC5DaGFubmVsR3JvdXBfMV9DQU4xXy1fbWVzc2FnZV9rb21iaV8xXzQwaHpfMHgzMjAuY3N2IikKYW1iaWVudF90ZW1wZXJhdHVyZSA8LSByZWFkX2NzdigiaG90LkNoYW5uZWxHcm91cF8yX0NBTjFfLV9tZXNzYWdlX21mZF81MGh6XzB4NTI3LmNzdiIpCmBgYApUaGVuLCB3ZSdsbCBsb2FkIHRoZSBkYXRhIENTVnMuIFRoZXNlIGFyZSBleHRyYWN0ZWQgZnJvbSBDQU4gZHVtcCBkYXRhIGFuZCBpbnRlcnBvbGF0ZWQgdG8gMC4xcyAtIGVhY2ggQ1NWIHdpbGwgdGh1cyBoYXZlIHNhbWUgdGltZXN0YW1wcyBhbmQgYSBzaW5nbGUgZGF0YSBjb2x1bW4uIFRoZSBDU1YgbmFtZXMgcmVwcmVzZW50IHRoZSBvcmlnaW5hdGluZyBjaGFubmVsIGdyb3VwIGFuZCB0aGVyZWZvcmUgdGhlIGFjdHVhbCBzaWduYWwgbmFtZSBpcyBub3QgcHJlc2VudCBpbiB0aGUgQ1NWIG5hbWUuIFRoaXMgYm9pbHMgZG93biB0byBob3cgb3RoZXIgdG9vbHMgZXhwb3J0IHRoZSBkYXRhIHJhdGhlciB0aGFuIGEgY29uc2Npb3VzIGNob2ljZS4gCgpgYGB7cn0KY29sbmFtZXMoY29hc3RpbmcpWzJdID0gImNvYXN0aW5nIgpjb2xuYW1lcyhvaWxfdGVtcClbMl0gPSAidF9vaWwiCmNvbG5hbWVzKGFtYmllbnRfdGVtcGVyYXR1cmUpWzJdID0gInRfYW1iIgpjb2xuYW1lcyhzcGVlZClbMl0gPSAic3BlZWQiCmBgYApUaGUgcmF3IGV4cG9ydCBDU1ZzIGhhZCBhIGxpdHRsZSBiaXQgdmVyYm9zZSBjb2x1bW4gbmFtZXMgc28gbGV0J3MgcmVuYW1lIHRoZSBjb2x1bW5zIHRvIG1vcmUgaHVtYW4gcmVhZGFibGUgZm9ybWF0CgpgYGB7cn0KZGZfbGlzdCA8LSBsaXN0KGNvYXN0aW5nLCBvaWxfdGVtcCwgYW1iaWVudF90ZW1wZXJhdHVyZSwgc3BlZWQpCmRmIDwtIGRmX2xpc3QgJT4lIHJlZHVjZShmdWxsX2pvaW4sIGJ5PSd0aW1lc3RhbXBzJykKaGVhZChkZikKYGBgCkFzIGFsbCBDU1ZzIGhhZCBzYW1lIHRpbWVzdGFtcHMgYW5kIHNhbWUgbnVtYmVyIG9mIHNhbXBsZXMgd2UgY2FuIGpvaW4gYWxsIHRoZSBkYXRhIGludG8gYSBzaW5nbGUgZGF0YWZyYW1lIGZvciBlYXNpZXIgaGFuZGxpbmcuIFRvIGRvIHRoaXMsIGxldCdzIGZpcnN0IGNvbnN0cnVjdCBhIGxpc3Qgb2YgYWxsIGRhdGFzZXRzIGFuZCB0aGVuIHBhc3MgdGhhdCB0byBgcmVkdWNlYCBmdW5jdGlvbiB0byBqb2luIHRoZSByb3dzIHRocm91Z2ggY29tbW9uIGNvbHVtbiBgdGltZXN0YW1wc2AuIFNob3dpbmcgdGhlIGZpcnN0IGZldyByb3dzIG9mIHRoZSByZXN1bHQgZGF0YWZyYW1lIHdlJ2xsIHNlZSBvaWwgdGVtcGVyYXR1cmUgc3RhcnRzIHdpdGggYSBjb29sIC01OSB2YWx1ZSBiZWZvcmUgdGhlIGlnbml0aW9uIGlzIG9uLiBSYXRoZXIgdGhhbiBmaXhpbmcgdGhpcyBpbiB0aGUgZGF0YSB3ZSdsbCBqdXN0IGZpbHRlciBpdCBvdXQgbGF0ZXIgYnkgbGltaXRpbmcgdGhlIGdyYXBoIGF4aXMuIAoKYGBge3J9CnRhaWwoZGYpClRJTUVfUkFOR0UgPSBjKDAsIDIxNTApCmBgYApCeSBsb29raW5nIGF0IHRoZSBsYXN0IGZldyByb3dzIG9mIHRoZSBkYXRhZnJhbWUgd2Ugc2VlIHRoZSB0cmlwIGxlbmd0aCB3YXMgMjE1MCBzZWNvbmRzLiBMZXQncyBkZWZpbmUgYSB0aW1lIHJhbmdlIG9mIGludGVyZXN0IGFzIGFuIGludGVnZXIgdmVjdG9yIC0gdGhpcyB3YXkgd2UgY2FuIGxhdGVyIGVhc2lseSB6b29tIHRoZSBncmFwaHMgdG8gYSBzcGVjaWZpYyB0aW1lIGZyYW1lLgoKIyMgUGxvdHRpbmcKCmBgYHtyfQpzcGVlZF9wbG90IDwtIGRmICU+JQogIHNlbGVjdCh0aW1lc3RhbXBzLCBzcGVlZCkgJT4lCiAgbmEub21pdCgpICU+JQogIGdncGxvdCgpICsKICBnZW9tX2xpbmUoYWVzKHggPSB0aW1lc3RhbXBzLCB5ID0gc3BlZWQpLCBsaW5ld2lkdGggPSAwLjQsIGFscGhhID0gMC43NSwgY29sb3IgPSAiZGVlcHNreWJsdWU0IikgKwogIHNjYWxlX3hfY29udGludW91cyhicmVha3MgPSBzZXEoMCwgVElNRV9SQU5HRVsyXSwgYnkgPSAxMjApLCBleHBhbmQgPSBjKDAuMDEsIDAuMDEpLCBsaW1pdHMgPSBUSU1FX1JBTkdFKSArCiAgc2NhbGVfeV9jb250aW51b3VzKGJyZWFrcyA9IHNlcSgwLCAxMzAsIGJ5ID0gMTApLCBsaW1pdHMgPSBjKDAsIDEzMCkpICsKICB5bGFiKCJTcGVlZCBba20vaF0iKSArCiAgdGhlbWVfbWluaW1hbCgpICsKICB0aGVtZSgKICAgIGF4aXMudGl0bGUueCA9IGVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0LnggPSBlbGVtZW50X2JsYW5rKCksCiAgICBheGlzLnRpdGxlLnkgPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAwLCByID0gMTAsIGIgPSAwLCBsID0gMCkpLAogICAgcGxvdC5tYXJnaW4gPSBtYXJnaW4odCA9IDUuNSwgciA9IDUuNSwgYiA9IDAsIGwgPSA1LjUpKQoKYGBgCkhhdmluZyBkYXRhZnJhbWUgcHJlcGFyZWQgd2UgY2FuIGNyZWF0ZSB0aGUgcGxvdHMgZm9yIGVhY2ggdmFyaWFibGUuIFRoZXJlJ3MgYSBsb3QgZ29pbmcgb24gaGVyZSBzbyBsZXQncyBicmVhayBkb3duIGEgcGxvdCBmb3IgdmVoaWNsZSBzcGVlZDoKCiogVGFrZSBhbGwgb2YgYSBkYXRhZnJhbWUgYW5kIHNlbGVjdCBgdGltZXN0YW1wYCwgYHNwZWVkYCB0dXBsZXMgCiogRmVlZCB0aGUgcmVzdWx0IG9mIHNlbGVjdGlvbiB0aHJvdWdoIG9taXQgZmlsdGVyIHRoYXQgaWdub3JlcyBjb2x1bW5zIHRoYXQgaGF2ZSBubyBzcGVjaWZpZWQgdmFsdWUKKiBGZWVkIHRoZSBmaWx0ZXJpbmcgcmVzdWx0IHRvIGEgbmV3IGBnZ3Bsb3QoKWAgb2JqZWN0CiogV2hpY2ggY29uc2lzdHMgb2YgYSBsaW5lIHBsb3QgaGF2aW5nIHRpbWVzdGFtcHMgYXMgeCBheGlzIGFuZCBzcGVlZCBhcyB5IGF4aXMgCiogWCBheGlzIGlzIGEgY29udGludW91cyBzaWduYWwgd2l0aCBicmVha3MgZXZlcnkgMTIwIHNlY29uZHMsIGxpbWl0ZWQgdG8gdGhlIGNvbmZpZ3VyZWQgZGF0YSByYW5nZSBhcyBvcHBvc2VkIHRvIGFsbCBvZiBkYXRhLCBhbmQgaXQgaGFzIDElIHBhZGRpbmcgb24gYm90aCBzaWRlcwoqIFkgYXhpcyBpcyBhIGNvbnRpbnVvdXMgc2lnbmFsIHdpdGggcmFuZ2UgMC4uMTMwIGFuZCBhIGJyZWFrIGV2ZXJ5IDEwIGttL2gKKiBUaGUgWSBheGlzIGxhYmVsIGlzIGBTcGVlZCBba20vaF1gCiogVGhlIHBsb3QgdXNlcyBhIG1pbmltYWwgdGhlbWUsIHRoaXMgYmFzaWNhbGx5IHJlc2V0cyBiYWNrZ3JvdW5kIGNvbG9ycyBldGMKKiBCdXQgdGhlbiB3ZSdsbCBvdmVycmlkZSBmZXcgdGhlbWUgc2V0dGluZ3MKICAqIHggYXhpcyB3aWxsIG5vdCBzaG93IHRpdGxlIG5vciBicmVhayB0ZXh0cyAKICAqIHkgYXhpcyBtYXJnaW5zIGFyZSBhZGp1c3RlZCB0byBwdXNoIHRpdGxlIGxpdHRsZSBiaXQgZnVydGhlciBmcm9tIGJyZWFrIHRleHRzCiAgKiBvdmVyYWxsIHBsb3QgbWFyZ2lucyBhcmUgYWRqdXN0ZWQgZm9yIHN0YWNraW5nIG11bHRpcGxlIHBsb3RzIG9uIHRvcCBvZiBlYWNoIG90aGVyCgpOb3RlIHdlbGwgc3RhcnQgdGhlIHggYnJlYWsgc2VxdWVuY2UgYWx3YXlzIGZyb20gemVyby4gVGhpcyBhbGxvd3MgdGltZSByYW5nZSB0byBzdGFydCBmcm9tIGFueSBzZWNvbmQgYnV0IGJyZWFrcyB3aWxsIHN0aWxsIGJlIHplcm8gLWJhc2VkLiAKCgpgYGB7cn0Kb2lsX3RlbXBfcGxvdCA8LSBkZiAlPiUKICBzZWxlY3QodGltZXN0YW1wcywgdF9vaWwpICU+JQogIG5hLm9taXQoKSAlPiUKICBnZ3Bsb3QoKSArCiAgZ2VvbV9saW5lKGFlcyh4ID0gdGltZXN0YW1wcywgeSA9IHRfb2lsKSwgbGluZXdpZHRoID0gMC40LCBhbHBoYSA9IDAuNzUsIGNvbG9yID0gImZpcmVicmljazQiKSArCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IHNlcSgwLCBUSU1FX1JBTkdFWzJdLCBieSA9IDEyMCksIGV4cGFuZCA9IGMoMC4wMSwgMC4wMSksIGxpbWl0cyA9IFRJTUVfUkFOR0UpICsKICBzY2FsZV95X2NvbnRpbnVvdXMoYnJlYWtzID0gc2VxKDcwLCAxMTAsIGJ5ID0gNSksIGxpbWl0cyA9IGMoNzAsIDExMCkpICsKICB5bGFiKCJPaWwgVGVtcGVyYXR1cmUgW0NdIikgKwogIHRoZW1lX21pbmltYWwoKSArCiAgdGhlbWUoCiAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X2JsYW5rKCksIGF4aXMudGV4dC54ID0gZWxlbWVudF9ibGFuaygpLAogICAgYXhpcy50aXRsZS55ID0gZWxlbWVudF90ZXh0KG1hcmdpbiA9IG1hcmdpbih0ID0gMCwgciA9IDEwLCBiID0gMCwgbCA9IDApKSwKICAgIHBsb3QubWFyZ2luID0gbWFyZ2luKHQgPSAwLCByID0gNS41LCBiID0gMCwgbCA9IDUuNSkpCmBgYAoKYGBge3J9CmFtYl90ZW1wX3Bsb3QgPC0gZGYgJT4lCiAgc2VsZWN0KHRpbWVzdGFtcHMsIHRfYW1iKSAlPiUKICBuYS5vbWl0KCkgJT4lCiAgZ2dwbG90KCkgKwogIGdlb21fbGluZShhZXMoeCA9IHRpbWVzdGFtcHMsIHkgPSB0X2FtYiksIGxpbmV3aWR0aCA9IDAuNCwgYWxwaGEgPSAwLjc1LCBjb2xvciA9ICJkYXJrZ3JlZW4iKSArCiAgc2NhbGVfeF9jb250aW51b3VzKGJyZWFrcyA9IHNlcSgwLCBUSU1FX1JBTkdFWzJdLCBieSA9IDEyMCksIGV4cGFuZCA9IGMoMC4wMSwgMC4wMSksIGxpbWl0cyA9IFRJTUVfUkFOR0UpICsKICBzY2FsZV95X2NvbnRpbnVvdXMoYnJlYWtzID0gc2VxKDIyLCAzMiwgYnkgPSAxKSwgbGltaXRzID0gYygyMiwgMzIpKSArCiAgeWxhYigiQW1iaWVudCBUZW1wZXJhdHVyZSBbQ10iKSArCiAgdGhlbWVfbWluaW1hbCgpICsKICB0aGVtZSgKICAgIGF4aXMudGl0bGUueCA9IGVsZW1lbnRfYmxhbmsoKSwgYXhpcy50ZXh0LnggPSBlbGVtZW50X2JsYW5rKCksCiAgICBheGlzLnRpdGxlLnkgPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAwLCByID0gMTAsIGIgPSAwLCBsID0gMCkpLAogICAgcGxvdC5tYXJnaW4gPSBtYXJnaW4odCA9IDAsIHIgPSA1LjUsIGIgPSAwLCBsID0gNS41KSkKYGBgClRoZSBwbG90cyBmb3Igb2lsIHRlbXBlcmF0dXJlIGFuZCBhbWJpZW50IHRlbXBlcmF0dXJlIHdpbGwgYmUgc2ltaWxhciwgb25seSBjaGFuZ2luZyBpbiBjb2xvciwgeSBheGlzIHJhbmdlLCB0aXRsZSBhbmQgbWFyZ2lucwoKCmBgYHtyfQpjb2FzdGluZ19wbG90IDwtIGRmICU+JQogIHNlbGVjdCh0aW1lc3RhbXBzLCBjb2FzdGluZykgJT4lCiAgbmEub21pdCgpICU+JQogIGdncGxvdCgpICsKICBnZW9tX2xpbmUoYWVzKHggPSB0aW1lc3RhbXBzLCB5ID0gY29hc3RpbmcpLCBsaW5ld2lkdGggPSAwLjQsIGFscGhhID0gMC43NSwgY29sb3IgPSAiZ3JheTM1IikgKwogIHNjYWxlX3hfY29udGludW91cyhicmVha3MgPSBzZXEoMCwgVElNRV9SQU5HRVsyXSwgYnkgPSAxMjApLCBleHBhbmQgPSBjKDAuMDEsIDAuMDEpLCBsaW1pdHMgPSBUSU1FX1JBTkdFKSArCiAgc2NhbGVfeV9kaXNjcmV0ZShicmVha3MgPSBzZXEoMCwgMSwgYnkgPSAxKSwgbGltaXRzID0gYygwLCAxKSwgbGFiZWxzID0gYygib2ZmIiwgIm9uIikpICsKICBsYWJzKHggPSAiVGltZSBbc10iLCB5ID0gIkNvYXN0aW5nIikgKwogIHRoZW1lX21pbmltYWwoKSArCiAgdGhlbWUoCiAgICBheGlzLnRpdGxlLnggPSBlbGVtZW50X3RleHQobWFyZ2luID0gbWFyZ2luKHQgPSAxMCwgciA9IDAsIGIgPSAwLCBsID0gMCkpLAogICAgYXhpcy50aXRsZS55ID0gZWxlbWVudF90ZXh0KG1hcmdpbiA9IG1hcmdpbih0ID0gMCwgciA9IDEwLCBiID0gMCwgbCA9IDApKSwKICAgIHBsb3QubWFyZ2luID0gbWFyZ2luKHQgPSAwLCByID0gNS41LCBiID0gNS41LCBsID0gNS41KSkKYGBgClRoZSBwbG90IGZvciBkaXNjcmV0ZSBjb2FzdGluZyBzaWduYWwgd2lsbCBiZSBsaXR0bGUgYml0IGRpZmZlcmVudC4gRmlyc3RseSwgd2Ugc2V0IHRoZSB5IGF4aXMgc2NhbGluZyB0byBkaXNjcmV0ZSBhbmQgcHJvdmlkZSBsYWJlbHMgZm9yIHRoZSBwb3NzaWJsZSBkaXNjcmV0ZSB2YWx1ZXMuIFNlY29uZCwgYXMgdGhpcyBwbG90IHdpbGwgYmUgYXQgdGhlIGJvdHRvbSBvZiB0aGUgZ3JhcGggc3RhY2sgd2UgbGV0IHRoZSB4IGF4aXMgdGl0bGUgYW5kIGJyZWFrcyB0byBiZSBkcmF3biAtIHRoZXNlIHdpbGwgc2VydmUgdGhlIGVudGlyZSBwbG90CgoKYGBge3J9CmcxIDwtIGdncGxvdEdyb2Ioc3BlZWRfcGxvdCkKZzIgPC0gZ2dwbG90R3JvYihvaWxfdGVtcF9wbG90KQpnMyA8LSBnZ3Bsb3RHcm9iKGFtYl90ZW1wX3Bsb3QpCmc0IDwtIGdncGxvdEdyb2IoY29hc3RpbmdfcGxvdCkKYGBgCk91ciBpbnRlbnRpb24gaXMgdG8gc3RhY2sgdGhlIHBsb3RzIG9uIHRvcCBvZiBlYWNoIG90aGVyLiBXZSBjYW4gdXNlIGEgZ3JpZCB0byBkbyBzdWNoIGxheW91dCBmb3IgdXMgLSBpdCBjb3VsZCBkbyBhbHNvIG11bHRpY29sdW1uIHBsb3RzIGJ1dCBoZXJlIHdlIGp1c3QgdXNlIG9uZSBhcyBhICJzdGFjayIuIEluIG9yZGVyIHRvIHVzZSBncmlkLCB3ZSdsbCBuZWVkIHRvIGNvbnZlcnQgdGhlIHBsb3RzIHRvIGdyaWQgZ3JhcGhpY2FsIG9iamVjdHMgYWthIGdyb2JzLgoKCmBgYHtyfQptYXhXaWR0aCA9IGdyaWQ6OnVuaXQucG1heChnMSR3aWR0aHMsIGcyJHdpZHRocywgZzMkd2lkdGhzLCBnNCR3aWR0aHMpCmcxJHdpZHRocyA8LSBhcy5saXN0KG1heFdpZHRoKQpnMiR3aWR0aHMgPC0gYXMubGlzdChtYXhXaWR0aCkKZzMkd2lkdGhzIDwtIGFzLmxpc3QobWF4V2lkdGgpCmc0JHdpZHRocyA8LSBhcy5saXN0KG1heFdpZHRoKQpgYGAKVGhlIGdyb2JzIG1pZ2h0IGdldCBzbGlnaHRseSBkaWZmZXJpbmcgd2lkdGhzLiBXZSB3YW50IHRvIGhhdmUgZXF1YWwgd2lkdGhzIGluIGFsbCBncm9icyBzbyB0aGF0IHRoZSBicmVha3Mgb24gdGltZSBheGlzIHdpbGwgYWxpZ24gbmljZWx5IGFjY3Jvc3MgYWxsIHBsb3RzLiBUbyBkbyB0aGlzLCB3ZSBjYW4gZmluZCB0aGUgd2lkdGggb2YgdGhlIHdpZGVzdCBncm9iIGFuZCBmb3JjZSB0aGF0IHRvIGFsbCBncm9icy4gQ3JlZGl0OiBodHRwczovL2dpc3QuZ2l0aHViLmNvbS90b21ob3BwZXIvZmFhMjQ3OTdiYjQ0YWRkZWJhNzkKCgpgYGB7ciwgZmlnLmhlaWdodD04fQpncmlkLmFycmFuZ2UoCiAgYXJyYW5nZUdyb2IoZzEsZzIsZzMsZzQsCiAgICBuY29sID0gMSwKICAgIGhlaWdodHMgPSBjKDEsMSwxLC41KQogICkKKQpgYGAKTm93IHdlIGNhbiBmaW5hbGx5IGFycmFuZ2UgdGhlIGdyb2JzIHRvIGEgZ3JpZCwgdGhpcyB3aWxsIHBsb3QgaXQgb3V0LiBXZSBjYW4gY29udHJvbCB0aGUgaGVpZ2h0IG9mIHRoZSBlYWNoIGdyb2Igc2VwYXJhdGVseSAtIGhlcmUgd2UgZ2l2ZSA1MCUgaGVpZ2h0IHRvIHRoZSBjb2FzdGluZyBzaWduYWwgYXMgaXQgZG9lcyBub3QgbmVlZCBhcyBtdWNoIHZlcnRpY2FsIHJlYWwgZXN0YXRlIGFzIHRoZSBjb250aW51b3VzIHNpZ25hbHMgZG8uIAoKIyMgQ29uY2x1c2lvbnMKCiogVXNpbmcgZ2dwbG90MiB3aXRoIGdyaWRzIGNhbiB5aWVsZCBoaWdoIHF1YWxpdHkgdGltZSBzZXJpZXMgcGxvdHMgbWl4aW5nIGNvbnRpbnVvdXMgYW5kIGRpc2NyZXRlIHNpZ25hbHMKKiBJdCdzIGxpdHRsZSBiaXQgdGVkaW91cyB0byBnZXQgdGhlIHBsb3RzIHN0YWNrZWQsIGFsaWduZWQgYW5kIHNjYWxlZCAKKiBWZXJ0aWNhbCByZWFsIGVzdGF0ZSBpcyBzY2FyY2UgLSBtaXhpbmcgbW9yZSBzaWduYWxzIHdpbGwgY2F1c2Ugdmlld2VycyBoYXZpbmcgaGFyZCB0aW1lIHRvIHNjcm9sbCB0byBzZWUgdGltZSBheGlzCiogQXQgcHJlc2VudCwgbm8ga25vd24gZWFzeSB3YXkgdG8gb3ZlcmxheSBzZXZlcmFsIGRpc2NyZXRlIHNpZ25hbHMgb24gdG9wIG9mIGNvbnRpbnVvdXMgc2lnbmFscyAtIHRoaXMgd291bGQgc2F2ZSB2ZXJ0aWNhbCBzcGFjZSB3aXRob3V0IGEgcmVhbCBkYW5nZXIgb2YgY29uZnVzaW5nIHZpZXdlcnMKCgo=