ewing Demos
  • Home
  • Gallery
  • sysetholApp
  • IsleRoyaleApp
  • fivePlotApp
  • fiveShowApp
  • tempApp
  • triangleApp
  • hexmapApp
  • hexmoveApp

triangleApp (Shinylive)

An interactive spatial substrate network explorer for tridiagonal coordinate systems and plant module layouts.
Author

Brian S. Yandell

← Back to Demos Gallery

The live application below is running completely client-side in your browser using serverless Shinylive (WebAssembly). You can interactively adjust substrate width, step density, toggle plant modules, and inspect spatial grid statistics.

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 750
#| components: [viewer]

library(shiny)
library(bslib)
library(ggplot2)
library(cowplot)
library(stats)
library(graphics)

# --- Source: triangle.R ---
## $Id: triangle.R,v 0.9 2002/12/09 yandell@stat.wisc.edu Exp $
##
## Functions for Bland Ewing's modeling.
##
##     Copyright (C) 2000,2001,2002 Brian S. Yandell.
##
## This program is free software; you can redistribute it and/or modify it
## under the terms of the GNU General Public License as published by the
## Free Software Foundation; either version 2, or (at your option) any
## later version.
##
## These functions are distributed in the hope that they will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## The text of the GNU General Public License, version 2, is available
## as http://www.gnu.org/copyleft or by writing to the Free Software
## Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
##
###########################################################################################
## rtri( n, width )
##
## plot.current( x, species )
## text.current( x, species )
##
###########################################################################################
###########################################################################################
### Tridiagonal Coordinate System S3 Classes & Algebra
###########################################################################################
tricoord <- function(a, b = NULL, c = NULL) {
  if (is.data.frame(a) && all(c("a", "b", "c") %in% names(a))) {
    res <- a
  } else if (is.matrix(a) && ncol(a) == 3) {
    res <- as.data.frame(a)
    names(res) <- c("a", "b", "c")
  } else if (!is.null(b) && !is.null(c)) {
    res <- data.frame(a = a, b = b, c = c)
  } else if (is.numeric(a) && length(a) == 3) {
    res <- data.frame(a = a[1], b = a[2], c = a[3])
  } else {
    stop("Invalid tricoord input format")
  }
  class(res) <- c("tricoord", "data.frame")
  res
}

`+.tricoord` <- function(e1, e2) {
  # Handle vector offsets cleanly
  if (is.numeric(e2) && length(e2) == 3) {
    tricoord(e1$a + e2[1], e1$b + e2[2], e1$c + e2[3])
  } else if (inherits(e2, "tricoord")) {
    tricoord(e1$a + e2$a, e1$b + e2$b, e1$c + e2$c)
  } else {
    stop("Invalid right hand operand for tricoord addition")
  }
}

`-.tricoord` <- function(e1, e2) {
  if (is.numeric(e2) && length(e2) == 3) {
    tricoord(e1$a - e2[1], e1$b - e2[2], e1$c - e2[3])
  } else if (inherits(e2, "tricoord")) {
    tricoord(e1$a - e2$a, e1$b - e2$b, e1$c - e2$c)
  } else {
    stop("Invalid right hand operand for tricoord subtraction")
  }
}

###########################################################################################
rtri <- function( n, width, tri = matrix(0,3,n), roundoff = TRUE )
{
  tri <- as.matrix( tri )
  if( n == 1 ) {
    xy <- stats::runif( 2, 0, width )
    if( roundoff )
      xy <- round( xy )

    i <- sample( 3, 1  )
    i1 <- 1 + i%%3
    i2 <- 1 + (i+1)%%3
    tri[i1,] <- tri[i1,] + xy[1]
    tri[i2,] <- tri[i2,] - xy[2]
    tri[i,] <- - ( tri[i1,] + tri[i2,] )
    return( tri )
  }
  else {
    xy <- data.frame( x = stats::runif( n, 0, width ),
      y = - stats::runif( n, 0, width ))
    if( roundoff )
      xy <- round( xy )

    out <- sample( 3, n, replace = TRUE )
    for( i in 1:3 ) {
      outi <- out == i
      if( any( outi )) {
        i1 <- 1 + i%%3
        i2 <- 1 + (i+1)%%3
        tri[i1,outi] <- tri[i1,outi] + xy$x[outi]
        tri[i2,outi] <- tri[i2,outi] + xy$y[outi]
        tri[i,outi] <- - ( tri[i1,outi] + tri[i2,outi] )
      }
    }
  }
  tri
}
###########################################################################################
car2tri.default <- function(x,y)
  car2tri( cbind( x, y ))
car2tri <- function( xy, xmult = ( 2 + sq3 ) / 4, ymult = ( 3 + 2 * sq3 ) / 12,
  sq3 = sqrt( 3 ))
{
#  if( !is.matrix( xy ))
#    xy <- t( as.matrix( xy ))
  aa <- xmult * xy[,1] - ymult * xy[,2]
  bb <- - xmult * xy[,1] - ymult * xy[,2]
  cc <- -( aa + bb )
  rbind( a = aa, b = bb, c = cc )
}
###########################################################################################
tri2car.default <- function(aa,bb,cc=-(aa+bb))
  tri2car( rbind( aa, bb, cc ))
tri2car <- function(tri, xmult = 2 / ( 2 + sq3 ), ymult = 6 / ( 3 + 2 * sq3 ),
  sq3 = sqrt( 3 ))
{
  if( inherits(tri, "tricoord") ) {
    # If the user passes our S3 tricoord dataframe, map it correctly natively.
    x <- ( tri$a - tri$b ) * xmult
    y <- - ( tri$a + tri$b ) * ymult
  } else {
    if( !is.matrix( tri ))
      tri <- as.matrix( tri )
    x <- ( tri[1,] - tri[2,] ) * xmult
    y <- - ( tri[1,] + tri[2,] ) * ymult
  }
  data.frame( x = x, y = y )
}
###########################################################################################
cardist <- function( xy )
  sqrt( xy[,1]^2 + xy[,2]^2 )
###########################################################################################
tridist <- function( tri )
  apply( tri, 1, max )
###########################################################################################
gasket <- function( aa, bb )
{
  n <- length( aa )
  pp <- c(-1,1,0,1)
  dda <- diff( aa )
  ddb <- diff( bb )
  ss <- sign( sign( dda ) - sign( ddb ))
  dda <- 2 - abs( dda )
  ddb <- 2 - abs( ddb )
  aa <- 2 * aa
  aa <- c( aa[1], rbind( aa[-n] + pp[dda+1+ss], aa[-1] + pp[dda+1-ss], aa[-1] ))
  bb <- 2 * bb
  bb <- c( bb[1], rbind( bb[-n] + pp[ddb+1-ss], bb[-1] + pp[ddb+1+ss], bb[-1] ))
  data.frame( aa = aa, bb = bb )
}

# --- Source: substrate_triangle.R ---
get_substrate_grid <- function(width, step = 1, orientation = "up") {
  pts <- expand.grid(a = seq(0, width - step, by = step), 
                     b = seq(0, width - step, by = step))
  
  if (orientation == "up") {
    pts <- subset(pts, a + b <= width - step)
    pts$c <- -(pts$a + pts$b)
  } else {
    pts <- subset(pts, a + b <= width - step)
    pts$a <- -pts$a
    pts$b <- -pts$b
    pts$c <- -(pts$a + pts$b)
  }
  return(pts)
}

substrate_topology <- function(width = 10, step = 1) {
  W <- width - step
  
  # Topology adjacency offsets
  list(
    fr2   = list(offset = c(0, 0, 0), dir = "down"),
    fr1   = list(offset = c(-W, -W, 2*W), dir = "up"),
    fr3   = list(offset = c(-W, 0, W), dir = "up"),
    fr4   = list(offset = c(0, -W, W), dir = "up"),
    tw1   = list(offset = c(-W, 0, W), dir = "down"),
    twig  = list(offset = c(-W, 0, W), dir = "down"),
    tw2   = list(offset = c(-2*W, 0, 2*W), dir = "up"),
    lftop = list(offset = c(-2*W, W, W), dir = "down"),
    lfbot = list(offset = c(-3*W, W, 2*W), dir = "up")
  )
}

create_substrate <- function(topology, width = 10, step = 1) {
  W <- width - step
  all_points <- data.frame()
  labels_df <- data.frame()
  poly_df <- data.frame()
  
  for (sub in names(topology)) {
    cfg <- topology[[sub]]
    grid <- get_substrate_grid(width, step, cfg$dir)
    
    o_a <- cfg$offset[1]
    o_b <- cfg$offset[2]
    o_c <- cfg$offset[3]

    # Needs tricoord and tri2car which are presumably exported/available from R/triangle.R
    grid_tri <- tricoord(grid$a, grid$b, grid$c)
    grid_tri <- grid_tri + cfg$offset
    
    car_pts <- tri2car(grid_tri)
    car_pts$substrate <- sub
    all_points <- rbind(all_points, car_pts)
    
    # Determine bounds and midpoints for side labels 1,2,3
    if (cfg$dir == "up") {
      v_top <- c(o_a, o_b, o_c)
      v_br  <- c(o_a + W, o_b, o_c - W)
      v_bl  <- c(o_a, o_b + W, o_c - W)
      
      m1 <- (v_top + v_bl) / 2
      m2 <- (v_top + v_br) / 2
      m3 <- (v_bl + v_br)  / 2
      centroid <- (v_top + v_br + v_bl) / 3
      p_mat <- cbind(v_top, v_br, v_bl)
    } else {
      v_bot <- c(o_a, o_b, o_c)
      v_tr  <- c(o_a, o_b - W, o_c + W)
      v_tl  <- c(o_a - W, o_b, o_c + W)
      
      m1 <- (v_bot + v_tr) / 2
      m2 <- (v_bot + v_tl) / 2
      m3 <- (v_tl + v_tr)  / 2
      centroid <- (v_bot + v_tr + v_tl) / 3
      p_mat <- cbind(v_bot, v_tr, v_tl)
    }
    
    # Interpolate slightly towards the centroid to put text "just inside" the edges
    w_in <- 0.25 # weight towards centroid
    l1 <- m1 * (1 - w_in) + centroid * w_in
    l2 <- m2 * (1 - w_in) + centroid * w_in
    l3 <- m3 * (1 - w_in) + centroid * w_in
    
    mat_l <- cbind(l1, l2, l3)
    car_l <- tri2car(mat_l)
    car_l$label <- c("1", "2", "3")
    car_l$substrate <- sub
    labels_df <- rbind(labels_df, car_l)
    
    car_p <- tri2car(p_mat)
    car_p$substrate <- sub
    poly_df <- rbind(poly_df, car_p)
  }
  
  centers <- stats::aggregate(cbind(x,y) ~ substrate, data=all_points, mean)
  
  obj <- list(
    points = all_points, 
    labels = labels_df, 
    poly = poly_df, 
    centers = centers,
    topology = topology
  )
  class(obj) <- "substrate"
  return(obj)
}

autoplot.substrate <- function(object, ...) {
  ggplot2::ggplot() +
    # Draw the black boundary lines outlining the substrates exactly over outer dots
    ggplot2::geom_polygon(data=object$poly, ggplot2::aes(x=x, y=y, group=substrate), fill=NA, color="black", linewidth=0.7) +
    # Plot grid dots
    ggplot2::geom_point(data=object$points, ggplot2::aes(x=x, y=y, color=substrate), size=1.5) +
    # Plot Substrate Labels (Centers)
    ggplot2::geom_text(data=object$centers, ggplot2::aes(x=x, y=y, label=substrate), color="black", fontface="bold", size=5) +
    # Axis side numbers
    ggplot2::geom_text(data=object$labels, ggplot2::aes(x=x, y=y, label=label), color="darkred", fontface="bold", size=4) +
    ggplot2::theme_void() +
    ggplot2::coord_fixed() +
    ggplot2::ggtitle("Ewing Tridiagonal Substrate Network Mapping")
}

create_hex_overlay <- function(object, step = 1) {
  pts <- if (inherits(object, "substrate")) object$points else object
  if (is.null(pts) || nrow(pts) == 0) return(data.frame())
  
  xmult <- 2 / (2 + sqrt(3))
  ymult <- 6 / (3 + 2 * sqrt(3))
  d <- step * sqrt(xmult^2 + ymult^2)
  r <- d / sqrt(3)
  
  angles <- (seq(0, 5) * 60 + 30) * pi / 180
  dx <- r * cos(angles)
  dy <- r * sin(angles)
  
  n_pts <- nrow(pts)
  hex_list <- vector("list", n_pts)
  
  for (i in seq_len(n_pts)) {
    px <- pts$x[i] + dx
    py <- pts$y[i] + dy
    sub <- pts$substrate[i]
    hex_list[[i]] <- data.frame(
      x = px,
      y = py,
      cell_id = i,
      substrate = sub,
      stringsAsFactors = FALSE
    )
  }
  
  do.call(rbind, hex_list)
}


# --- Source: triangleApp.R ---
triangleApp <- function(width = 10, step = 1, title = "Tridiagonal Substrate Network Explorer") {
  
  app_theme <- bslib::bs_theme(
    version = 5,
    bg = "#ffffff",
    fg = "#212529",
    primary = "#1a73e8",
    secondary = "#7209b7",
    success = "#2ec4b6"
  )
  
  all_modules <- c("fr1", "fr2", "fr3", "fr4", "tw1", "tw2", "lftop", "lfbot")
  
  ui <- bslib::page_sidebar(
    title = title,
    theme = app_theme,
    
    sidebar = bslib::sidebar(
      width = 300,
      shiny::div(
        style = "font-size: 0.85rem; padding: 5px 0;",
        shiny::div(
          style = "display: flex; gap: 8px; align-items: flex-end; margin-bottom: 6px;",
          shiny::div(
            style = "flex: 1;",
            shiny::span("Width (Radius):", style = "font-weight: 600; color: #1a73e8; display: block; margin-bottom: 2px;"),
            shiny::numericInput("width_in", NULL, value = width, min = 2, max = 30, step = 1)
          ),
          shiny::div(
            style = "flex: 1;",
            shiny::span("Step Density:", style = "font-weight: 600; color: #1a73e8; display: block; margin-bottom: 2px;"),
            shiny::numericInput("step_in", NULL, value = step, min = 0.5, max = 5, step = 0.5)
          )
        ),
        shiny::div(style = "border-top: 1px solid rgba(0,0,0,0.1); margin: 6px 0;"),
        shiny::div(
          style = "margin-bottom: 6px;",
          shiny::span("Substrate Modules:", style = "font-weight: 600; color: #7209b7; display: block; margin-bottom: 2px;"),
          shiny::selectizeInput("modules_in", NULL,
                                choices = all_modules,
                                selected = all_modules,
                                multiple = TRUE,
                                options = list(plugins = list("remove_button")))
        ),
        shiny::div(style = "border-top: 1px solid rgba(0,0,0,0.1); margin: 6px 0;"),
        shiny::div(
          style = "margin-bottom: 6px;",
          shiny::span("Display Layers:", style = "font-weight: 600; color: #2ec4b6; display: block; margin-bottom: 2px;"),
          shiny::checkboxGroupInput("layers_in", NULL,
                                    choices = c("Boundaries" = "poly", "Dots" = "points", "Labels" = "centers", "Numbers" = "labels"),
                                    selected = c("poly", "points", "centers", "labels"),
                                    inline = TRUE)
        ),
        shiny::div(style = "border-top: 1px solid rgba(0,0,0,0.1); margin: 6px 0;"),
        shiny::actionButton("reset_btn", "Reset Defaults", class = "btn-outline-secondary btn-sm w-100 mt-1")
      )
    ),
    
    shiny::tags$head(
      shiny::tags$link(rel = "stylesheet", href = "https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap"),
      shiny::tags$style(shiny::HTML("
        body {
          background-color: #f8f9fa;
          font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        }
        .card {
          background: #ffffff !important;
          border: 1px solid rgba(0, 0, 0, 0.08) !important;
          border-radius: 12px !important;
          box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
          transition: all 0.3s ease;
          margin-bottom: 20px;
        }
        .card:hover {
          border-color: rgba(26, 115, 232, 0.3) !important;
        }
        .card-header {
          background: rgba(0, 0, 0, 0.02) !important;
          border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
          font-weight: bold;
        }
        .sidebar {
          background: #ffffff !important;
          border-right: 1px solid rgba(0, 0, 0, 0.08) !important;
        }
        .stat-badge {
          background: #f1f3f5;
          padding: 8px 12px;
          border-radius: 8px;
          margin-bottom: 8px;
        }
      "))
    ),
    
    shiny::fluidRow(
      shiny::column(
        width = 8,
        bslib::card(
          bslib::card_header("Tridiagonal Substrate Spatial Network Mapping"),
          bslib::card_body(
            shiny::plotOutput("plot_substrate", height = "520px")
          )
        )
      ),
      shiny::column(
        width = 4,
        bslib::card(
          bslib::card_header("Substrate Grid Statistics & Topology"),
          bslib::card_body(
            shiny::uiOutput("summary_stats")
          )
        )
      )
    ),
    shiny::fluidRow(
      shiny::column(
        width = 12,
        bslib::card(
          bslib::card_header("Instructions & Substrate Network Background"),
          bslib::card_body(
            shiny::HTML("
              <p><strong>Instructions:</strong> Adjust the <em>Width (Radius)</em> and <em>Step Density</em> to dynamically scale the tridiagonal coordinate system. Use the module selector to filter specific plant components (fruits <code>fr1..fr4</code>, twigs <code>tw1..tw2</code>, leaves <code>lftop..lfbot</code>) or toggle layer visibility.</p>
              <p><strong>Background:</strong> In Bland Ewing's population ethology model, individuals navigate a triangular substrate network represented by 3D isometric coordinates <code>(a, b, c)</code> where <code>c = -(a + b)</code>. Geometric patch topologies seamlessly map connectivity between plant micro-habitats.</p>
            ")
          )
        )
      )
    )
  )
  
  server <- function(input, output, session) {
    
    shiny::observeEvent(input$reset_btn, {
      shiny::updateNumericInput(session, "width_in", value = width)
      shiny::updateNumericInput(session, "step_in", value = step)
      shiny::updateSelectizeInput(session, "modules_in", selected = all_modules)
      shiny::updateCheckboxGroupInput(session, "layers_in", selected = c("poly", "points", "centers", "labels"))
    })
    
    substrate_data <- shiny::reactive({
      shiny::req(input$width_in, input$step_in)
      
      shiny::validate(
        shiny::need(input$width_in >= 2, "Width must be at least 2."),
        shiny::need(input$step_in > 0, "Step must be greater than 0."),
        shiny::need(input$width_in > input$step_in, "Width must be greater than Step density.")
      )
      
      tryCatch({
        topo <- substrate_topology(width = input$width_in, step = input$step_in)
        sub_obj <- create_substrate(topo, width = input$width_in, step = input$step_in)
        sub_obj
      }, error = function(e) {
        shiny::validate(paste("Error constructing substrate topology:", e$message))
      })
    })
    
    output$plot_substrate <- shiny::renderPlot({
      sub_obj <- substrate_data()
      shiny::req(sub_obj)
      
      mods <- input$modules_in
      layers <- input$layers_in
      
      if (!is.null(mods) && length(mods) > 0) {
        sub_obj$points  <- sub_obj$points[sub_obj$points$substrate %in% mods, ]
        sub_obj$poly    <- sub_obj$poly[sub_obj$poly$substrate %in% mods, ]
        sub_obj$labels  <- sub_obj$labels[sub_obj$labels$substrate %in% mods, ]
        sub_obj$centers <- sub_obj$centers[sub_obj$centers$substrate %in% mods, ]
      } else {
        sub_obj$points  <- sub_obj$points[0, ]
        sub_obj$poly    <- sub_obj$poly[0, ]
        sub_obj$labels  <- sub_obj$labels[0, ]
        sub_obj$centers <- sub_obj$centers[0, ]
      }
      
      p <- ggplot2::ggplot()
      if ("poly" %in% layers && nrow(sub_obj$poly) > 0) {
        p <- p + ggplot2::geom_polygon(data = sub_obj$poly, ggplot2::aes(x = x, y = y, group = substrate), 
                                      fill = NA, color = "black", linewidth = 0.7)
      }
      if ("points" %in% layers && nrow(sub_obj$points) > 0) {
        p <- p + ggplot2::geom_point(data = sub_obj$points, ggplot2::aes(x = x, y = y, color = substrate), size = 1.6)
      }
      if ("centers" %in% layers && nrow(sub_obj$centers) > 0) {
        p <- p + ggplot2::geom_text(data = sub_obj$centers, ggplot2::aes(x = x, y = y, label = substrate), 
                                    color = "black", fontface = "bold", size = 4.5)
      }
      if ("labels" %in% layers && nrow(sub_obj$labels) > 0) {
        p <- p + ggplot2::geom_text(data = sub_obj$labels, ggplot2::aes(x = x, y = y, label = label), 
                                    color = "darkred", fontface = "bold", size = 3.5)
      }
      
      p + ggplot2::theme_void() + 
        ggplot2::coord_fixed() + 
        ggplot2::ggtitle(paste0("Substrate Network Topology (Width = ", input$width_in, ", Step = ", input$step_in, ")"))
    })
    
    output$summary_stats <- shiny::renderUI({
      sub_obj <- substrate_data()
      shiny::req(sub_obj)
      
      mods <- input$modules_in
      pts <- if (!is.null(mods)) sub_obj$points[sub_obj$points$substrate %in% mods, ] else sub_obj$points[0, ]
      
      total_pts <- nrow(pts)
      active_mods_count <- if (!is.null(mods)) length(mods) else 0
      
      counts <- if (total_pts > 0) table(pts$substrate) else numeric()
      counts_html <- if (length(counts) > 0) {
        paste0("<li><strong>", names(counts), ":</strong> ", counts, " points</li>", collapse = "")
      } else {
        "<li><em>No active modules selected</em></li>"
      }
      
      x_bounds <- if (total_pts > 0) paste0(round(range(pts$x), 2), collapse = " to ") else "N/A"
      y_bounds <- if (total_pts > 0) paste0(round(range(pts$y), 2), collapse = " to ") else "N/A"
      
      shiny::HTML(paste0("
        <div class='stat-badge'>
          <strong style='color: #1a73e8;'>Active Modules:</strong> ", active_mods_count, " / ", length(all_modules), "
        </div>
        <div class='stat-badge'>
          <strong style='color: #7209b7;'>Total Coordinate Dots:</strong> ", total_pts, "
        </div>
        <div class='stat-badge'>
          <strong style='color: #2ec4b6;'>X Span:</strong> ", x_bounds, "<br/>
          <strong style='color: #2ec4b6;'>Y Span:</strong> ", y_bounds, "
        </div>
        <hr style='margin: 10px 0; border-top: 1px solid rgba(0,0,0,0.1);'/>
        <h5 style='font-size: 0.95em; font-weight: bold;'>Points per Substrate:</h5>
        <ul style='padding-left: 18px; font-size: 0.85em; line-height: 1.5em;'>
          ", counts_html, "
        </ul>
      "))
    })
  }
  
  shiny::shinyApp(ui = ui, server = server)
}

# --- Launch Application ---
triangleApp()

Programmatic Application Usage

Launch the interactive application natively in R:

library(ewing)

triangleApp()