From 758807af67adbda6a9f26902b88efb3f78e06e92 Mon Sep 17 00:00:00 2001 From: Douglas Ezra Morrison Date: Wed, 3 Jun 2026 02:47:04 -0700 Subject: [PATCH 01/17] Add concept-map appendix: dependency diagram + descendants table (#872) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New appendix `chapters/concept-map.qmd` visualizes how every definition and result in the notes depends on the others: - a color-by-type dependency diagram, with node size proportional to the number of descendants so the most foundational results stand out, and - a table listing each result's direct and indirect descendants (live `@ref` cross-reference links), sorted by number of direct descendants. The graph is built by `data-raw/callout-graph.R`, which scans the `.qmd` sources and saves the result to `inst/extdata/callout-graph.rds`. The appendix reads that saved artifact, so the scan does not re-run on every render — re-run the script when divs are added, removed, or re-titled. References are attributed to the enclosing callout, or to the callout an immediately-following proof/solution div belongs to; references that aren't inside a result (plain prose) don't create edges. Stacked on the title-audit PR (#875), which gives every node a concise label. Registered as the final appendix in the book and website configs. Co-Authored-By: Claude Opus 4.8 (1M context) Co-authored-by: d-morrison <2474437+d-morrison@users.noreply.github.com> --- _quarto-book.yml | 1 + _quarto-website.yml | 1 + chapters/concept-map.qmd | 156 ++++++++++++++++++++++++++++++++ data-raw/callout-graph.R | 160 +++++++++++++++++++++++++++++++++ inst/extdata/callout-graph.rds | Bin 0 -> 14758 bytes 5 files changed, 318 insertions(+) create mode 100644 chapters/concept-map.qmd create mode 100644 data-raw/callout-graph.R create mode 100644 inst/extdata/callout-graph.rds diff --git a/_quarto-book.yml b/_quarto-book.yml index 96ead57a0..cd3456b1f 100644 --- a/_quarto-book.yml +++ b/_quarto-book.yml @@ -56,6 +56,7 @@ book: - chapters/CONTRIBUTING.qmd - chapters/exam-formula-sheet.qmd - chapters/practice-exam-mle-linreg.qmd + - chapters/concept-map.qmd back-to-top-navigation: true page-navigation: true diff --git a/_quarto-website.yml b/_quarto-website.yml index 1449c3860..9671ff085 100644 --- a/_quarto-website.yml +++ b/_quarto-website.yml @@ -35,6 +35,7 @@ project: - chapters/CONTRIBUTING.qmd - chapters/exam-formula-sheet.qmd - chapters/package-versions.qmd + - chapters/concept-map.qmd website: title: "Regression Models for Epidemiology" diff --git a/chapters/concept-map.qmd b/chapters/concept-map.qmd new file mode 100644 index 000000000..36ae5ab2d --- /dev/null +++ b/chapters/concept-map.qmd @@ -0,0 +1,156 @@ +--- +title: "Concept Map: Definitions and Results" +--- + +{{< include shared-config.qmd >}} + +This appendix shows how the definitions and results +(`def`, `thm`, `lem`, `cor`, `prp` callouts) +in the notes depend on one another. + +We say result $B$ is a **descendant** of result $A$ +if $A$ is referenced inside the statement or proof of $B$ +(directly, or transitively through a chain of intermediate results). +The more descendants a result has, +the more of the rest of the notes rests on it --- +so descendant count is a rough measure of how foundational a result is. + +The dependency graph is built by `data-raw/callout-graph.R`, +which scans the source `.qmd` files and saves the result to +`inst/extdata/callout-graph.rds`. +This appendix reads that saved file rather than re-scanning the notes on +every render, so **re-run that script whenever divs are added, removed, or +re-titled** to refresh the diagram and table below. + +```{r} +#| label: load-concept-graph +#| code-fold: true +#| message: false +#| warning: false + +library(igraph) +library(ggraph) + +cg <- readRDS(here::here("inst/extdata/callout-graph.rds")) + +type_levels <- c("def", "thm", "lem", "cor", "prp") +type_labels <- c( + def = "Definition", thm = "Theorem", lem = "Lemma", + cor = "Corollary", prp = "Proposition" +) +type_palette <- c( + def = "#1b9e77", thm = "#d95f02", lem = "#7570b3", + cor = "#e7298a", prp = "#66a61e" +) +``` + +There are `r nrow(cg$nodes)` labeled definitions and results in the notes, +connected by `r nrow(cg$edges)` direct dependency links. + +## Dependency diagram + +@fig-concept-map shows the results that participate in at least one +dependency link, laid out so that connected results sit near each other. +Color encodes the type of result, +and **node size grows with the number of descendants**, +so the most foundational results stand out +(the largest, most-connected nodes are also labeled). +Results with no detected dependency links +(foundational definitions used only in prose, or standalone results) +are omitted from the diagram but appear in @sec-descendants-table +if they have descendants. + +:::{#fig-concept-map} + +```{r} +#| label: fig-concept-map-plot +#| code-fold: true +#| message: false +#| warning: false +#| fig-width: 11 +#| fig-height: 9 + +connected <- cg$nodes$id[cg$nodes$id %in% c(cg$edges$from, cg$edges$to)] +core <- graph_from_data_frame( + cg$edges, + vertices = cg$nodes[cg$nodes$id %in% connected, ], + directed = TRUE +) +V(core)$type <- factor(V(core)$type, levels = type_levels) + +set.seed(204) +ggraph(core, layout = "stress") + + geom_edge_link( + arrow = arrow(length = unit(1.8, "mm"), type = "closed"), + end_cap = circle(1.6, "mm"), + edge_alpha = 0.25, edge_width = 0.3 + ) + + geom_node_point(aes(size = n_desc + 1, color = type)) + + geom_node_text( + aes(label = ifelse(n_desc >= 3, name, "")), + repel = TRUE, size = 2.7, max.overlaps = 30, segment.alpha = 0.3 + ) + + scale_color_manual( + values = type_palette, labels = type_labels, name = "Type", drop = FALSE + ) + + scale_size_continuous(range = c(1.5, 9), guide = "none") + + theme_void() + + theme( + legend.position = "bottom", + text = element_text(family = "serif") + ) +``` + +Dependency structure of the definitions and results in the notes. +An arrow points from a result to each result that uses it. +Node and label size are proportional to the total number of descendants. + +::: + +## Descendants of each result {#sec-descendants-table} + +@tbl-descendants lists every result that has at least one descendant, +sorted by the number of **direct** descendants. +For each result, the first column links to its direct descendants +(results that reference it directly) +and the second column links to its indirect descendants +(results that reach it only through a chain of intermediate results). +All entries are live cross-reference links. + +::: {#tbl-descendants} + +```{r} +#| label: tbl-descendants-build +#| code-fold: true +#| message: false +#| warning: false + +ref_list <- function(ids) { + if (!length(ids)) return("—") + ids <- ids[order(match(sub("-.*", "", ids), type_levels), ids)] + paste0("@", ids, collapse = ", ") +} + +ranked <- cg$nodes[cg$nodes$n_direct >= 1, ] +ranked <- ranked[order(-ranked$n_direct, -ranked$n_desc, ranked$id), ] + +direct_col <- vapply( + ranked$id, function(v) ref_list(cg$descendants[[v]]$direct), character(1) +) +indirect_col <- vapply( + ranked$id, function(v) ref_list(cg$descendants[[v]]$indirect), character(1) +) +descendant_table <- data.frame( + Result = paste0("@", ranked$id), + `Direct descendants` = direct_col, + `Indirect descendants` = indirect_col, + check.names = FALSE, row.names = NULL, stringsAsFactors = FALSE +) + +knitr::kable(descendant_table, format = "pipe", align = "lll") +``` + +Direct and indirect descendants of every result that has at least one +descendant, sorted by number of direct descendants. + +::: diff --git a/data-raw/callout-graph.R b/data-raw/callout-graph.R new file mode 100644 index 000000000..512585c27 --- /dev/null +++ b/data-raw/callout-graph.R @@ -0,0 +1,160 @@ +# Build the dependency graph of definitions and results in the lecture notes. +# +# This scans every `.qmd` for `def`/`thm`/`lem`/`cor`/`prp` callout divs and the +# cross-references (`@type-id`) inside each one, then saves the resulting graph +# to `inst/extdata/callout-graph.rds`. The `concept-map.qmd` appendix reads that +# saved artifact, so the scan does NOT re-run on every render. +# +# Re-run this script (from the repo root) whenever divs are added, removed, or +# re-titled: +# +# Rscript data-raw/callout-graph.R + +library(igraph) +library(stringr) + +`%||%` <- function(a, b) if (is.null(a)) b else a + +# Scan all `.qmd` files and return a list of `nodes` and `edges` data frames. +# +# A reference creates a dependency edge from the referenced result to the result +# whose statement or proof contains it. References are attributed to: +# * the enclosing callout div, if the reference is inside one; otherwise +# * the callout that an enclosing `proof`/`solution` div *immediately* follows +# (only blank lines and `---`/slidebreak separators may sit between them). +# References that are neither inside a callout nor inside a directly-attached +# proof (e.g. plain prose) are not turned into edges. +extract_callout_graph <- function(root) { + qmds <- list.files( + c(file.path(root, "chapters"), file.path(root, "_subfiles")), + pattern = "[.]qmd$", recursive = TRUE, full.names = TRUE + ) + # `chapters/_subfiles` is a symlink to `_subfiles`; drop the duplicate paths. + qmds <- qmds[!grepl("/chapters/_subfiles/", qmds)] + + open_re <- "^:::+\\s*\\{#(def|thm|lem|cor|prp)-([A-Za-z0-9_-]+)([^}]*)\\}\\s*$" + head_re <- "^#{2,6}\\s+(.*\\S)\\s*$" + ref_re <- "@((?:def|thm|lem|cor|prp)-[A-Za-z0-9_-]+)" + sep_re <- "^(-{3,}|\\{\\{< *slidebreak *>\\}\\})$" # adjacency-preserving lines + + nodes <- list() + edges <- list() + + for (f in qmds) { + lines <- readLines(f, warn = FALSE) + stack <- list() # open fenced divs, innermost last; may carry $owner + pending <- NA_character_ # callout just closed, still adjacent to a proof + n <- length(lines) + for (i in seq_len(n)) { + l <- lines[i] + + mo <- str_match(l, open_re) + if (!is.na(mo[1, 1])) { + type <- mo[1, 2] + full <- paste0(type, "-", mo[1, 3]) + # The title is the heading on the first non-blank line inside the div. + title <- NA_character_ + for (j in seq(i + 1, min(i + 5, n))) { + if (j > n || str_trim(lines[j]) != "") { + hm <- str_match(lines[j], head_re) + if (!is.na(hm[1, 1])) title <- hm[1, 2] + break + } + } + nodes[[full]] <- data.frame( + id = full, type = type, + title = title %||% full, + file = sub(paste0(root, "/"), "", f, fixed = TRUE), + stringsAsFactors = FALSE + ) + stack[[length(stack) + 1]] <- list(kind = "callout", id = full) + pending <- NA_character_ + next + } + + if (grepl("^:::+", l)) { + content <- str_trim(sub("^:::+", "", l)) + if (nzchar(content)) { + # A proof/solution div directly following a callout inherits it. + is_proof <- grepl("proof|solution", content, ignore.case = TRUE) + owner <- if (is_proof) pending else NA_character_ + stack[[length(stack) + 1]] <- + list(kind = "div", class = content, owner = owner) + pending <- NA_character_ + } else if (length(stack)) { + top <- stack[[length(stack)]] + stack[[length(stack)]] <- NULL + pending <- if (top$kind == "callout") top$id else NA_character_ + } + next + } + + refs <- str_match_all(l, ref_re)[[1]] + if (nrow(refs)) { + target <- NA_character_ + for (k in rev(seq_along(stack))) { + s <- stack[[k]] + if (s$kind == "callout") { + target <- s$id + break + } + if (!is.null(s$owner) && !is.na(s$owner)) { + target <- s$owner + break + } + } + if (!is.na(target)) { + for (r in refs[, 2]) { + if (r != target) { + edges[[length(edges) + 1]] <- + data.frame(from = r, to = target, stringsAsFactors = FALSE) + } + } + } + } + + # Real content at the top level ends the previous callout's eligibility to + # bind a following proof (blank lines and separators don't count). + trimmed <- str_trim(l) + if (!length(stack) && trimmed != "" && !grepl(sep_re, trimmed)) { + pending <- NA_character_ + } + } + } + + nodes <- do.call(rbind, nodes) + rownames(nodes) <- NULL + edges <- if (length(edges)) unique(do.call(rbind, edges)) else + data.frame(from = character(), to = character()) + edges <- edges[edges$from %in% nodes$id & edges$to %in% nodes$id, , drop = FALSE] + rownames(edges) <- NULL + list(nodes = nodes, edges = edges) +} + +root <- here::here() +cg <- extract_callout_graph(root) + +# Precompute descendant counts and the direct/indirect descendant id lists, so +# the chapter does no graph analysis at render time. +ig <- graph_from_data_frame(cg$edges, vertices = cg$nodes, directed = TRUE) +cg$nodes$n_desc <- vapply( + cg$nodes$id, + function(v) length(subcomponent(ig, v, mode = "out")) - 1L, + integer(1) +) +cg$nodes$n_direct <- as.integer(degree(ig, mode = "out")[cg$nodes$id]) + +cg$descendants <- lapply(stats::setNames(cg$nodes$id, cg$nodes$id), function(v) { + direct <- setdiff(names(which(distances(ig, v, mode = "out")[1, ] == 1)), v) + reach <- setdiff(subcomponent(ig, v, mode = "out")$name, v) + list(direct = sort(direct), indirect = sort(setdiff(reach, direct))) +}) + +cg$generated_from <- "data-raw/callout-graph.R" + +saveRDS(cg, here::here("inst/extdata/callout-graph.rds")) + +message(sprintf( + "callout-graph.rds: %d results, %d dependency links (%d results with >=1 descendant)", + nrow(cg$nodes), nrow(cg$edges), sum(cg$nodes$n_direct >= 1) +)) diff --git a/inst/extdata/callout-graph.rds b/inst/extdata/callout-graph.rds new file mode 100644 index 0000000000000000000000000000000000000000..5912dcf12cd6d8357cb4513f9a244c528b8e0fd2 GIT binary patch literal 14758 zcmdVAQ;;t}u&3EJPP{kw6eC+`UJ-Hj<~ z)0<4@k%ghK;7J#yR?qIaat=1Gi+VY--6o$04)(OjO}Fo(^p21MA!7VPbD0Y&i_5zq z3n_s~?&aH&DK+<70Cz)iNMKbCQiS?S1jZ99U!G&)xlkUM^b%yiFs~Iq43ysnJP7AI za^tE@#s1Sb<(c@@{%&hl%fgg};~M|gwt`hOohEy5K^JvIAVJflx@j{;hUa|5cFq&I z#NxQMp)I_DU53Otq@y0+#31XXkcKVZV?M$a(63*Xl3mpAulVML?&ns(9S0i179_Im zT#L=Ow|(Y9i&dWQ0OOb1VK--XB*)6!C{ijW;(+5wW}v`j{yVfvf0#k-H2R!KFi+u| zAg-<*9mL*z_hth>jTmZ->|(-#CTaVn&~^z_L%~M-m|>?I+>)CyJb^Ey7}DV-f>d9l zDU~pS`OeD%(}hFIIvvG{5Y(gZnwm&T{XG@Z7l7d~iw2;X$$j-GB5{P?I6Von#ux3r z_h*#p;)q2o0KnH9?lp$;!T(XY?WnO0d^DS|T=xF4cNQ1qjhf{ZkJ zfC_eqOAKO$Z`Y4ZcuWFlPJ1MR8HWFwIfBEy;_DigOdT4#ae-NP+Oe^$Qt8s{r=|;L zP1+fjGgOFf7Jw+aJ^qGog&=qnK!AZW69a}>&tOv~78eYZ>ht*2#k?9F5-)4ml>6uc zvB=@xkpf|n0}#dXPllF_=z~CdNx(Gb7x%aYw83L#kWn$SBTTE&lpvMiVu%=1X3GLm z5mGd~{>GB-&A)7q84( z@&EzL@7h#yN;(DmO>6IFfn#_W17d^?$Nt;7vk17lSb4Q1xAXlZ@UKQMcF^I4Ns=fD zj&Y~MK0cTT0;ol(@JhRQaD#b#E9bW^R!iflNBatq(JvGb#xs54LXwHR^(*EJhd1*# z3(Sg@t{Boxt(Ad8{#IM9-WGSWR_6E;YY0gTzOddl1Q<8*ix4h`&EHpO$}K=qdwvQ? z3?Z+)=Tc?Tp!qA%O9mE(%4-%?MHU0Sm=hFIaz%hi+g}9{3O`>-w z4YHoDHkA8HdlkDN#KCMdCr!`5c_x9Rpz@y4%V)8;OPN_JJN>icPks4Rw~vb-`Cjv} zLaLPOxI>$XY)3t@kWtxhUiOXU2YBeu0{2j%)j?cdrrE0*2wlK{pp!K6AT#OHG=YTC zaQtG&!tC|#4yv@3Eq|?2^=UfMcPb|{X(_9Ru*@_rOdM3Dve>N5jz{{}^0gS6w-^{0 zN4oz0U14rdslA{G#ni{kS$Rxz0khCo(4zFxltRpyCQf(Kb#T6Ny;;CQrfD=+5V4%S z(SRX)MY`{h{@PUlQrKJ@l4EKlB2}fZ)MR!@?z8zABFL0PV`QN!cpsEE+lfdgig}># zzp=H|&}aGUtZ;t9+T&Usz#Br z-OSt!nM9vk6Y1F;*a&zq8NS0a!UvM@O0YbMWv|trK*23wk6ke<|5XdrsIU{DUH}P2 z)@QqV&+-elM4$x)B?hV-Psb5TP`_dD8Sv>y|fl1xV;K;g>r2Uty$1(NB{0{V>C)mcw1loZcV zBpT?zggQn=hJh{(7_;hln$GffWE>))ACHF9+gj?7O??`njg)@Gm+Xpa6u4=7qXm_+!r z<>CpqaFKeM6vlr$a!MDLo$!Mn%u|s$)Z_kocFB~hT!oY*HEue; z5j3^=>(S0itv|k}`-|6?5oa2T?*@W_ZD_c1nAYQd-CA5ig$C9Q>8n4oC;aUob%}~2 zUBgpeD*#8mion@rUQCC`*ZwTgmDz%kPA7R=gQ)|*)a@JKg;p*5E2(7PDucqiLeTtX zWh0+$a)gKNAVsFWd!c5-wKjx!kdN^e%Lv3EQ_3m>nBzb~6VD1SProf8$;I*2Mjkg^ z11TxSU_rBoW0*UMVr*(lLpedlIaJ17OgM7GLXGzQ0mKhqU*B5%e4t<2cOPAKe2lMABjoA1hx$ zIySygzz_xnfh8v6_pBuU+_aKaqBCIGOVvd=UQUs*zG}|j(XFi2dt*xCUfXk4Tugj~ z=K~lsMTeGbBH=0iCXZ25O>OLuL7zKozB0 z6>00Pgg$tV3@J)H{YT!G5>JMV8Q3f%=cPyO-8g2M`YwT2a(;KlsSbflUzrlgFBNCW z7Re;y*O(%FL9`Z5qURolDVw3pyFzlG2~u|KA9b+_pdn7EoFKQXYsjoDd+8T@*me3g za>+i~Dmd3veXbcU1|SB!0M^KeQlC0%2Kj6UgUR|Pj5I|<^PT3dacN(v7zhpVP76(c zcSxUgix~;fq;E(yY+*+X_H2acR4(FzYl(P^!O^wcw#*v0?kq#E!pEk7=f&#>lB;3Q zj;TLEs&;?|LE=3s$uL{+VUjjq1*bGn^yrXDNwsS6CV!`g{AF;JKq=PULL`(>*Tpvr ze-RG#rXj8{u=g*Y??3Z=!zpH*b1)}WxAa^VfTluQif4NW(R>1TBP~=X0G@3Qi5GBq zOHc#ZR+3LfKV3Woi}eFRQd(fQIOeu#r@n-Uj(N}wVAcw497gT3FGdQ&xF%Tyd^ZOz z2|~KPd+CeG0Uex_5+msC8kHWC!v(uYhT6I|5qQJys;C>4jEQx*xz{|r&4AquZkB{5 z6)h#BY6}rjvswVXiRMI0RTzU{qG2iHlBc?Ac(#vo&mY@Qd*Y7mD#CnNd=ghZ63hX> z!BS@VBA=X)(&Adta#L16bQV+-MpVNojQ!bXLgQwxl(gNgY}HQy9Wl^na~4fCOIyPw zo5Q|vzl4lzE{pCW^-;xO_H3BBz0@mQone;*UgVo#%Ms46GVnf>E?CL!UV2FHNJ{Y~ z=I{=2TpF7-BT-8aelU0MIF@|8Kq;uz<_7QT2rY@lE|4IgX-Y3q^DLahj4fJ4!&`FI z!V%9{8cn|vAY!p_xj~+f{`3r$k(q#YO4YY?4U@MKLM~Mlr&{C)BzuNT_vJ0-Z__ou zbqN9Katr>^c#gsh8+bw{FOxfG`-p{s?PesNGl^tY?0EHcZlES_Do~S;D~ZoUXU2%B+6^ zZKbV{%XR{8MGZp)$nWvQbC_kDGEr;|17jy$Z5>=i=8Aby)&@;tymBhuqR-f+Wxi!$ zzM>XBl0sc=eciwfUk7;JaeKAY*iW;k!|YHNed93QKp$3UH-zv+w# z(gyL;{%@W`M~3lWzHXZ%>3@+}BY8e=poDyfQGx&u_fV zwtIEja_({N~ZpAK2Ddnqgs-5LX*lg9@A>kS~{lc3v13X;LNuKtf zqdgC`Z8D7~kQn9}^TiFKW-nH9Uyo1ecHE}kAoxg6VD)~w2nlbaaKkj!Q@Wv3ZWj8oJNX zIf0uTA6~yZUfnd`%bn|5DD^BAC z7Gh_7JqH(jp5g~f2Fm>F*mdnfLE8Wie zHvCWV4IS>q#%>9Fo2l!_%2 zV%Qe7km8rRTrRnKo~bIkt2X1nmp;_=d9dKs^pCdVN#gI8^KJ~2)o4cr`ohSXh8Z00 zy+@M%_@<3it89mG>f#bm_mENc>IL7;iS{c{)(Uh9+?+gtxr!KTn(XFyy~4e(;yQUR zuOe^loz5ORR9s*2bTeN2u>sq2%VPV1U1~jieph>WTG!kYQoZOW3)SSnRXsaAra?9o znaEXVUf7nWtG)e=sAFvz9lL2Af3&VeKAl>JNk4n%Nxf^GyT7;X4#7X|hQU7_V`@k6 zK(ovJC!eS7hE4e6RLL}Nnn=WX&6ZV%kTGao%}h*DzHMj?$XVBiH5%Ze*~{{`Ba1o) zFMvU`9>zkG?d?|jmm93bJ)Pr5lpyVEHpmK&61`J=RJ}#fi7z&DMTwzu&QKW!_FWKs zwLMTxdQ9FoR&p}U{uX>`EK}IMaF~6%H}lris_@P^=PUoF;a?r`9I`>WtX4gV?y)4+ zG3Yoh*P2=dK%43O>2)8~5QUojx+VQKm@A){pkDnPyAEx@D&Lh|MhtrneKNMeOs-Us zIba*q4!(G)@CPaW6{SPE(bB^`oub?RxWsldWr{pr-iKWrPSmsuPOul)1EI1){1Ym> zn2@iLl8kLcO(aIf+q|tIw{;JcN`VpTtjU^EWBb>@7!i@$O8~!CI?P-;37&sw>-VEb z%u!>*93Pe(vw|sZ&?*}29izB$alZ4>K*Hhagk9=}ZK&LEw_hP^+2s$A+LVgAL&L#Pm z+QO%e!&K>*jhf_TTjo1}YQ4vM^YmxCj2-AecS#5#rT4y6D1!kZ$!{=Kfq^h2{xpOk zm4XCEm(po4eRI(K$D=bS)v;d1i*H+v8+$`C0{4&%!@ZUSKWgINw1pAs!j88*E8wm! zw=_YEvq-r6;qMb?EcuBNqh%}D)j6IPj6xA*KQnZ!IYC~-@~7$f?ZbplYh=DNu76he z59}xTLRPA>VXTzog~A4WA`AGyw}u8MKMZ6(2j}0(P(%k8)Vw>>=UX2^h z$ah5nnI81SpR>!79{%7vM%v5?{!&5$P^DU;37iV;VMpTaYf9t%-|}5qs zSY2*to?jOZ=dq^`GO|KN9~De6)IHz2;C|V*y4Z@tmKd{oj+InWTIL0a+lfkn} z$=>#EKphB2FvdUia84lGF?pL<&dnFyzL_S3NMA{3lH4BY!we&vbEF-Kl9Gnd(>BzN z5tD(-@IF!{Bw$)ZnGtFeE{+ z75pb4v}(P_`Bdx3Mq}{j0^GTC0zcIHfFwG%2@2(pb78ZKL=@6@^H6DWl8KL&jL22S zFMnbimTLaMHuAJa)odwQs2I1qZPOHX5(LQl%vK|M7dg}5jcms(Xs$0&QgU#KUu#tue-R~ZQHW}te3zg}&O#s=**(ahPf~3{PMv5$=5`->z9&@aCcRI{K z-lAuoDF;H=9L4MTt)vV&ud9`UXJc7M!@0JUS7W3kH2+AvjPMN(x1(EC^uM8Iy^YkA zErC3_908y#N`o!~7oe2Jmg#itxVAucoNy=UVT|4iI*j8hb+=KHtbv((`M$Hj>1u1&3hX0lyj{RO8YsqfNb7h)aMF{agLgjqh+l z`(j%abPylh=VDde={~Bhi9*qkk}Vy%8SD5*;f)*t_!CRF9%Znqg`)Y-OVDyxg9`D5 zONvV^#(9G)ciW`PZynuCdWA-;J>#lxJf3&01Jpjwy zf!OExE(@!{s{JUWK&=`J53_=(jZK)wRI-}8JOBPg9?|iJm5*Ei+PmfG%D36z3R`s_&V937&DPVKRr}BBA}+kt-Wh_RvQGfc^u{S8`AI4N+Hy56m1`~c1u&G?S4}g ziIciHBdmT?ynOR#&p>=5omSAIQbMZotc)36RJ8!&O{esT-md`{ppxS z9Y99o!&8qR=2~W+AZOuJaby(Kus$TH`AeHJQ3z?4`F}xhnHmv<(M!<1=o$VZbwrd)nPu?nMJghwcECh%}yr(pkbvc0g}RFA4Bb2OE?l*pTN;#La{O3XIf2BBE3 za4nuQhjHY`zev_CpntY2qGc}QmCXa&YDbs7oV~}%7k=UYXp^(_d=onhd3MFUp za^!igsj~aZ&AgDbp!F$4c3mGeoK3rjX}z6dqq-AEb5<~xSg1)hq|Xrt1Ey;0wm;vMu5}>o1|WY2o6KmdiXy< z+Fb<5dZ^Z;_kQ`)mxSr0&-&39`rKoR0h)>KydJ4|jsu%1kaH3iOrkr>p3tfw0kQR# z^{*B840M{-Eq-n_O~<7nbMC4GrtQ8J>u~(73;0O?x>mWZXAs7pDd6v~7&W zscjAq8@z4o?C!N~a&2Kd)AaxfxM~I()~Go@OG|C3JlxjQ5x<;|5e>e+wsa{D z+C{ICC9a#^ZcV&T1;mRDfAung`%Xg2x1b#eR@jm68(dPqu;Kr>d{{2WKLoq)EBAwTOdM_!2YOMc-S!!A zQdDI^{~T+Kf^DHdb9xhGMv!k>SeWiEdnt73Y|lT*ZM!mm!8@r0F-!K1f@0KkNMVs@ z7e6X~&8wR|X*81T0_S)h9-e5e*Ju@)vMNHpA-lc4rgx`kJyb1+BEPoAae2=c|NBpq zMO5ZN_^wqED&W*r&s1DlSyu6)mk=RCSm2EDR5(I_8-l(;@U$)A|L|Mc-dx~GchXLz z`fE0f$E)v_n<-I@g6=ytmoLcObe}3~(&8CUS1Zs$AECDkyl!16iJT|tu*cH{ zQ$&CVakj1(6A`;OvL!DL;%Rq8?5K93mHdV^!JQ0mx2y$J)L@g=?pGMWw}-dz3Gh4n z9_)o$(i~;f2yB5}blmq)SBzkU&aFZ_0`XJWorDYHjd1>hECW$1IYHv}9{O)ww;~MB zQzFE|r_wu%`3E;Y4lc^NZbk^((-)&xJ!LSlfY&gGUk;UY8w$6M@#j$v z5r@J>Bk;aF^0hGml5XX?C|UHQeF-<|QYp%iRicD!>SB>kql|Tr!PPqAzFg2~Fhti) zNf5nM;p`B;Y*%8&6?lPydy7Nc38U0xS&v}~hP5a&>`vj#X)K=fC{|exWK~VwN%-rZ%=JvJaUD`Bdsb97kyfRqL5r_v41|KSN`0vE!-E%42?bW(eYlreb^)9mL+5 zmU=BI2-j_wX&7OpU|xX9kiFLUN1#t|mMoDU@OiOn*S0UnX>?DDQE!uK0U6Xh^j!NS zX2K^H;9X|h8aZ#7HOF&SJhq2P9{j?iL;_S}rhCx?LBycTaa| z^rGQ5@SL8pg3`CAY}GlND=<~Xio(7RYl@r+Sa?_qg~HEJLxEgKoe6XY<*~sJ3;9rM z1v!=-au;`JrjQ&AUgxU~)`ZGz1pQ)$OTQ5JW+qBdONIC=ua+@`V=V-UkFeF!B_-%PvCBz(xTs=uznw_>5n5h`Y326}$MBc;kh1-EG1kpp z+=sR(fc&^xqzp_eai)Uv2#IK@|!|nuw9L}7-Ke$^`B}>0k^T~C(_k@ z?aA>EmYm}1mRm!0P%fIaLwr!rW+PH*P8KunPaG(VJ&hw$9Uz z-48$PXUD-lcJfD--8oLp+e#p%m-0uHvl8vJTh^}KBL;V$Nczt;K1;9f1RuRs*G`Hbb$5UtAg96#G__S? zgQL8P%_{x%p<%T_!Tnsz#S`a;S0O-&#je6RHIe6gS4(JP@4K^R``S&cIi$7lT&^)? zy-ClQHZ$07aq2+r?Ig7)<*wa5D;fUcKuU8`Hk=je=c=0IROF4(lNAJAU%{PQ^?EyE z-$Sq4_D&TE<6R>U#a&b6`@?2RG`muaS$3e7JxVj=p;$@7^EUl`gWLCrZ_n@Nebvvd zL#E&gZ=FPiT__XWCYo%Z_6H)Qjfx|*`CDftqqr*5ouIZl_pIQcl`4!J=&l+)muI1Y zKz0MQ=jrkb25&XrIkv~sR9aB=461_t3NM7$5B3@t4E*>^!zp!#gh9TO0PJ9u+L}ZI zyLK^1Z@SZLQDflT`KwUeM;5})*q&*Sw_hK&M8}BL@^@SUHIzxio=l;lIhK%m;Utl* zCn#E?Xx?GCT_QP!^7bZ-GRVR*O7CZlqf0OP6lp4>J1047)`SZ+r-XL@eTz^LLVk^R zAY*QlN&v9tr(6Yda2BFv(>r6TYlH>gKxg)_k-{eCZyUlCYR4LeI42uG`F71o7mtn4FNsZ6E~gAilbX@$*8Fi()U_A zr*hGF(|ScY`zO-c??yGIVR-B(|C6~6Lz+dwVgfZR46Th4w^n!J9E8AMc2P~<-rSPQ z!_O~w>o2o!+^lQ^WpZ=<^?45~PXr-~K9J#7y8H@P0`KB`>wk7!obGdHF`Lh9Bf@)X zf%SQvX}xP!PHwg`fWg7g(5gEd`g=R;Y0)t?T+t^z-zn9z{XL;zEV}T*;ppUh9(q;^ z)JwP69>f{WB(u|P~=(bf#$vr5FwIc+HC%@D#1YSAo+=`deiwW6&oo0b-Im?i-74(wR24_FE*YDH*3O@gcBnKLPiSF(5RY z>usSJa)$V7sSAA-IKoYWRAXGm5~$VZxs^p=2W1!}5%jwfCtJwpYqPm8LjuUWV*}iG zkQ>3KjsCBz$qGqELDMlDy@AJ%-9F&Q~n-Y(28LU{O*& z!|(QH^|=0YQI;!|Z(#Ul_b|qE@w_Wg-OS7BY5DB{mc&n$kQP^m{y7|1=$P4q%ru>z zPl+V{wYb=s%SLKr(E&+bQd=&zdv-3QQ!o>;N5u80t@cC+I5)E3)6gpuD459{{J2-# zNwz*5E6!twpAI|eG{@zXnnQo91epSN%8 z&f)Fn@IF`WI#>3jl4&4kvE}elYzmjzp9=hrVX^TZOaN(oI%O4ywb~< z_KD`7Jv6EaBQdfNJ2>2iGp)_oo|EEC+1hEy*c^-_EA84@j1q;Bl7vL8h>c2*#nr>m z1*CS<<>z3dt?q%g`wOi2Y*9%~%=4|<{QkaH1ae8SYbzNy z*+IG)>_V&$Nf*^+ldCh7gwO@!!V0?;{AN77lZo%ghGNc`zQ>kUz=r;N#>RiGxC|TV zlvWO(0aI@@h+JPVnQ zRvK;3IW>2a9f}PsJN~<%eg{-fN%8nh+x%B){TJcY<|HCetMX{G`Aq*y6&@B_vKE&# zgz@ogY=ez5&hvb!$?$z`En%JPkx!wNj zQFPChO$-f%P(|ZQ(|O^brdVO}Om3xjCX^D7lm3|kExY}T6+nhUDrP7bQ8Y6A_vGrG zIdJP=aNU0o-$Hb|K0{nO^B*-8_A%`~Q>%n#WrNN#Mm>;7t4|u-ldsQttXu+(>0w+C zY_%zVZL3|Da;$ZHlmK+;;3LPrHP-kbvpBt*vJvt^#&dG7&rzEKIh%^Z`;BG(J0ERf z^vo=n?+CaO%LkEM1Hbckg{mINmh44?b9|+?Y=swt!XIo!a&B|C-7YF(9r*&?ky#$0 zNGf(}`oaovkS|izpuTc7Qi|)S$8O3AretTSrlBi<%f0Pe z;!w__$%;dQg`dFe0dv~00yv9xd<FQCITrt?NDhK`(Q>oOLGqE#*<{ce!1nU zgQSRf-TTn@riLn{HV&n5Fk&#pLtgd}3Eoit90hb-Si(lOJ!4Tu6e)i`9(rY=x<|yv z$yw>c3`Ji)RkQ7{_YtTSn)zrrqd&;}@Q7rfevSp{22%plphsdy5l(>XbtgfJqeW#4)%9i)dHBK+1UUl`Z(3<(7$Mg;uM28wr~~UF3F`q>gH2^UR*@ zH&_iU?i(KjGPte)b~;(_A;c0$Led77wK~clpCE72n)~blzQFC#m3m4{C5a@KXCkC* z#1IhFO){Lyw)8^xtM$m~>i)^~3oXh53^^|8L1Gn}pA<|4o$o-1)Uq%IUPSAeNB+%{buc) zLtnNp+)q@bKjqMig5LRw!69b=Cyh)r4E8f%(;!!&a{N~Jfd&Xg>l;dGdJ~{{hv0)< z{wz^^Z>3_5B$Pb0+gaf)Yq? z)gV#~sza*>$TMgsA{^_{9d5lTO{jrzb#XHl9FrY^ z%ynf~OFY#_;&5b%lN>i~9WJ_cbtA%7aGK2Z9ZH)%dlJt9?%HI$wPk(gS62D-Jkf{I zVKQE#z6-4~dK9P@1a0-q)P#>6h!?)g2YlAqbNDQxl=wz{bPY|JP5|^OpS)khK>lvP zpxw18g)E@gss4q!;Z@$o#Z>~zTdqmQbOArGT3MP zc(LNqa9qEB)J=x9np{wqb(rOM^?PT$2DZCy%`}XOp%WuC%i-o}QbHZRs_KAwPo^ERxhF*%C9 zvk-s@X#5?2ySVV3y$Axj2st3RMLI2s;@T-+0Wxz;mnOgFv}4h9 z;0@~{Fyt@5?v7)pkgA!?W4csvlfZN6Z56QmXRWXSY@z7d7|lj4z(^?Sa>92M#)mFX zUoc(YDz~-xk@ol2%@Z*yP$0E$F2*xCgx~xt7S{MzoExS$r z?;OqH$qLMmNuew%gUl9onVVGrB!T33$B*p#$K*t6e4pzL*B-zdSys0F2Mt3t;pdeHa%bW@>*eQ?C* zu?&UB%1~4sx4LPDFy6vqVa)wY|6*;F=@alToK# zAohPOWZ5RYeMIPv4I+jDLjmUU7G)SS;4-Pv^g9uyK~3Q@UJ?;bsjAn$MLrBfS#{Nv z%b~+r$hg)(daCtse8VQ-lT)@MRd5E3l^*DfS44aJ#X&k&(QQ5#-~Fb~0J|9cX5W69W!ep!=2T(E$_UW>DE!YS$^Hi}TVfyqn4v6=k7Xq~~(p>ro({RLUBhGEf86T~UHXg{XgnAizsjNBfi3i~ z9mhcx#=Xbkqh_HI2V|>2XJF|$Je_>@?5r6e#ml9AZC?;Lg&f7W_#0!H1!=Hlz&1+* zg1L4Tm<`i9mXrMiM_gLRhj0+S%X%12D7hTUR)E^Hlbw0|k;GRlRHSVi))kLBgY8ge z5AHn;bJyK;nB~R9Uj#n#g9hpX^ZfHOB8Rzj2nOl-NkPz3{r^o5FYXClWL>s1P=KU$ zLarK6xh@Lq-2^->zgbUmtd!MQb|?2G7LpuPQ*1@4n^W0|NMNSr%>=I9P!!lhZhL)x P{s6_jRD4ty0t5Xw43NE} literal 0 HcmV?d00001 From e29bc1b1840c25496791190881aa850dfb78c3bc Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:21:45 +0000 Subject: [PATCH 02/17] fix: use boxed title labels for all nodes in concept map --- chapters/concept-map.qmd | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/chapters/concept-map.qmd b/chapters/concept-map.qmd index 36ae5ab2d..4cbcaf9a3 100644 --- a/chapters/concept-map.qmd +++ b/chapters/concept-map.qmd @@ -54,7 +54,7 @@ dependency link, laid out so that connected results sit near each other. Color encodes the type of result, and **node size grows with the number of descendants**, so the most foundational results stand out -(the largest, most-connected nodes are also labeled). +(the largest, most-connected nodes appear biggest). Results with no detected dependency links (foundational definitions used only in prose, or standalone results) are omitted from the diagram but appear in @sec-descendants-table @@ -82,18 +82,20 @@ set.seed(204) ggraph(core, layout = "stress") + geom_edge_link( arrow = arrow(length = unit(1.8, "mm"), type = "closed"), - end_cap = circle(1.6, "mm"), + end_cap = circle(3, "mm"), edge_alpha = 0.25, edge_width = 0.3 ) + - geom_node_point(aes(size = n_desc + 1, color = type)) + - geom_node_text( - aes(label = ifelse(n_desc >= 3, name, "")), - repel = TRUE, size = 2.7, max.overlaps = 30, segment.alpha = 0.3 + geom_node_label( + aes(label = title, fill = type, size = n_desc + 1), + color = "white", alpha = 0.85, + label.padding = unit(0.12, "lines"), + label.r = unit(0.08, "lines"), + fontface = "bold" ) + - scale_color_manual( + scale_fill_manual( values = type_palette, labels = type_labels, name = "Type", drop = FALSE ) + - scale_size_continuous(range = c(1.5, 9), guide = "none") + + scale_size_continuous(range = c(1.8, 5), guide = "none") + theme_void() + theme( legend.position = "bottom", From d1ea852bfea05d341f7cce7729f8caa2672fded2 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:22:14 +0000 Subject: [PATCH 03/17] feat: add Concept Map to website navbar Appendices dropdown --- _quarto-website.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/_quarto-website.yml b/_quarto-website.yml index 9671ff085..90d53d2bf 100644 --- a/_quarto-website.yml +++ b/_quarto-website.yml @@ -123,6 +123,8 @@ website: href: chapters/exam-formula-sheet.qmd - text: "Package Versions" href: chapters/package-versions.qmd + - text: "Concept Map" + href: chapters/concept-map.qmd bibliography: references.bib From dd97e4a98f622e4e830f6ce192dcbb2db88e4c46 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:22:28 +0000 Subject: [PATCH 04/17] docs: remind to add new chapters to website navbar dropdown --- .claude/commands/new-chapter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/commands/new-chapter.md b/.claude/commands/new-chapter.md index 08d805143..5c9222b68 100644 --- a/.claude/commands/new-chapter.md +++ b/.claude/commands/new-chapter.md @@ -14,7 +14,7 @@ First, parse `$ARGUMENTS`: the first whitespace-delimited token is the **slug** Steps: 1. Create `chapters/.qmd` with YAML frontmatter holding just `title:` (set to the title). Do NOT set `date:` — the book sets `date: last-modified` globally, and a per-page `date:` would override it. Do NOT add a top-level `#` heading in the body — Quarto renders the frontmatter `title:` as the page heading. -2. Register the chapter in the `book.chapters:` list in `_quarto-book.yml` at a logical position (read the file first). If it belongs to an existing `part:`, nest it under that part. +2. Register the chapter in the `book.chapters:` list in `_quarto-book.yml` at a logical position (read the file first). If it belongs to an existing `part:`, nest it under that part. **Also** add an entry to the appropriate navbar dropdown (`Chapters` or `Appendices`) in `_quarto-website.yml`, and ensure the file is in the `render:` list. The navbar is NOT auto-generated from `_quarto-book.yml` -- manual addition is required. 3. If the chapter is long, you may split content into includes under `chapters/_subfiles//`. Subfiles must NOT start with a heading and must NOT contain a references section. 4. Confirm it renders: `quarto render chapters/.qmd --to html`. From b2a458a428accd40108fbcdb9898f7cd554d9778 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:48:16 +0000 Subject: [PATCH 05/17] concept-map: large SVG layout, HTML-only figure, count-only descendants table --- chapters/concept-map.qmd | 69 ++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/chapters/concept-map.qmd b/chapters/concept-map.qmd index 4cbcaf9a3..64f5dd1ef 100644 --- a/chapters/concept-map.qmd +++ b/chapters/concept-map.qmd @@ -49,16 +49,13 @@ connected by `r nrow(cg$edges)` direct dependency links. ## Dependency diagram +::: {.content-visible when-format="html"} + @fig-concept-map shows the results that participate in at least one dependency link, laid out so that connected results sit near each other. -Color encodes the type of result, -and **node size grows with the number of descendants**, -so the most foundational results stand out -(the largest, most-connected nodes appear biggest). -Results with no detected dependency links -(foundational definitions used only in prose, or standalone results) -are omitted from the diagram but appear in @sec-descendants-table -if they have descendants. +Color encodes the type of result. +Results with no detected dependency links are omitted from the diagram. +Zoom in with Ctrl+scroll (or pinch on mobile) to read individual labels. :::{#fig-concept-map} @@ -67,8 +64,10 @@ if they have descendants. #| code-fold: true #| message: false #| warning: false -#| fig-width: 11 -#| fig-height: 9 +#| fig-width: 50 +#| fig-height: 40 +#| out-width: "100%" +#| fig-format: "svg" connected <- cg$nodes$id[cg$nodes$id %in% c(cg$edges$from, cg$edges$to)] core <- graph_from_data_frame( @@ -86,16 +85,16 @@ ggraph(core, layout = "stress") + edge_alpha = 0.25, edge_width = 0.3 ) + geom_node_label( - aes(label = title, fill = type, size = n_desc + 1), + aes(label = title, fill = type), color = "white", alpha = 0.85, label.padding = unit(0.12, "lines"), label.r = unit(0.08, "lines"), - fontface = "bold" + fontface = "bold", + size = 2.5 ) + scale_fill_manual( values = type_palette, labels = type_labels, name = "Type", drop = FALSE ) + - scale_size_continuous(range = c(1.8, 5), guide = "none") + theme_void() + theme( legend.position = "bottom", @@ -105,19 +104,24 @@ ggraph(core, layout = "stress") + Dependency structure of the definitions and results in the notes. An arrow points from a result to each result that uses it. -Node and label size are proportional to the total number of descendants. +Color indicates result type (see legend). + +::: + +::: + +::: {.content-hidden when-format="html"} + +The dependency diagram is only available in the +[HTML version of the notes](https://d-morrison.github.io/rme/chapters/concept-map.html). ::: ## Descendants of each result {#sec-descendants-table} @tbl-descendants lists every result that has at least one descendant, -sorted by the number of **direct** descendants. -For each result, the first column links to its direct descendants -(results that reference it directly) -and the second column links to its indirect descendants -(results that reach it only through a chain of intermediate results). -All entries are live cross-reference links. +sorted by the number of **direct** descendants (results that reference it directly). +The total column counts all transitive descendants. ::: {#tbl-descendants} @@ -127,32 +131,21 @@ All entries are live cross-reference links. #| message: false #| warning: false -ref_list <- function(ids) { - if (!length(ids)) return("—") - ids <- ids[order(match(sub("-.*", "", ids), type_levels), ids)] - paste0("@", ids, collapse = ", ") -} - ranked <- cg$nodes[cg$nodes$n_direct >= 1, ] ranked <- ranked[order(-ranked$n_direct, -ranked$n_desc, ranked$id), ] -direct_col <- vapply( - ranked$id, function(v) ref_list(cg$descendants[[v]]$direct), character(1) -) -indirect_col <- vapply( - ranked$id, function(v) ref_list(cg$descendants[[v]]$indirect), character(1) -) descendant_table <- data.frame( - Result = paste0("@", ranked$id), - `Direct descendants` = direct_col, - `Indirect descendants` = indirect_col, + Result = ranked$title, + Type = type_labels[ranked$type], + `Direct descendants` = ranked$n_direct, + `Total descendants` = ranked$n_desc, check.names = FALSE, row.names = NULL, stringsAsFactors = FALSE ) -knitr::kable(descendant_table, format = "pipe", align = "lll") +knitr::kable(descendant_table, format = "pipe", align = "llrr") ``` -Direct and indirect descendants of every result that has at least one -descendant, sorted by number of direct descendants. +All results with at least one descendant, +sorted by number of direct descendants (most-foundational first). ::: From 6e410d8f825372a00f077d99edb46a324fc1407d Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 22:31:19 +0000 Subject: [PATCH 06/17] Fix concept map: ggrepel non-overlapping labels, fix crossrefs, simplify table to total descendants count --- chapters/concept-map.qmd | 64 ++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/chapters/concept-map.qmd b/chapters/concept-map.qmd index 64f5dd1ef..ed3bae10e 100644 --- a/chapters/concept-map.qmd +++ b/chapters/concept-map.qmd @@ -30,6 +30,7 @@ re-titled** to refresh the diagram and table below. library(igraph) library(ggraph) +library(ggrepel) cg <- readRDS(here::here("inst/extdata/callout-graph.rds")) @@ -49,18 +50,20 @@ connected by `r nrow(cg$edges)` direct dependency links. ## Dependency diagram -::: {.content-visible when-format="html"} +::: {.content-visible unless-format="pdf"} +::: {.content-visible unless-format="docx"} @fig-concept-map shows the results that participate in at least one dependency link, laid out so that connected results sit near each other. Color encodes the type of result. Results with no detected dependency links are omitted from the diagram. Zoom in with Ctrl+scroll (or pinch on mobile) to read individual labels. +Lines from labels to dots indicate where ggrepel moved a label to avoid overlap. :::{#fig-concept-map} ```{r} -#| label: fig-concept-map-plot +#| label: concept-map-ggraph #| code-fold: true #| message: false #| warning: false @@ -78,19 +81,35 @@ core <- graph_from_data_frame( V(core)$type <- factor(V(core)$type, levels = type_levels) set.seed(204) -ggraph(core, layout = "stress") + +layout <- create_layout(core, layout = "stress") + +ggraph(layout) + geom_edge_link( arrow = arrow(length = unit(1.8, "mm"), type = "closed"), - end_cap = circle(3, "mm"), + end_cap = circle(2, "mm"), edge_alpha = 0.25, edge_width = 0.3 ) + - geom_node_label( - aes(label = title, fill = type), - color = "white", alpha = 0.85, - label.padding = unit(0.12, "lines"), - label.r = unit(0.08, "lines"), + geom_node_point(color = "grey60", size = 0.8, alpha = 0.7) + + geom_label_repel( + data = as.data.frame(layout), + aes(x = x, y = y, label = title, fill = type), + inherit.aes = FALSE, + color = "white", fontface = "bold", - size = 2.5 + size = 2.2, + max.overlaps = Inf, + force = 3, + force_pull = 0.5, + box.padding = unit(0.25, "lines"), + label.padding = unit(0.1, "lines"), + label.r = unit(0.05, "lines"), + label.size = 0.1, + alpha = 0.88, + segment.color = "grey60", + segment.alpha = 0.6, + segment.size = 0.3, + min.segment.length = 0, + seed = 204 ) + scale_fill_manual( values = type_palette, labels = type_labels, name = "Type", drop = FALSE @@ -108,9 +127,17 @@ Color indicates result type (see legend). ::: +::: ::: -::: {.content-hidden when-format="html"} +::: {.content-visible when-format="pdf"} + +The dependency diagram is only available in the +[HTML version of the notes](https://d-morrison.github.io/rme/chapters/concept-map.html). + +::: + +::: {.content-visible when-format="docx"} The dependency diagram is only available in the [HTML version of the notes](https://d-morrison.github.io/rme/chapters/concept-map.html). @@ -120,32 +147,31 @@ The dependency diagram is only available in the ## Descendants of each result {#sec-descendants-table} @tbl-descendants lists every result that has at least one descendant, -sorted by the number of **direct** descendants (results that reference it directly). -The total column counts all transitive descendants. +sorted by the number of total descendants (direct or transitive). ::: {#tbl-descendants} ```{r} -#| label: tbl-descendants-build +#| label: descendants-table-build #| code-fold: true #| message: false #| warning: false -ranked <- cg$nodes[cg$nodes$n_direct >= 1, ] -ranked <- ranked[order(-ranked$n_direct, -ranked$n_desc, ranked$id), ] +ranked <- cg$nodes[cg$nodes$n_desc >= 1, ] +ranked <- ranked[order(-ranked$n_desc, ranked$id), ] descendant_table <- data.frame( Result = ranked$title, Type = type_labels[ranked$type], - `Direct descendants` = ranked$n_direct, `Total descendants` = ranked$n_desc, check.names = FALSE, row.names = NULL, stringsAsFactors = FALSE ) -knitr::kable(descendant_table, format = "pipe", align = "llrr") +knitr::kable(descendant_table, format = "pipe", align = "llr") ``` All results with at least one descendant, -sorted by number of direct descendants (most-foundational first). +sorted by number of total descendants (most-foundational first). ::: + From 2b0682270475c1c306350de3f4ce7860cb4565f5 Mon Sep 17 00:00:00 2001 From: Douglas Ezra Morrison Date: Thu, 4 Jun 2026 17:47:36 -0700 Subject: [PATCH 07/17] Concept map: layered layout, basic nodes on top, seed implicit deps - Switch the dependency diagram from a force-directed "stress" layout to a layered Sugiyama layout and flip y so the most basic results (DAG roots -- many descendants, few ancestors) sit at the top. This is far more evenly spaced and removes most of the scattered whitespace. - Seed an `implicit_edges` list in data-raw/callout-graph.R: foundational prerequisites a result relies on but doesn't cite with an explicit @ref (e.g. probability -> odds, probability -> conditional probability, odds -> log-odds, independence -> iid, hazard -> cumulative hazard, ...). Regenerate inst/extdata/callout-graph.rds (88 -> 115 dependency links). Extend the list as more obvious gaps are noticed. Co-Authored-By: Claude Opus 4.8 (1M context) --- chapters/concept-map.qmd | 8 +++++- data-raw/callout-graph.R | 45 +++++++++++++++++++++++++++++++++ inst/extdata/callout-graph.rds | Bin 14758 -> 15135 bytes 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/chapters/concept-map.qmd b/chapters/concept-map.qmd index ed3bae10e..581ab661f 100644 --- a/chapters/concept-map.qmd +++ b/chapters/concept-map.qmd @@ -81,7 +81,13 @@ core <- graph_from_data_frame( V(core)$type <- factor(V(core)$type, levels = type_levels) set.seed(204) -layout <- create_layout(core, layout = "stress") +# Layered (Sugiyama) layout instead of a force-directed one: it arranges the +# dependency DAG into ranks, which is far more evenly spaced than "stress" and +# gives a clear top-to-bottom reading order. +layout <- create_layout(core, layout = "sugiyama") +# Put the most *basic* results (roots of the DAG -- many descendants, few +# ancestors) at the top by flipping the vertical axis. +layout$y <- -layout$y ggraph(layout) + geom_edge_link( diff --git a/data-raw/callout-graph.R b/data-raw/callout-graph.R index 512585c27..c61ca806a 100644 --- a/data-raw/callout-graph.R +++ b/data-raw/callout-graph.R @@ -134,6 +134,51 @@ extract_callout_graph <- function(root) { root <- here::here() cg <- extract_callout_graph(root) +# Curated *implicit* dependencies: foundational prerequisites that one result +# relies on but does not cite with an explicit `@ref` (so the scan above misses +# them). Each row is `prerequisite -> dependent` (same direction as the scanned +# edges: from the thing depended on, to the thing that depends on it). Extend +# this list as obvious gaps are noticed; ids must match `cg$nodes$id`. +implicit_edges <- data.frame( + stringsAsFactors = FALSE, + rbind( + c("def-probability", "def-odds"), + c("def-probability", "def-conditional-prob"), + c("def-probability", "def-indpt"), + c("def-probability", "def-pdf"), + c("def-probability", "def-cdf"), + c("def-conditional-prob", "def-c-odds"), + c("def-conditional-prob", "def-cond-expectation"), + c("def-odds", "def-logodds"), + c("def-odds", "def-odds-fn"), + c("def-odds", "def-c-odds"), + c("def-logodds", "def-logit-fn"), + c("def-logit-fn", "def-expit"), + c("def-indpt", "def-iid"), + c("def-indpt", "def-cident"), + c("def-indpt", "def-independence-diagnostics"), + c("def-expectation", "def-variance"), + c("def-expectation", "def-cov"), + c("def-expectation", "def-cond-expectation"), + c("def-variance", "def-cov"), + c("def-variance", "def-cov-vec-x"), + c("def-cov", "def-cov-vec-x"), + c("def-cdf", "def-pdf"), + c("def-cdf", "def-surv-fn"), + c("def-hazard", "def-cuhaz"), + c("def-hazard", "def-cond-hazard"), + c("def-hazard", "def-hazard-ratio"), + c("def-cond-hazard", "def-cond-loghaz") + ) +) +names(implicit_edges) <- c("from", "to") +cg$edges <- unique(rbind(cg$edges, implicit_edges)) +cg$edges <- cg$edges[ + cg$edges$from %in% cg$nodes$id & cg$edges$to %in% cg$nodes$id, , + drop = FALSE +] +rownames(cg$edges) <- NULL + # Precompute descendant counts and the direct/indirect descendant id lists, so # the chapter does no graph analysis at render time. ig <- graph_from_data_frame(cg$edges, vertices = cg$nodes, directed = TRUE) diff --git a/inst/extdata/callout-graph.rds b/inst/extdata/callout-graph.rds index 5912dcf12cd6d8357cb4513f9a244c528b8e0fd2..bdbefa6b78999d020e6606c6543ac068cbce23fd 100644 GIT binary patch literal 15135 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 literal 14758 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 P{s6_jRD4ty0t5Xw43NE} From 13c85b2b6b4b88e4320b42c8b71807580b26eddb Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:33:46 +0000 Subject: [PATCH 08/17] concept-map: add direct-descendants column; scale label font by descendant count Co-Authored-By: Claude Sonnet 4.6 --- chapters/concept-map.qmd | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/chapters/concept-map.qmd b/chapters/concept-map.qmd index 581ab661f..3722aee78 100644 --- a/chapters/concept-map.qmd +++ b/chapters/concept-map.qmd @@ -57,6 +57,8 @@ connected by `r nrow(cg$edges)` direct dependency links. dependency link, laid out so that connected results sit near each other. Color encodes the type of result. Results with no detected dependency links are omitted from the diagram. +Node labels are larger for results with more total descendants, +so the most foundational results are visually prominent. Zoom in with Ctrl+scroll (or pinch on mobile) to read individual labels. Lines from labels to dots indicate where ggrepel moved a label to avoid overlap. @@ -98,11 +100,10 @@ ggraph(layout) + geom_node_point(color = "grey60", size = 0.8, alpha = 0.7) + geom_label_repel( data = as.data.frame(layout), - aes(x = x, y = y, label = title, fill = type), + aes(x = x, y = y, label = title, fill = type, size = n_desc), inherit.aes = FALSE, color = "white", fontface = "bold", - size = 2.2, max.overlaps = Inf, force = 3, force_pull = 0.5, @@ -120,6 +121,7 @@ ggraph(layout) + scale_fill_manual( values = type_palette, labels = type_labels, name = "Type", drop = FALSE ) + + scale_size_continuous(range = c(2.0, 4.5), guide = "none") + theme_void() + theme( legend.position = "bottom", @@ -130,6 +132,7 @@ ggraph(layout) + Dependency structure of the definitions and results in the notes. An arrow points from a result to each result that uses it. Color indicates result type (see legend). +Label font size is proportional to the number of total descendants. ::: @@ -169,15 +172,15 @@ ranked <- ranked[order(-ranked$n_desc, ranked$id), ] descendant_table <- data.frame( Result = ranked$title, Type = type_labels[ranked$type], - `Total descendants` = ranked$n_desc, + `Direct descendants` = ranked$n_direct, + `Total descendants` = ranked$n_desc, check.names = FALSE, row.names = NULL, stringsAsFactors = FALSE ) -knitr::kable(descendant_table, format = "pipe", align = "llr") +knitr::kable(descendant_table, format = "pipe", align = "llrr") ``` All results with at least one descendant, sorted by number of total descendants (most-foundational first). ::: - From 5f53a29f21aa9f6af6967f58d38ff14bae934f5d Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 21:27:15 +0000 Subject: [PATCH 09/17] fix: strip \index{} from node labels, raise min font size to ~8pt - Strip LaTeX \index{} annotations from titles at load time (concept-map.qmd) and at extraction time (callout-graph.R) so they never appear as node labels - scale_size_continuous range c(2.0, 4.5) -> c(3.0, 6.0): minimum label size is now ~8.5pt (was ~5.7pt); ggplot2 size units are mm, 1mm * 2.845 = pt --- chapters/concept-map.qmd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chapters/concept-map.qmd b/chapters/concept-map.qmd index 3722aee78..e3a61e731 100644 --- a/chapters/concept-map.qmd +++ b/chapters/concept-map.qmd @@ -33,6 +33,7 @@ library(ggraph) library(ggrepel) cg <- readRDS(here::here("inst/extdata/callout-graph.rds")) +cg$nodes$title <- gsub("\\\\index\\{[^}]*\\}", "", cg$nodes$title) type_levels <- c("def", "thm", "lem", "cor", "prp") type_labels <- c( @@ -121,7 +122,7 @@ ggraph(layout) + scale_fill_manual( values = type_palette, labels = type_labels, name = "Type", drop = FALSE ) + - scale_size_continuous(range = c(2.0, 4.5), guide = "none") + + scale_size_continuous(range = c(3.0, 6.0), guide = "none") + theme_void() + theme( legend.position = "bottom", From 52e8329c20204b03a77cfcbd3afdd2320cf79586 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 21:30:44 +0000 Subject: [PATCH 10/17] fix: strip \index{} from titles during extraction; fix proof word-boundary regex - gsub("\\\\index\\{[^}]*\\}", "", title) strips LaTeX index annotations from heading text before storing in the node data frame - proof-detection regex updated to use \\b word boundaries so that div IDs like `disproof-foo` or `approval-workflow` no longer match --- data-raw/callout-graph.R | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/data-raw/callout-graph.R b/data-raw/callout-graph.R index c61ca806a..7683d9065 100644 --- a/data-raw/callout-graph.R +++ b/data-raw/callout-graph.R @@ -57,7 +57,9 @@ extract_callout_graph <- function(root) { for (j in seq(i + 1, min(i + 5, n))) { if (j > n || str_trim(lines[j]) != "") { hm <- str_match(lines[j], head_re) - if (!is.na(hm[1, 1])) title <- hm[1, 2] + if (!is.na(hm[1, 1])) { + title <- gsub("\\\\index\\{[^}]*\\}", "", hm[1, 2]) + } break } } @@ -76,7 +78,7 @@ extract_callout_graph <- function(root) { content <- str_trim(sub("^:::+", "", l)) if (nzchar(content)) { # A proof/solution div directly following a callout inherits it. - is_proof <- grepl("proof|solution", content, ignore.case = TRUE) + is_proof <- grepl("\\b(proof|solution)\\b", content, ignore.case = TRUE) owner <- if (is_proof) pending else NA_character_ stack[[length(stack) + 1]] <- list(kind = "div", class = content, owner = owner) From 566a09ce078686fc33ce765382a2154ab6501bf7 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sat, 6 Jun 2026 01:03:20 +0000 Subject: [PATCH 11/17] fix: add str_trim() to index stripping, guard nodes empty list --- data-raw/callout-graph.R | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/data-raw/callout-graph.R b/data-raw/callout-graph.R index 7683d9065..6aece84e9 100644 --- a/data-raw/callout-graph.R +++ b/data-raw/callout-graph.R @@ -58,7 +58,7 @@ extract_callout_graph <- function(root) { if (j > n || str_trim(lines[j]) != "") { hm <- str_match(lines[j], head_re) if (!is.na(hm[1, 1])) { - title <- gsub("\\\\index\\{[^}]*\\}", "", hm[1, 2]) + title <- str_trim(gsub("\\\\index\\{[^}]*\\}", "", hm[1, 2])) } break } @@ -124,7 +124,10 @@ extract_callout_graph <- function(root) { } } - nodes <- do.call(rbind, nodes) + nodes <- if (length(nodes)) do.call(rbind, nodes) else + data.frame(id = character(), type = character(), + title = character(), file = character(), + stringsAsFactors = FALSE) rownames(nodes) <- NULL edges <- if (length(edges)) unique(do.call(rbind, edges)) else data.frame(from = character(), to = character()) From fa7db3fbe61dd8f3306951f15eca5db616e93d82 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Sat, 6 Jun 2026 01:03:30 +0000 Subject: [PATCH 12/17] =?UTF-8?q?fix:=20stress=20layout,=20larger=20font?= =?UTF-8?q?=20minimum=20(4mm=E2=89=8811pt),=20trimws()=20for=20index=20str?= =?UTF-8?q?ipping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chapters/concept-map.qmd | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/chapters/concept-map.qmd b/chapters/concept-map.qmd index e3a61e731..d23d05e94 100644 --- a/chapters/concept-map.qmd +++ b/chapters/concept-map.qmd @@ -33,7 +33,7 @@ library(ggraph) library(ggrepel) cg <- readRDS(here::here("inst/extdata/callout-graph.rds")) -cg$nodes$title <- gsub("\\\\index\\{[^}]*\\}", "", cg$nodes$title) +cg$nodes$title <- trimws(gsub("\\\\index\\{[^}]*\\}", "", cg$nodes$title)) type_levels <- c("def", "thm", "lem", "cor", "prp") type_labels <- c( @@ -71,7 +71,7 @@ Lines from labels to dots indicate where ggrepel moved a label to avoid overlap. #| message: false #| warning: false #| fig-width: 50 -#| fig-height: 40 +#| fig-height: 50 #| out-width: "100%" #| fig-format: "svg" @@ -84,13 +84,10 @@ core <- graph_from_data_frame( V(core)$type <- factor(V(core)$type, levels = type_levels) set.seed(204) -# Layered (Sugiyama) layout instead of a force-directed one: it arranges the -# dependency DAG into ranks, which is far more evenly spaced than "stress" and -# gives a clear top-to-bottom reading order. -layout <- create_layout(core, layout = "sugiyama") -# Put the most *basic* results (roots of the DAG -- many descendants, few -# ancestors) at the top by flipping the vertical axis. -layout$y <- -layout$y +# Stress layout (from graphlayouts, via ggraph) minimises a stress function so +# connected nodes sit close together and nodes are distributed evenly across +# the canvas — much less dead whitespace than the Sugiyama hierarchical layout. +layout <- create_layout(core, layout = "stress") ggraph(layout) + geom_edge_link( @@ -122,12 +119,9 @@ ggraph(layout) + scale_fill_manual( values = type_palette, labels = type_labels, name = "Type", drop = FALSE ) + - scale_size_continuous(range = c(3.0, 6.0), guide = "none") + + scale_size_continuous(range = c(4.0, 8.0), guide = "none") + theme_void() + - theme( - legend.position = "bottom", - text = element_text(family = "serif") - ) + theme(legend.position = "bottom") ``` Dependency structure of the definitions and results in the notes. From e049fb2b8bf09cd38af1b27030b44f991c3f0085 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 07:59:24 +0000 Subject: [PATCH 13/17] use FR layout + axis normalisation to spread concept-map nodes Co-Authored-By: Claude Sonnet 4.6 --- chapters/concept-map.qmd | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/chapters/concept-map.qmd b/chapters/concept-map.qmd index d23d05e94..d13057657 100644 --- a/chapters/concept-map.qmd +++ b/chapters/concept-map.qmd @@ -84,10 +84,13 @@ core <- graph_from_data_frame( V(core)$type <- factor(V(core)$type, levels = type_levels) set.seed(204) -# Stress layout (from graphlayouts, via ggraph) minimises a stress function so -# connected nodes sit close together and nodes are distributed evenly across -# the canvas — much less dead whitespace than the Sugiyama hierarchical layout. -layout <- create_layout(core, layout = "stress") +# Fruchterman-Reingold applies repulsion between all node pairs (not just +# connected ones), spreading nodes more evenly across the canvas. Normalise +# axes afterwards to fill the full canvas and eliminate edge whitespace. +layout <- create_layout(core, layout = "fr", niter = 2000) +norm_axis <- function(x) (x - min(x)) / diff(range(x)) * 2 - 1 +layout$x <- norm_axis(layout$x) +layout$y <- norm_axis(layout$y) ggraph(layout) + geom_edge_link( From 68e11499b7ea711364c9baa3ff3f21877470c0a5 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 20:47:00 +0000 Subject: [PATCH 14/17] fix: replace deprecated subcomponent() with bfs() in callout-graph.R Replace the two subcomponent() calls (deprecated in igraph 2.x) with bfs(..., mode = "out", unreachable = FALSE)$order equivalents. No change in computed results; removes deprecation warnings when the script runs. Co-Authored-By: Claude Sonnet 4.6 --- data-raw/callout-graph.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data-raw/callout-graph.R b/data-raw/callout-graph.R index 6aece84e9..62ebda9c7 100644 --- a/data-raw/callout-graph.R +++ b/data-raw/callout-graph.R @@ -189,14 +189,14 @@ rownames(cg$edges) <- NULL ig <- graph_from_data_frame(cg$edges, vertices = cg$nodes, directed = TRUE) cg$nodes$n_desc <- vapply( cg$nodes$id, - function(v) length(subcomponent(ig, v, mode = "out")) - 1L, + function(v) length(bfs(ig, v, mode = "out", unreachable = FALSE)$order) - 1L, integer(1) ) cg$nodes$n_direct <- as.integer(degree(ig, mode = "out")[cg$nodes$id]) cg$descendants <- lapply(stats::setNames(cg$nodes$id, cg$nodes$id), function(v) { direct <- setdiff(names(which(distances(ig, v, mode = "out")[1, ] == 1)), v) - reach <- setdiff(subcomponent(ig, v, mode = "out")$name, v) + reach <- setdiff(names(bfs(ig, v, mode = "out", unreachable = FALSE)$order), v) list(direct = sort(direct), indirect = sort(setdiff(reach, direct))) }) From 489a72bc0681295dd7aa6ca200c1349bf92f5a2a Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 20:47:21 +0000 Subject: [PATCH 15/17] fix: guard norm_axis against degenerate layout; increase label font size - norm_axis: guard against diff(range(x)) == 0 (all nodes collapse to the same coordinate in a sparse graph) to prevent silent NaN positions and an empty figure. - scale_size_continuous: widen font-size range from c(4.0, 8.0) to c(8.0, 16.0) to make node labels more readable. Co-Authored-By: Claude Sonnet 4.6 --- chapters/concept-map.qmd | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/chapters/concept-map.qmd b/chapters/concept-map.qmd index d13057657..5f00eb1fc 100644 --- a/chapters/concept-map.qmd +++ b/chapters/concept-map.qmd @@ -88,7 +88,11 @@ set.seed(204) # connected ones), spreading nodes more evenly across the canvas. Normalise # axes afterwards to fill the full canvas and eliminate edge whitespace. layout <- create_layout(core, layout = "fr", niter = 2000) -norm_axis <- function(x) (x - min(x)) / diff(range(x)) * 2 - 1 +norm_axis <- function(x) { + r <- diff(range(x)) + if (r == 0) return(x) + (x - min(x)) / r * 2 - 1 +} layout$x <- norm_axis(layout$x) layout$y <- norm_axis(layout$y) @@ -122,7 +126,7 @@ ggraph(layout) + scale_fill_manual( values = type_palette, labels = type_labels, name = "Type", drop = FALSE ) + - scale_size_continuous(range = c(4.0, 8.0), guide = "none") + + scale_size_continuous(range = c(8.0, 16.0), guide = "none") + theme_void() + theme(legend.position = "bottom") ``` From cb8249689706f4cfd11c3313234e59399b3d9775 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 20:48:50 +0000 Subject: [PATCH 16/17] docs: add step 5 to new-chapter skill (re-run callout-graph.R) When a new chapter contains def/thm/lem/cor/prp callout divs, contributors need to re-run Rscript data-raw/callout-graph.R to keep the concept map current. Add this as an explicit step 5 in the new-chapter skill. Co-Authored-By: Claude Sonnet 4.6 --- .claude/commands/new-chapter.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.claude/commands/new-chapter.md b/.claude/commands/new-chapter.md index 5c9222b68..133cc0b5d 100644 --- a/.claude/commands/new-chapter.md +++ b/.claude/commands/new-chapter.md @@ -17,6 +17,9 @@ Steps: 2. Register the chapter in the `book.chapters:` list in `_quarto-book.yml` at a logical position (read the file first). If it belongs to an existing `part:`, nest it under that part. **Also** add an entry to the appropriate navbar dropdown (`Chapters` or `Appendices`) in `_quarto-website.yml`, and ensure the file is in the `render:` list. The navbar is NOT auto-generated from `_quarto-book.yml` -- manual addition is required. 3. If the chapter is long, you may split content into includes under `chapters/_subfiles//`. Subfiles must NOT start with a heading and must NOT contain a references section. 4. Confirm it renders: `quarto render chapters/.qmd --to html`. +5. If the chapter contains `def`/`thm`/`lem`/`cor`/`prp` callout divs, + re-run `Rscript data-raw/callout-graph.R` to refresh + `inst/extdata/callout-graph.rds` and keep the concept map current. Style rules (see `.github/instructions/`): From 9a75c85202955949a552291c8ade894ba94813c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 08:08:31 +0000 Subject: [PATCH 17/17] Regenerate callout-graph.rds; drop now-redundant render-time index strip The committed callout-graph.rds was stale: def-expectation had acquired \index{} commands in its heading after the last regeneration, and the dependency graph had grown (new content merged from main). Re-run data-raw/callout-graph.R to refresh it (434 results, 115 links). With the .rds back in sync, the extraction script already strips \index{} from titles, so the duplicate render-time strip in concept-map.qmd is redundant and removed. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011CqGbHzLqKVGnbEpLEdadG --- chapters/concept-map.qmd | 1 - inst/extdata/callout-graph.rds | Bin 15135 -> 15160 bytes 2 files changed, 1 deletion(-) diff --git a/chapters/concept-map.qmd b/chapters/concept-map.qmd index 5f00eb1fc..923c9c776 100644 --- a/chapters/concept-map.qmd +++ b/chapters/concept-map.qmd @@ -33,7 +33,6 @@ library(ggraph) library(ggrepel) cg <- readRDS(here::here("inst/extdata/callout-graph.rds")) -cg$nodes$title <- trimws(gsub("\\\\index\\{[^}]*\\}", "", cg$nodes$title)) type_levels <- c("def", "thm", "lem", "cor", "prp") type_labels <- c( diff --git a/inst/extdata/callout-graph.rds b/inst/extdata/callout-graph.rds index bdbefa6b78999d020e6606c6543ac068cbce23fd..19b5337ac74a186d9efc23831e6ccdad807c5118 100644 GIT binary patch literal 15160 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