DOI: 10.7910/dvn/zzdym9
title: Time-Series Matrix (TSMx): A visualization tool for plotting multiscale temporal trends

description: &lt;b&gt;Time-Series Matrix (TSMx): A visualization tool for plotting multiscale temporal trends&lt;/b&gt;
&lt;br&gt;&lt;br&gt;
TSMx is an R script that was developed to facilitate multi-temporal-scale visualizations of time-series data. The script requires only a two-column CSV of years and values to plot the slope of the linear regression line for all possible year combinations from the supplied temporal range. The outputs include a time-series matrix showing slope direction based on the linear regression, slope values plotted with colors indicating magnitude, and results of a Mann-Kendall test. The start year is indicated on the &lt;i&gt;y&lt;/i&gt;-axis and the end year is indicated on the &lt;i&gt;x&lt;/i&gt;-axis. In the example below, the cell in the top-right corner is the direction of the slope for the temporal range 2001–2019. The red line corresponds with the temporal range 2010–2019 and an arrow is drawn from the cell that represents that range. One cell is highlighted with a black border to demonstrate how to read the chart—that cell represents the slope for the temporal range 2004–2014. This publication entry also includes an excel template that produces the same visualizations without a need to interact with any code, though minor modifications will need to be made to accommodate year ranges other than what is provided. 
&lt;br&gt;&lt;br&gt;
&lt;i&gt;TSMx for R was developed by Georgios Boumis; TSMx was originally conceptualized and created by Brad G. Peter in Microsoft Excel.&lt;/i&gt;
&lt;br&gt;&lt;br&gt;
&lt;i&gt;Please refer to the associated publication&lt;/i&gt;: 
&lt;br&gt;
Peter, B.G., Messina, J.P., Breeze, V., Fung, C.Y., Kapoor, A. and Fan, P., 2024. Perspectives on modifiable spatiotemporal unit problems in remote sensing of agriculture: evaluating rice production in Vietnam and tools for analysis. &lt;i&gt;Frontiers in Remote Sensing&lt;/i&gt;, 5, p.1042624.
&lt;br&gt;
&lt;a href="https://www.frontiersin.org/journals/remote-sensing/articles/10.3389/frsen.2024.1042624"&gt;https://www.frontiersin.org/journals/remote-sensing/articles/10.3389/frsen.2024.1042624&lt;/a&gt;
&lt;br&gt;&lt;br&gt;
&lt;img src ="https://github.com/cartoscience/seagul/blob/main/tsmx/TSMx_Sample-Chart.png?raw=true" width="1000" height="auto"&lt;/img&gt;
TSMx sample chart from the supplied Excel template. Data represent the productivity of rice agriculture in Vietnam as measured via EVI (enhanced vegetation index) from the NASA MODIS data product (MOD13Q1.V006). 
&lt;br&gt;&lt;br&gt;
&lt;b&gt;TSMx&lt;/b&gt; R script: 
&lt;pre&gt;
# import packages

library(dplyr)
library(readr)
library(ggplot2)
library(tibble)
library(tidyr)
library(forcats)
library(Kendall)

options(warn = -1) # disable warnings

# read data (.csv file with "Year" and "Value" columns)

data &lt;- read_csv("EVI.csv")

# prepare row/column names for output matrices

years &lt;- data %&gt;% pull("Year")
r.names &lt;- years[-length(years)]
c.names &lt;- years[-1]
years &lt;- years[-length(years)]

# initialize output matrices

sign.matrix &lt;-
 matrix(data = NA,
 nrow = length(years),
 ncol = length(years))
pval.matrix &lt;-
 matrix(data = NA,
 nrow = length(years),
 ncol = length(years))
slope.matrix &lt;-
 matrix(data = NA,
 nrow = length(years),
 ncol = length(years))

# function to return remaining years given a start year

getRemain &lt;- function(start.year) {
 years &lt;- data %&gt;% pull("Year")
 start.ind &lt;- which(data[["Year"]] == start.year) + 1
 remain &lt;- years[start.ind:length(years)]
 return (remain)
}

# function to subset data for a start/end year combination

splitData &lt;- function(end.year, start.year) {
 keep &lt;-
 which(data[['Year']] &gt;= start.year & data[['Year']] &lt;= end.year)
 batch &lt;- data[keep,]
 return(batch)
}

# function to fit linear regression and return slope direction

fitReg &lt;- function(batch) {
 trend &lt;- lm(Value ~ Year, data = batch)
 slope &lt;- coefficients(trend)[[2]]
 return(sign(slope))
}

# function to fit linear regression and return slope magnitude

fitRegv2 &lt;- function(batch) {
 trend &lt;- lm(Value ~ Year, data = batch)
 slope &lt;- coefficients(trend)[[2]]
 return(slope)
}

# function to implement Mann-Kendall (MK) trend test and return significance
# the test is implemented only for n&gt;=8

getMann &lt;- function(batch) {
 if (nrow(batch) &gt;= 8) {
 mk &lt;- MannKendall(batch[['Value']])
 pval &lt;- mk[['sl']]
 } else {
 pval &lt;- NA
 }
 return(pval)
}

# function to return slope direction for all combinations given a start year

getSign &lt;- function(start.year) {
 remaining &lt;- getRemain(start.year)
 combs &lt;- lapply(remaining, splitData, start.year = start.year)
 signs &lt;- lapply(combs, fitReg)
 return(signs)
}

# function to return MK significance for all combinations given a start year

getPval &lt;- function(start.year) {
 remaining &lt;- getRemain(start.year)
 combs &lt;- lapply(remaining, splitData, start.year = start.year)
 pvals &lt;- lapply(combs, getMann)
 return(pvals)
}

# function to return slope magnitude for all combinations given a start year

getMagn &lt;- function(start.year) {
 remaining &lt;- getRemain(start.year)
 combs &lt;- lapply(remaining, splitData, start.year = start.year)
 magns &lt;- lapply(combs, fitRegv2)
 return(magns)
}

# retrieve slope direction, MK significance, and slope magnitude

signs &lt;- lapply(years, getSign)
pvals &lt;- lapply(years, getPval)
magns &lt;- lapply(years, getMagn)

# fill-in output matrices

dimension &lt;- nrow(sign.matrix)

for (i in 1:dimension) {
 sign.matrix[i, i:dimension] &lt;- unlist(signs[i])
 pval.matrix[i, i:dimension] &lt;- unlist(pvals[i])
 slope.matrix[i, i:dimension] &lt;- unlist(magns[i])
}

sign.matrix &lt;- data.frame(sign.matrix)
pval.matrix &lt;- data.frame(pval.matrix)
slope.matrix &lt;- data.frame(slope.matrix)

# rename rows/columns of output matrices

names(sign.matrix) &lt;- c.names
row.names(sign.matrix) &lt;- r.names
names(pval.matrix) &lt;- c.names
row.names(pval.matrix) &lt;- r.names
names(slope.matrix) &lt;- c.names
row.names(slope.matrix) &lt;- r.names

# pretty-print slope direction output matrix

sign.matrix &lt;-
 sign.matrix %&gt;% rownames_to_column() %&gt;% gather(colname, value, -rowname)
names(sign.matrix) &lt;- c("Start", "End", "Sign")
sign.matrix[['Color']] &lt;- NA
sign.matrix [['Trend']] &lt;- NA
pos.ind &lt;- which(sign.matrix[['Sign']] &gt; 0)
neg.ind &lt;- which(sign.matrix[['Sign']] &lt; 0)
sign.matrix[['Color']][pos.ind] &lt;- "#99CCFF"
sign.matrix[['Color']][neg.ind] &lt;- "#FF9966"
sign.matrix[['Trend']][pos.ind] &lt;- "+"
sign.matrix[['Trend']][neg.ind] &lt;- "-"
sign.matrix[['Trend']][is.na(sign.matrix[['Trend']])] &lt;- ""

ggplot(sign.matrix,
 aes(x = End,
 fct_rev(Start),
 col = Color,)) +
 scale_color_identity() +
 scale_fill_identity() +
 coord_fixed() +
 geom_tile(aes(fill = Color), color = 'black') + xlab("end year") + ylab("start year") +
 labs(title = "Slope direction", caption = "Boumis and Peter (2021)") +
 theme(
 axis.text.x = element_text(angle = 15),
 axis.text.y = element_text(angle = 15),
 panel.grid.major = element_blank(),
 panel.grid.minor = element_blank(),
 plot.title = element_text(hjust = 0.5, face = "italic"),
 plot.caption = element_text(face = "italic")
 ) + scale_x_discrete(position = "top") +
 geom_text(aes(label = Trend), color = "black")

# save .png of slope direction output matrix

ggsave(
 "Slope-Direction.png",
 device = "png",
 unit = "in",
 height = 7,
 width = 12,
 dpi = 300
)

# pretty-print MK significance output matrix

pval.matrix &lt;-
 pval.matrix %&gt;% rownames_to_column() %&gt;% gather(colname, value, -rowname)
names(pval.matrix) &lt;- c("Start", "End", "Pval")
pval.matrix[['Color']] &lt;- NA
pval.matrix [['Sign']] &lt;- NA
sign.ind &lt;- which(pval.matrix[['Pval']] &lt; 0.05)
nsign.ind &lt;- which(pval.matrix[['Pval']] &gt; 0.05)
pval.matrix[['Color']][sign.ind] &lt;- "#99FFFF"
pval.matrix[['Color']][nsign.ind] &lt;- "#CC33CC"
pval.matrix[['Sign']][sign.ind] &lt;- "&lt;0.05"
pval.matrix[['Sign']][nsign.ind] &lt;- "&gt;0.05"
pval.matrix[['Sign']][is.na(pval.matrix[['Pval']])] &lt;- ""

ggplot(pval.matrix,
 aes(x = End,
 fct_rev(Start),
 col = Color,)) +
 scale_color_identity() +
 scale_fill_identity() +
 coord_fixed() +
 geom_tile(aes(fill = Color), color = "black") + xlab("end year") + ylab("start year") +
 labs(title = "Mann-Kendall trend significance", caption = "Boumis and Peter (2021)") +
 theme(
 axis.text.x = element_text(angle = 15),
 axis.text.y = element_text(angle = 15),
 panel.grid.major = element_blank(),
 panel.grid.minor = element_blank(),
 plot.title = element_text(hjust = 0.5, face = "italic"),
 plot.caption = element_text(face = "italic")
 ) + scale_x_discrete(position = "top") +
 geom_text(aes(label = Sign), color = "black", size = 2)

# save .png of MK significance output matrix

ggsave(
 "MK-Significance.png",
 device = "png",
 unit = "in",
 height = 7,
 width = 12,
 dpi = 300
)

# pretty-print slope magnitude output matrix

slope.matrix &lt;-
 slope.matrix %&gt;% rownames_to_column() %&gt;% gather(colname, value, -rowname)
names(slope.matrix) &lt;- c("Start", "End", "Magnitude")

ggplot(slope.matrix, aes(End, fct_rev(Start), fill = Magnitude)) +
 xlab("end year") + ylab("start year") +
 geom_tile(color = "black") +
 coord_fixed() +
 labs(title = "Slope magnitude", caption = "Boumis and Peter (2021)") +
 scale_fill_gradient2(
 low = "#FF3300",
 mid = "#FFFFFF",
 high = "#0033FF",
 midpoint = 0.0
 ) +
 theme(
 axis.text.x = element_text(angle = 15),
 axis.text.y = element_text(angle = 15),
 panel.grid.major = element_blank(),
 panel.grid.minor = element_blank(),
 plot.title = element_text(hjust = 0.5, face = "italic"),
 plot.caption = element_text(face = "italic")
 ) + scale_x_discrete(position = "top") +
 guides(fill = guide_colourbar(
 title = "",
 barwidth = 0.5,
 barheight = 7.5
 ))

# save .png of slope magnitude output matrix

ggsave(
 "Slope-Magnitude.png",
 device = "png",
 unit = "in",
 height = 7,
 width = 12,
 dpi = 300
)
&lt;/pre&gt;
&lt;b&gt;Sample outputs:&lt;/b&gt;
&lt;img src ="https://github.com/cartoscience/seagul/blob/main/tsmx/TSMx_Slope-Direction_Small.png?raw=true" width="1000" height="auto"&lt;/img&gt;
&lt;img src ="https://github.com/cartoscience/seagul/blob/main/tsmx/TSMx_Slope-Magnitude_Small.png?raw=true" width="1000" height="auto"&lt;/img&gt;
&lt;img src ="https://github.com/cartoscience/seagul/blob/main/tsmx/TSMx_MK-Significance_Small.png?raw=true" width="1000" height="auto"&lt;/img&gt;
&lt;br&gt;
&lt;b&gt;Project information:&lt;/b&gt;&lt;br&gt;
SEAGUL: Southeast Asia Globalization, Urbanization, Land and Environment Changes
&lt;br&gt;
&lt;a href="http://seagul.info/"&gt;http://seagul.info/&lt;/a&gt;&lt;br&gt;
&lt;a href="https://lcluc.umd.edu/projects/divergent-local-responses-globalization-urbanization-land-transition-and-environmental"&gt;https://lcluc.umd.edu/projects/divergent-local-responses-globalization-urbanization-land-transition-and-environmental&lt;/a&gt;
&lt;br&gt;
This project was made possible by the the NASA Land-Cover/Land-Use Change Program (Grant #: 80NSSC20K0740)
&lt;br&gt;&lt;br&gt;
Another publication developed by the SEAGUL research group that is related to the kind of modifiable temporal unit problems (MTUP) presented here can be viewed at: &lt;a href="https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/M4ZGXP"&gt;MSZSI: Multi-Scale Zonal Statistics [AgriClimate] Inventory&lt;/a&gt;.