Flowcharts that belong in the analysis pipeline

Flowcharts should be beautiful. Just like this CC photo from Wasif Malik,

Thanks to Alan Haynes and
his excellent suggestions, I have spent some time improving the
flowchart component of the Gmisc package. The result is not meant to be
another decorative diagram tool. It is meant for the kind of figures
researchers keep redrawing by hand: CONSORT diagrams, cohort derivation
charts, screening flows, data-cleaning audit trails, and the small but
important maps that explain how a study population came to be.

I like tools such as Excalidraw
for thinking. They are fast, expressive, and excellent for
conversations. But when a figure enters a manuscript, the needs change.
Counts must be updated. Exclusions must match the analysis script.
Treatment arms should align. Follow-up losses should be traceable. The
figure should survive reviewer round three without becoming a manual
editing project.

That is the space where flowchart() in Gmisc is useful:
the diagram becomes part of the research workflow.

A colorful CONSORT-style flowchart generated with Gmisc

The figure above is the kind of chart I want Gmisc to make feel
natural. It is still a grid graphic in R, but it has the visual grammar
of a manuscript figure: grouped arms, side exclusions, count badges,
phase labels, and arrows that do not need nudging after every text
change.

Every figure in this post is generated by code, and the code is
included below each image. They all share the same two-line
preamble:


library(Gmisc)
library(grid)

To save any of them to a file, wrap the call in a graphics device,
e.g.


png("01-consort-color.png", width = 9, height = 7, units = "in", res = 180, bg = "white")
# ... the flowchart code ...
dev.off()

The CONSORT figure above is produced by:


options(boxGrobTxtPadding = unit(3, "mm"))

box_fill <- gpar(fill = "#DDEEFF", col = "#336699", lwd = 1.5)
con_gp <- gpar(col = "#336699", lwd = 1.5, fill = "#336699")
side_gp <- gpar(col = "#CC8800", lwd = 1.2, fill = "#CC8800")
excl_fill <- gpar(fill = "#FFF8E1", col = "#CC8800", lwd = 1.2)
heading_gp <- gpar(fill = "#C8DAF7", col = "#2F5F9F", lwd = 1.1)
badge_gp <- gpar(fill = "#336699", col = NA)
badge_txt_gp <- gpar(col = "white", cex = 0.65)

main_arm_margin <- 0.28
main_x <- 0.5
exclusion_margin <- 0.05

grid.newpage()
flowchart(
  assessed = boxGrob(
    "Patients assessed for eligibility",
    x = main_x, box_gp = box_fill,
    badge_label = "840", badge_gp = badge_gp, badge_txt_gp = badge_txt_gp
  ),
  randomised = boxGrob(
    "Randomised",
    x = main_x, box_gp = box_fill,
    badge_label = "126", badge_gp = badge_gp, badge_txt_gp = badge_txt_gp
  ),
  arms = list(
    cast = boxGrob(
      "Randomised to\ncast immobilisation",
      box_gp = box_fill,
      badge_label = "62", badge_gp = badge_gp, badge_txt_gp = badge_txt_gp
    ),
    surgical = boxGrob(
      "Randomised to\nsurgery",
      box_gp = box_fill,
      badge_label = "64", badge_gp = badge_gp, badge_txt_gp = badge_txt_gp
    )
  ),
  lost = list(
    lost_cast = boxGrob(
      "Lost to follow-up (n = 2)\n  1 no response\n  1 other surgery",
      just = "left", box_gp = excl_fill
    ),
    lost_surgical = boxGrob(
      "Lost to follow-up (n = 3)\n  2 no response\n  1 other surgery",
      just = "left", box_gp = excl_fill
    )
  ),
  analysis = list(
    analysis_cast = boxGrob(
      "Included in\nprimary analysis",
      box_gp = box_fill,
      badge_label = "60", badge_gp = badge_gp, badge_txt_gp = badge_txt_gp
    ),
    analysis_surgical = boxGrob(
      "Included in\nprimary analysis",
      box_gp = box_fill,
      badge_label = "61", badge_gp = badge_gp, badge_txt_gp = badge_txt_gp
    )
  )
) |>
  spread(axis = "y", margin = unit(5, "mm"), exclude = "lost") |>
  align(
    axis = "y",
    subelement = "lost",
    references = list("arms", "analysis")
  ) |>
  equalizeWidths(subelement = list("arms", "analysis")) |>
  spread(axis = "x", subelement = "arms", margin = main_arm_margin) |>
  spread(axis = "x", subelement = "analysis", margin = main_arm_margin) |>
  spread(axis = "x", subelement = "lost", margin = exclusion_margin) |>
  phaseLabel("arms", "Allocation", box_gp = heading_gp) |>
  phaseLabel("analysis", "Analysis", box_gp = heading_gp) |>
  insert(list(excluded = boxGrob(
    "Excluded (n = 714)\n  477 stable ankle mortise\n   64 incongruent ankle mortise\n   30 previous serious trauma\n  143 other reasons",
    just = "left", box_gp = excl_fill
  )), after = "assessed") |>
  move(subelement = "excluded", x = 1 - exclusion_margin, just = "right") |>
  align(
    axis = "y",
    subelement = "excluded",
    references = list("assessed", "randomised")
  ) |>
  connect("assessed", "excluded", type = "L", lty_gp = side_gp, arrow_size = 3, smooth = TRUE) |>
  connect("randomised", "arms", type = "N", lty_gp = con_gp, arrow_size = 3, smooth = TRUE) |>
  connect("assessed", "randomised", type = "v", lty_gp = con_gp, arrow_size = 3, smooth = TRUE) |>
  connect("arms", "lost", type = "L", lty_gp = side_gp, arrow_size = 3, smooth = TRUE) |>
  connect("arms", "analysis", type = "v", lty_gp = con_gp, arrow_size = 3) |>
  print()

A figure that can
change with the analysis

The biggest advantage of drawing a flowchart in code is not that code
is elegant. It is that research figures are rarely finished when we
think they are.

The inclusion count changes after a database refresh. A reviewer asks
for a sensitivity analysis. Someone notices that two exclusion
categories should be split. The statistician reruns the cohort
definition. If the diagram is hand-drawn, every one of those changes
creates a small risk of mismatch between the paper and the actual
analysis.

If the chart is generated, it can sit beside the code that produced
the numbers.


flowchart(...) |>
  spread(axis = "y") |>
  spread(subelement = "arms", axis = "x") |>
  connect("randomised", "arms", type = "N")

That is the mental model: define boxes, arrange boxes, connect boxes.
The final result can still be polished, but it remains reproducible.

Cohort
derivation from data people already have

Most clinical researchers do not start with a perfect trial flow.
They start with a registry extract, an EHR table, a REDCap project, an
Excel sheet from a collaborator, or a combination of all of them.

That workflow deserves a clear figure too.

Registry and EHR cohort derivation flowchart

This kind of diagram is useful because it does not only show who was
included. It shows how the study base was assembled: what sources were
linked, where exclusions entered, and which analytic populations came
out at the end.

I find this especially helpful for observational studies. A table can
report baseline characteristics, but a flowchart explains the
construction of the cohort. It gives the reader a quick answer to: “What
happened between the raw data and the model?”


source_gp <- gpar(fill = "#E8F5E9", col = "#2E7D32", lwd = 1.4)
link_gp <- gpar(fill = "#E3F2FD", col = "#1565C0", lwd = 1.4)
cohort_gp <- gpar(fill = "#FFF8E1", col = "#C69214", lwd = 1.4)
side_gp <- gpar(fill = "#FCE4EC", col = "#AD1457", lwd = 1.2)
final_gp <- gpar(fill = "#EDE7F6", col = "#512DA8", lwd = 1.4)
con_gp <- gpar(col = "#455A64", fill = "#455A64", lwd = 1.4)
excl_gp <- gpar(col = "#AD1457", fill = "#AD1457", lwd = 1.2)

source_margin <- 0.05
output_margin <- 0.05
main_x <- 0.5
main_path <- list("linked", "cohort")
exclusion_right <- 0.95
exclusion_gap <- unit(5, "pt")
exclusion_line_offset <- unit(14, "mm")

grid.newpage()
flowchart(
  sources = list(
    ehr = boxGrob("Hospital EHR\nadmissions\nn = 241,820",
                  box_gp = source_gp),
    registry = boxGrob("Quality registry\nprocedures\nn = 38,420",
                       box_gp = source_gp),
    deaths = boxGrob("Population registry\nfollow-up\nn = 100%",
                     box_gp = source_gp)
  ),
  linked = boxGrob(
    "Linked study base\nunique patients with follow-up\nn = 29,614",
    x = main_x,
    box_gp = link_gp,
    width = unit(72, "mm")
  ),
  exclusions = list(
    prior = boxGrob("Previous diagnosis\nn = 4,108",
                    just = "left", box_gp = side_gp,
                    width = unit(42, "mm")),
    missing = boxGrob("Missing key\ncovariates\nn = 962",
                      just = "left", box_gp = side_gp,
                      width = unit(42, "mm")),
    outside = boxGrob("Outside study\nwindow\nn = 1,327",
                      just = "left", box_gp = side_gp,
                      width = unit(42, "mm"))
  ),
  cohort = boxGrob(
    "Primary cohort\nn = 23,217",
    box_gp = cohort_gp,
    width = unit(62, "mm")
  ),
  outputs = list(
    primary = boxGrob("Primary analysis\ncomplete case\nn = 22,144",
                      box_gp = final_gp),
    imputed = boxGrob("Sensitivity analysis\nmultiple imputation\nn = 23,217",
                      box_gp = final_gp),
    negative = boxGrob("Negative control\noutcome check\nn = 21,903",
                       box_gp = final_gp)
  )
) |>
  spread(axis = "y", margin = unit(8, "mm"), exclude = "exclusions") |>
  equalizeWidths(subelement = main_path) |>
  align(axis = "x", subelement = "cohort", reference = "linked") |>
  move(subelement = c("exclusions", "prior"),
       y = position("linked", position = "center", type = "y") - exclusion_gap,
       just = c(NA, "top")) |>
  move(subelement = c("exclusions", "missing"),
       y = position(c("exclusions", "prior"), position = "bottom", type = "y") - exclusion_gap,
       just = c(NA, "top")) |>
  move(subelement = c("exclusions", "outside"),
       y = position(c("exclusions", "missing"), position = "bottom", type = "y") - exclusion_gap,
       just = c(NA, "top")) |>
  equalizeWidths(subelement = "sources") |>
  equalizeWidths(subelement = "exclusions", width = unit(42, "mm")) |>
  equalizeWidths(subelement = "outputs") |>
  move(subelement = "exclusions", x = exclusion_right, just = "right") |>
  spread(axis = "x", subelement = "sources", margin = source_margin, type = "center") |>
  spread(axis = "x", subelement = "outputs", margin = output_margin, type = "center") |>
  connect("sources", "linked", type = "vertical_axis", lty_gp = con_gp, arrow_size = 3) |>
  connect("linked", "cohort", type = "v", lty_gp = con_gp, arrow_size = 3, smooth = TRUE) |>
  connect("linked", "exclusions",
          type = "side", lty_gp = excl_gp, arrow_size = 3,
          side = "right", end_side = "left",
          side_route = "outside",
          side_offset = exclusion_line_offset,
          label = "Excluded\nn = 6,397",
          label_gp = gpar(col = "#AD1457", cex = 0.8)) |>
  connect("cohort", "outputs", type = "N", lty_gp = con_gp, arrow_size = 3, smooth = TRUE) |>
  print()

The audit trail is part of
the story

Another common workflow is less glamorous but just as important: data
validation.

Data cleaning and validation flowchart

Many research projects have a small data-engineering pipeline even
when nobody calls it that. Data arrive through forms, imports, manual
entry, and collaborator spreadsheets. Then someone checks missing
fields, duplicates, impossible dates, inconsistent IDs, and
outliers.

That process is often hidden in prose. A compact flowchart can make
it visible without turning the methods section into a systems manual. It
is also a useful project-management figure: the same chart can be shown
to clinicians, data managers, statisticians, and co-authors.

Note how the box shapes carry meaning here — ellipses, databases,
documents, tapes, and diamonds all come from dedicated
box*Grob() helpers:


input_gp <- gpar(fill = "#F3F8FF", col = "#3B73C5", lwd = 1.3)
process_gp <- gpar(fill = "#FFF4C7", col = "#C69214", lwd = 1.3)
issue_gp <- gpar(fill = "#FCE4EC", col = "#AD1457", lwd = 1.2)
output_gp <- gpar(fill = "#E8F5E9", col = "#2E7D32", lwd = 1.3)
note_gp <- gpar(fill = "#FFFFFF", col = "#607D8B", lwd = 1, lty = 2)
con_gp <- gpar(col = "#555555", fill = "#555555", lwd = 1.3)
issue_con_gp <- gpar(col = "#AD1457", fill = "#AD1457", lwd = 1.1)

main_path <- list("validation", "clean")
issue_column_x <- 0.08
log_column_x <- 0.92
input_shape_width <- unit(42, "mm")
input_shape_height <- unit(24, "mm")
issue_shape_width <- unit(48, "mm")
issue_shape_height <- unit(14, "mm")

grid.newpage()
flowchart(
  inputs = list(
    web = boxEllipseGrob("REDCap\nform",
                         width = input_shape_width,
                         height = input_shape_height,
                         box_gp = input_gp),
    import = boxDatabaseGrob("CSV\nimport",
                             width = input_shape_width,
                             height = input_shape_height,
                             box_gp = input_gp),
    manual = boxDocumentGrob("Manual\nentry",
                             width = input_shape_width,
                             height = input_shape_height,
                             box_gp = input_gp)
  ),
  shape_note = boxGrob(
    "Shape indicates\nsource type",
    just = "left",
    width = unit(36, "mm"),
    box_gp = note_gp
  ),
  validation = boxTapeGrob(
    "Validation queue\nIDs, dates, ranges, missingness",
    width = unit(.58, "npc"),
    height = unit(.14, "npc"),
    box_gp = process_gp
  ),
  issues = list(
    missing = boxDiamondGrob("Missing\nfields",
                             width = issue_shape_width,
                             height = issue_shape_height,
                             box_gp = issue_gp),
    duplicate = boxDiamondGrob("Duplicate\nID",
                               width = issue_shape_width,
                               height = issue_shape_height,
                               box_gp = issue_gp),
    outlier = boxDiamondGrob("Outlier\nvalue",
                             width = issue_shape_width,
                             height = issue_shape_height,
                             box_gp = issue_gp)
  ),
  log = boxDocumentsGrob(
    "Issue log\nqueries sent\nchanges reviewed",
    width = unit(48, "mm"),
    height = unit(.44, "npc"),
    box_gp = issue_gp
  ),
  clean = boxDatabaseGrob(
    "Analysis-ready dataset\nlocked for report",
    width = unit(.44, "npc"),
    height = unit(.16, "npc"),
    box_gp = output_gp
  )
) |>
  spread(axis = "y", margin = unit(7, "mm"),
         exclude = list("issues", "shape_note")) |>
  spread(axis = "x", subelement = "inputs",
         from = 0, to = 0.7, margin = 0.05,
         type = "center") |>
  equalizeWidths(subelement = main_path) |>
  align(axis = "x", subelement = "validation", reference = "inputs") |>
  align(axis = "x", subelement = "clean", reference = "validation") |>
  align(axis = "y", subelement = "shape_note", reference = "inputs") |>
  align(axis = "y", subelement = "log",
        references = list("validation", "clean")) |>
  spread(axis = "y", subelement = "issues",
         from = position("log", position = "top", type = "y"),
         to = position("log", position = "bottom", type = "y"),
         margin = unit(2, "mm")) |>
  move(subelement = "shape_note", x = 0.95, just = "right") |>
  move(subelement = "issues", x = issue_column_x, just = "left") |>
  move(subelement = "log", x = log_column_x, just = "right") |>
  connect("inputs", "validation",
          type = "vertical_axis", lty_gp = con_gp, arrow_size = 3) |>
  connect("issues", "log",
          type = "horizontal_axis", lty_gp = issue_con_gp, arrow_size = 3) |>
  connect("validation", "clean", type = "vertical_axis",
          lty_gp = con_gp, arrow_size = 3, smooth = TRUE) |>
  print()

Follow-up is rarely just
down the page

Longitudinal studies often need to distinguish between people who are
lost, censored, withdrawn, dead, or still contributing information up to
a time point. A simple downward flow can imply that everyone leaving a
box disappears from the analysis, which is not always true.

Follow-up accounting with dotted return arrows

Dotted return arrows are useful for this. They can show that a
participant left direct follow-up but still contributes information to
the final analysis up to censoring. That is a visual detail, but it
communicates an analytical idea.

This is where small flowchart improvements matter. Not because the
reader cares about the drawing API, but because the figure can express
the study design more faithfully.


options(boxGrobTxtPadding = unit(1, "mm"))

main_gp <- gpar(fill = "#FFFFFF", col = "#263238", lwd = 1.2)
arm_gp <- gpar(fill = "#E3F2FD", col = "#1565C0", lwd = 1.3)
ex_gp <- gpar(fill = "#FFF8E1", col = "#C69214", lwd = 1.2)
con_gp <- gpar(col = "#1565C0", fill = "#1565C0", lwd = 1.3)
side_gp <- gpar(col = "#C69214", fill = "#C69214", lwd = 1.2)
dotted_gp <- gpar(col = "#455A64", fill = "#455A64", lwd = 1.1, lty = 2)

arm_from <- .24
arm_to <- .76
box_width <- unit(54, "mm")
ex_width <- unit(45, "mm")
ex_page_margin <- 0.03           # excluded columns hug the page edge by this npc margin
side_offset <- unit(4, "mm")     # side branches step out this far before turning to the excluded box
fan_in_offset <- unit(2, "mm")   # dotted return line runs 2 mm outside the excluded boxes

grid.newpage()
flowchart(
  rando = boxGrob("Randomised\nN = 197", box_gp = main_gp),
  groups = list(
    boxGrob("96 assigned to intervention\n95 received treatment",
            box_gp = arm_gp),
    boxGrob("101 assigned to control\n93 received treatment",
            box_gp = arm_gp)
  ),
  ex1 = list(
    boxGrob("8 died\n1 withdrew consent", just = "left", box_gp = ex_gp),
    boxGrob("18 died\n1 withdrew consent", just = "left", box_gp = ex_gp)
  ),
  groups1 = list(
    boxGrob("87 completed day 30\nfollow-up", box_gp = arm_gp),
    boxGrob("79 completed day 30\nfollow-up", box_gp = arm_gp)
  ),
  ex2 = list(
    boxGrob("8 died", just = "left", box_gp = ex_gp),
    boxGrob("9 died\n1 withdrew consent\n2 lost to follow-up",
            just = "left", box_gp = ex_gp)
  ),
  groups2 = list(
    boxGrob("79 completed day 180\nfollow-up", box_gp = arm_gp),
    boxGrob("68 completed day 180\nfollow-up", box_gp = arm_gp)
  ),
  analysis = list(
    boxGrob("95 included in primary\noutcome analysis", box_gp = arm_gp),
    boxGrob("95 included in primary\noutcome analysis", box_gp = arm_gp)
  )
) |>
  spread(axis = "y", margin = unit(0.02, "npc")) |>
  equalizeWidths(subelement = stringr::regex("^groups|analysis"), width = box_width) |>
  equalizeHeights(subelement = stringr::regex("^groups|analysis")) |>
  equalizeWidths(subelement = stringr::regex("^ex"), width = ex_width) |>
  spread(subelement = stringr::regex("^groups|analysis"), axis = "x",
         from = arm_from, to = arm_to, type = "center") |>
  move(subelement = "rando",
       x = position("groups", position = "center", type = "x")) |>
  move(subelement = list(c("ex1", 1), c("ex2", 1)),
       x = ex_page_margin, just = "left") |>
  move(subelement = list(c("ex1", 2), c("ex2", 2)),
       x = 1 - ex_page_margin, just = "right") |>
  connect("rando", "groups", type = "N", lty_gp = con_gp, arrow_size = 3, smooth = TRUE) |>
  connect(c("groups$1", "groups1$1"), c("ex1$1", "ex2$1"),
          type = "side", lty_gp = side_gp, arrow_size = 3,
          side = "left", end_side = "right",
          side_route = "outside", side_offset = side_offset) |>
  connect(c("groups$2", "groups1$2"), c("ex1$2", "ex2$2"),
          type = "side", lty_gp = side_gp, arrow_size = 3,
          side = "right", end_side = "left",
          side_route = "outside", side_offset = side_offset) |>
  connect("groups", "groups1", type = "vertical", lty_gp = con_gp, arrow_size = 3) |>
  connect("groups1", "groups2", type = "vertical", lty_gp = con_gp, arrow_size = 3) |>
  connect("groups2", "analysis", type = "vertical", lty_gp = con_gp, arrow_size = 3) |>
  connect(list("ex1$1", "ex2$1"), "analysis$1", type = "side",
          lty_gp = dotted_gp, arrow_size = 3,
          side = "left", end_side = "left",
          side_route = "outside",
          side_offset = fan_in_offset) |>
  connect(list("ex1$2", "ex2$2"), "analysis$2", type = "side",
          lty_gp = dotted_gp, arrow_size = 3,
          side = "right", end_side = "right",
          side_route = "outside",
          side_offset = fan_in_offset) |>
  print()

Why this belongs in Gmisc

Gmisc has always collected the small tools I found myself needing
around medical statistics: descriptive tables, transition plots, and
grid-based figures. Flowcharts fit that same pattern. They are not a
statistical model, but they are part of how research is
communicated.

The new flowchart work in 3.4.0 is therefore aimed at the practical
problems:

  • making CONSORT-like diagrams less painful to draw
  • keeping grouped stages aligned and readable
  • making arrows behave predictably
  • supporting side paths, return paths, and repeated box patterns
  • producing figures that can be regenerated when the study
    changes

The vignette contains the full API and examples:


vignette("Grid-based_flowcharts", package = "Gmisc")

The blog figures in this post are intentionally close to things
researchers already have in their workflow: trial enrollment, registry
construction, data validation, and follow-up accounting. My hope is that
they make the flowchart tools feel less like a drawing utility and more
like a small extension of the analysis itself.

Loss aversion and the difficulty of stopping surgery

Loss aversion was introduced by Daniel Kahneman and Amos Tversky. While rooted in economics, the idea captures a fundamental feature of human decision-making across domains. The image is CC from Jim Choate

In orthopaedic trauma, translating evidence into practice is hardest when the evidence tells us to stop operating. Stig Brorson’s excellent review of proximal humerus fractures (the book is open access) illustrates just how difficult surgical de-implementation can be. Multiple randomized trials, most prominently the ProFHER trial, have shown that surgery offers no long-term advantage over non-surgical treatment for most patients. Yet, despite this, surgical rates have remained high, with a growing tendency to replace rather than preserve the joint. This persistent gap between evidence and practice is not primarily explained by ignorance or lack of data. It is better understood through the lens of human decision-making.
Continue reading

What should we do with the posterior malleolus?

Hidden treat (CC by
Karon Elliott Edleson

The posterior malleolus has long been viewed as an important determinant of outcome after ankle fractures. Earlier observational studies (Verhage et al. (2016) and van Hoff et al. (2015)) suggested worse long-term results when the fragment was incongruent or displaced, which in turn fueled a trend toward more frequent fixation. Against this background, many of us were eager to see the POSTFIX RCT that put that theory to a test and the results were not what many expected. Continue reading

A few tips for beginners on how to engage with the open source community

Engaging the open source community can be challenging at start. The image is CC by Ryan Roberts.

I have been writing code in multiple languages since 1994 and for almost a decade now I have been active within the open source community, primarily in R but I’ve also published some JavaSript/TypeScript packages. Publishing something that other people enjoy is a pure joy and the fact that some of my packages have download counts in the thousands is something that truly warms my heart.

Throughout the years I have though noticed that there is newcomers to the open source community often struggle with how to get help, which is why I decided to write this post. I’ll start out with some basics and then a little more advanced topics such as how to write an issue or a pull request. Continue reading

Easiest flowcharts eveR?

A guide to flowcharts using my Gmisc package. The image is CC by Michael Staats.

A flowchart is a type of diagram that represents a workflow or process. The building blocks are boxes and the arrows that connect them. If you have submitted any research paper the last 10 years you have almost inevitably been asked to produce a flowchart on how you generated your data. While there are excellent click-and-draw tools I have always found it to be much nicer to code my charts. In this post I will go through some of the abilities in my Gmisc package that makes this process smoother. Continue reading

News in htmlTable 2.0

A short intro to the new features in htmlTable 2.0. The image is a blend based on a CC image by Ken Xu.

The htmlTable 2.0 package was just released on CRAN! It is my most downloaded package with 160 000+ downloads/month and this update is something that I have been wanting to do for a long time. For those of you that never encountered htmlTable it is a package that takes a `matrix`/`data.frame` and outputs a nicely formatted HTML table. When I created the package there weren’t that many alternatives and knitr was this new thing that everyone was excited about, magrittr with its ubiquitous `%>%` pipe had not even entered the scene. The current update should make it easier to streamline table look, separate layout from content and use tidyverse functionality. Continue reading

Surgery for sacroiliac joint dysfunction, is this a thing?

A nice surprise article from JBJS on low back pain. The image is CC by Wanderer99.

Low back pain has a history of failed trials supporting surgery (see Brox et al. and Fairbank et al.) and I was therefore thrilled when I encountered Dengler et al.’s randomized controlled trial (RCT) on sacroiliac joint dysfunction. They showed that arthrodesis outperformed conservative management both early and up to two years after surgery. Continue reading

Long-awaited updates to htmlTable

Lets celebrate 2019 with some updates to my most popular package ever, the htmlTable. The image is CC by Thomas Hawk

One of the most pleasant surprises has been the popularity of my htmlTable-package with more than 100k downloads per month. This is all thanks to more popular packages relying on it and the web expanding beyond its original boundaries. As a thank you I have taken the time to update and fix some features (the 1.13 release) – enjoy! Continue reading