Maral Dorri
Maral Dorri

Reputation: 478

Set plot height equal to adjacent table height through function in terms of number of rows of data in Rmarkdown

I am using the mtcars data to create a side by side plot and table in Rmarkdown.

---
title: "document"
author: "Maral Dorri"
date: 'May 2022'
output:
  html_document
---

I create a column and add the table on the right:

<div class = "row">
<div class = "col-md-3">
```{r}
raw_dat <- mtcars[1:15, ] %>% rownames_to_column(var = "id") %>% select(id, mpg) %>% 
  mutate(links = paste(.$id, "And <a href = 'https://www.cars.com//'>here</a>"))
tibble(
  name = raw_dat$id,
  link = paste(raw_dat$mpg, "And <a href = 'https://www.cars.com//'>here</a>")) %>%
  mutate(link = map(link, gt::html)) %>%
  gt

```
</div>

Then I create another column and print the plot on the left

<div class = "col-md-9">
```{r, fig.height=5.5}
   ggplot(raw_dat, aes(factor(id, rev(id)), mpg)) +
   geom_point() +
   coord_flip() +
   theme(plot.margin = margin(0.6, unit = "cm"))
```
</div>
</div>

The results are:

enter image description here

I know I can set the height of the figure manually and capture the same size as the table on the right, but I want to make this automatic since for my actual data, it will be changing periodically. So the function used for the height of the plot should be in terms of number of rows of data in the table.

The desired output would line up each row of the data in the table on the right with the y axis labels of the plot on the left, as shown (done manually, and not perfectly aligned)

enter image description here

Upvotes: 7

Views: 763

Answers (2)

Jon Spring
Jon Spring

Reputation: 66880

The bstfun package offers a nice automatic approach for this. (Note the package relies on a variety of other packages, like magick and webshot.)

library(patchwork)
# devtools::install_github("ddsjoberg/bstfun")
library(bstfun)
as_ggplot(my_table) + my_plot

If it's ok to be approximate, you can get the table and axis names roughly aligned using:

as_ggplot(my_table) | (plot_spacer() / my_plot) +
  plot_layout(heights = c(1,90)) # manual adjustment
  

enter image description here

Source data

library(gt); library(tidyverse)
raw_dat <- mtcars[1:15, ] %>% rownames_to_column(var = "id") %>% select(id, mpg) %>% 
  mutate(links = paste(.$id, "And <a href = 'https://www.cars.com//'>here</a>"))
my_table <- tibble(
  name = raw_dat$id,
  link = paste(raw_dat$mpg, "And <a href = 'https://www.cars.com//'>here</a>")) %>%
  mutate(link = map(link, gt::html)) %>%
  gt

my_plot <- ggplot(raw_dat, aes(factor(id, rev(id)), mpg)) +
  geom_point() +
  coord_flip() +
  theme(plot.margin = margin(0.6, unit = "cm"))

Upvotes: 1

Kat
Kat

Reputation: 18754

Update... again

Alright this will work until the plot has expanded to the point where the plot margin has scaled beyond the height of the table's header row (about 15 rows)

This is the styles and JS.

<style>
.main-container {
  max-width: unset;
}
</style>


```{r listen,results="asis",engine="js"}
// tbl font size is 1em (assuming-- rendered 16px; the padding t/b 8px)
// do what you're told 
setTimeout(function(){  // add to buttons
  ch = document.querySelector('.col-md-3').clientHeight; // how tall is the table
  sh = document.querySelector('.col-md-9');              // pull the 2nd column for manipulation
  si = document.querySelector('img');                    // assuming there's only one plot
  sih = si.clientHeight;                                 // plot height
  siw = si.clientWidth;                                  // plot width
  fs = $('.gt_table').css('line-height');                // size of text 
  bbt = $('.gt_table_body').css('border-top-width');     // table, head, body border (* 6)
  fss = parseInt(fs, 10);                                // strip the px from the value
  bbw = parseInt(bbt, 10);                               // strip the px from the value
  cz = sih/(sih - 11 - bbw - fss); // height of plot - margins - padding - the bottom (ticks, label, values)
  cha = ch * cz;                   // add additional height, was 1.075 before cz
  nw = siw/sih * cha;              // new width of plot
  sh.setAttribute('style', 'height: ' + cha + 'px; width: ' + nw + 'px;'); // shape container for centering
  si.setAttribute('style', 'height: ' + cha + 'px; width: ' + nw + 'px;');
  
  mm = document.querySelector('div.main-container > div.row'); // now get and set sizes for centering it all
  mc = mm.clientWidth;                                  // row width
  co = document.querySelector('.col-md-3').clientWidth; //  width of the table
  ms = (mc - nw - co)/2;                                // calc margin sizes
  mm.setAttribute('style', 'margin-left: ' + ms + 'px; margin-right: ' + ms + 'px;'); // center the content
}, 100) // you know, in case I'm slow...

```

Here's how this is in my RMD altogether.

---
title: "Untitled"
author: "me"
date: '2022-06-08'
output: html_document
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = F)
library(tidyverse)
library(gt)
data(mtcars)
```

<style>
.main-container {
  max-width: unset;
}
</style>

<div class = "row">
<div class = "col-md-3">

```{r tblr}
raw_dat <- mtcars[1:15, ] %>% rownames_to_column(var = "id") %>% select(id, mpg) %>% 
  mutate(links = paste(.$id, "And <a href = 'https://www.cars.com//'>here</a>"))
tibble(
  name = raw_dat$id,
  link = paste(raw_dat$mpg, "And <a href = 'https://www.cars.com//'>here</a>")) %>%
  mutate(link = map(link, gt::html)) %>%
  gt

```

</div>

<div class = "col-md-9">

```{r pltr}
   ggplot(raw_dat, aes(factor(id, rev(id)), mpg)) +
   geom_point() +
   coord_flip() +
   theme(plot.margin = margin(0.6, unit = "cm"))
```

</div>
</div>


```{r listen,results="asis",engine="js"}
// tbl font size is 1em (assuming-- rendered 16px; the padding t/b 8px)
// do what you're told 
setTimeout(function(){  // add to buttons
  ch = document.querySelector('.col-md-3').clientHeight; // how tall is the table
  sh = document.querySelector('.col-md-9');              // pull the 2nd column for manipulation
  si = document.querySelector('img');                    // assuming there's only one plot
  sih = si.clientHeight;                                 // plot height
  siw = si.clientWidth;                                  // plot width
  fs = $('.gt_table').css('line-height');                // size of text 
  bbt = $('.gt_table_body').css('border-top-width');     // table, head, body border (* 6)
  fss = parseInt(fs, 10);                                // strip the px from the value
  bbw = parseInt(bbt, 10);                               // strip the px from the value
  cz = sih/(sih - 11 - bbw - fss); // height of plot - margins - padding - the bottom (ticks, label, values)
  cha = ch * cz;                   // add additional height, was 1.075 before cz
  nw = siw/sih * cha;              // new width of plot
  sh.setAttribute('style', 'height: ' + cha + 'px; width: ' + nw + 'px;'); // shape container for centering
  si.setAttribute('style', 'height: ' + cha + 'px; width: ' + nw + 'px;');
  
  mm = document.querySelector('div.main-container > div.row'); // now get and set sizes for centering it all
  mc = mm.clientWidth;                                  // row width
  co = document.querySelector('.col-md-3').clientWidth; //  width of the table
  ms = (mc - nw - co)/2;                                // calc margin sizes
  mm.setAttribute('style', 'margin-left: ' + ms + 'px; margin-right: ' + ms + 'px;'); // center the content
}, 100) // you know, in case I'm slow...

```

Update

I didn't realize that you wanted the labels to align between the plot and the table. Sorry about that. This updated JS will create the desired effect.

```{r listenOrElse,results="asis",engine="js"}

// do what you're told
setTimeout(function(){  // add to buttons
  ch = document.querySelector('.col-md-3').clientHeight;
  sh = document.querySelector('.col-md-9');
  si = document.querySelector('img'); // assuming there's only one!
  sih = si.clientHeight;
  siw = si.clientWidth;
  cha = ch * 1.075;   // add additional height, to account for plot padding
  nw = siw/sih * cha; // new width of plot
  console.log(ch);
  sh.style.height = cha + 'px';
  si.setAttribute('style', 'height: ' + cha + 'px; width: ' + nw + 'px; padding-top: 5px;');
}, 100) // you know, in case I'm slow...

```

enter image description here

Originally wrote

If you want the exact height of col-md-3 and col-md-9 to be the same, you could use JS.

The first thing I'll point out is that the max-width of the main container is a problem. I really suggest that you either unset this or change the value. I think it's set to 960 px. You can change the main container max-width property with styles.

<style>
.main-container {
  max-width: 1200px;
}
</style>

Ensuring that the columns are the same height:

```{r listenOrElse,results="asis",engine="js"}

// do what you're told
setTimeout(function(){  
  ch = document.querySelector('.col-md-3').clientHeight; // get table height
  sh = document.querySelector('.col-md-9');
  si = document.querySelector('img');                // assuming there's only one!
  sih = si.clientHeight;
  siw = si.clientWidth;
  nw = siw/sih * ch;             // new width of plot (maintain aspect-ratio)
  sh.style.height = ch + 'px';   // change height of container
                                 // change height and width of plot
  si.setAttribute('style', 'height: ' + ch + 'px; width: ' + nw + 'px');
}, 100) // you know, in case I'm slow...

```

enter image description here

Upvotes: 8

Related Questions