#pragma mode( separator(.,;) integer(h32) )

// ============================================================
// CHITABLE - beta 3d
// Chi-square test for a 2 x 2, 2 x 3 or 3 x 2 table
// ============================================================
// Author: Roger Metcalf, 2026.  Written with AI assistance
//   (Anthropic Claude); concept, direction and all hardware
//   testing by the author.
//
// FREE FOR EDUCATIONAL AND PERSONAL USE.  Keep this header with
//   the file and credit the author if you pass it on.  Not for
//   sale or commercial redistribution.  Provided as is, with no
//   warranty: check any result you intend to rely on.
// DISCLAIMER: a teaching and demonstration aid. Not validated for
//   forensic, clinical or legal use; do not rely on its results
//   for legal purposes.
//
// Tested on an HP Prime G2 and G1, both running the 2026-09-09
//   firmware (2.4.2), on 10 Oct 2026: the entry grid and its soft
//   keys, the Values form, the results pages and all three plots.
//   The arithmetic was
//   checked against scipy (chi2_contingency, fisher_exact,
//   association) and statsmodels on 4,900 random tables before
//   the PPL was written.  Betas 2, 3, 3a, 3b, 3c and 3d (below) have
//   NOT been run on hardware.  On the HP Prime Virtual Calculator
//   (2.4.2, build 2026-09-09, the calculators' firmware): beta 2
//   passed round 1 (17 Oct 2026); beta 3b, round 2 (17 Oct 2026),
//   did not compile until its two misplaced comment lines were
//   removed by hand, and then every CAS page was right (the tea
//   table: p = 17/35, exact (CAS)); beta 3c, beta 3b with that fix
//   and the round-2 string fixes, compiled and ran on the retest the
//   same afternoon (the tea table's pages 6 and 7 exact, the Results
//   menu whole).  Beta 3d is beta 3c with the fixes of the fleet
//   review of 19 Oct 2026 (below); it has run only in the PC harness.
//   The CAS's fallback (the "decimal" badge) has never been needed on
//   the emulator.  Beta 1j is the version both calculators ran, and
//   beta 1l (its fixes are all kept here) has not been re-run.
//
// Run ChiTable() from the Program Catalog.
//
// WHAT CHANGED in beta 3d (19 Oct 2026): the fleet review's fixes
//   (D8, D-CT-1 to D-CT-5, fleet review 19 Oct 2026).  Line numbers
//   are beta 3c's.
//   (1) THE RISK DIFFERENCE'S 95% CI IS NEWCOMBE'S (D8, Roger's
//   decision, 19 Oct 2026).  Beta 3c's Wald interval, RD +/- 1.96 SE
//   (csCompute 1111-1113), passed 1 for the tea table [3,1,1,3]
//   ((-0.1001, 1.1001)) and had width 0 for [5,0,0,5] ((1.0000,
//   1.0000)): of all 2 x 2 tables with row totals up to 12, 17.8%
//   had an end outside [-1, 1].  Newcombe's hybrid score interval
//   (1998, method 10) is built from each row's Wilson interval (new
//   csWils) and never does either: tea (-0.1355, 0.7891), [3,1,0,4]
//   (0.0852, 0.9544), [5,0,0,5] (0.3855, 1.0000).  Page 6's note
//   (csPage2x2 1830-1834) names it: "(95% CIs; Newcombe for RD, log
//   for RR and OR)"; with a zero cell it says the +0.5 goes into RR
//   and OR only.  Four names joined csCompute's LOCAL line 1054.
//   (2) p WITHOUT 1 - CDF (D-CT-1).  On the Prime 1 - CHISQUARE_CDF
//   is exactly 0 once the tail is below about 5E-13 (12 digits), so
//   CS_P in Home was 0 for any chi2 above about 52.  A new csUpper
//   gives the upper tail from a lower one: 2 NORMALD_CDF(0, 1,
//   -sqrt(chi2)) for df 1, EXP(-chi2 / 2) for df 2.  It computes the
//   p of chi2 (1082), of G2 (1084) and of Yates (1103), and so CS_P.
//   The screen is unchanged ("p < 0.0001" below 0.0001).
//   (3) THE ASSUMPTION, AS THE PAGES APPLY IT (D-CT-2).  Help page 1
//   (3198-3199) said "5 or more (at least 80% of cells, none below
//   1)", but pages 1, 3 and 5 flag ANY expected count below 5.  The
//   help now says "every expected count is 5 or more (the pages flag
//   any E below 5)".  Which rule to teach is still Roger's decision.
//   (4) "was rounded to" (D-CT-3, csEntry 1656): the typed value is
//   rounded before STRING, so 0.004 reads "0.00 was rounded to 0",
//   not "4.00E-3" with a small-capital E.
//   (5) HUGE COUNTS (D-CT-4).  A count above 9,999,999 is refused in
//   the cell editor (csEntry 1651-1661) and in the Values form
//   (csAskAll 1581-1585), with "Counts above 9,999,999 do not fit the
//   table: enter the data divided by 10." (x10 N already stops N
//   there).  On the exact page (csPageExact 1873-1875) the counts-put-
//   in lines are measured first; past 46 characters the page says
//   "(your counts are too long to write out here)" instead of running
//   off the right edge.
//   (6) A NEGATIVE COUNT IN THE VALUES FORM (D-CT-5, csAskAll 1583)
//   is no longer turned into 0 without a word: it stays in the grid,
//   and Run refuses it with "A count cannot be negative.", as the
//   cell editor does.
//   (7) The header's arithmetic notes (316, 327) say how p and the RD
//   interval are computed now.
//   (8) A DISCLAIMER line beside the terms of use at the top of this
//   header (Roger, 19 Oct 2026): a teaching aid, not validated for
//   forensic, clinical or legal use.  It is a comment; no page of the
//   program states the terms, so no page changed.  Nothing else
//   changed.
//
// WHAT CHANGED in beta 3c (17 Oct 2026): the round-2 emulator fixes.
//   Line numbers are beta 3b's.
//   (1) THE COMPILE FIX.  On the emulator (round 2) beta 3b gave
//   "Syntax Error" in csResMenu: two // comment lines (1277-1278)
//   sat INSIDE the open { of the CHOOSE list.  A comment inside an
//   open bracket is a syntax error on the Prime.  The comment now
//   sits above the CHOOSE (csResMenu 1270-1290); the list is as
//   before.  With those lines removed by hand, beta 3b compiled and
//   every CAS page was right.
//   (2) csCas (725-745).  Typed in Home on the emulator,
//   INSTRING("", "a") is "Error: Invalid input", and cR is "" after
//   any failed CAS call, so the fallback itself would have stopped
//   the program (the emulator's CAS never failed, so it did not
//   show).  The undef / error / infinity checks now run only on a
//   non-empty answer.  The other INSTRINGs (csIsFr 761, csFrDig
//   781) search "0123456789" for one character, never an empty
//   string; LEFT always takes 1 character (csNoZero 427, csCas 737)
//   and every MID starts at 1 or later, so LEFT(s, 0) = the whole
//   string and MID(s, 0, n) = an error cannot happen.
//   (3) Fixed decimals.  STRING(x, 2, d) prints SCIENTIFIC when
//   0 < |x| < 10^-d (STRING(0.096, 2, 1) is "9.6E-2" with a small-
//   capital E, emulator, round 2).  Every display that can meet such
//   a value now rounds first, STRING(ROUND(x, d), 2, d), so it shows
//   the rounded value (0.000 for one that rounds to 0): chi2, V, C,
//   G2, the expected counts, the contributions, the percentages, p1,
//   p2, z, Yates, the RD, RR and OR intervals, the critical value
//   and the Quit line (csEStr 1147, csScaleN 1234-1241, csGrid
//   1376-1430, csPageTest 1641-1657, csPageInterp 1679-1694,
//   csPage2x2 1728-1744, csBars 2016-2025, csMosaic 2193, csResBars
//   2316, csCvDraw 2557-2564, ChiTable 3281, csXX2Txt 965-974).
//   csSgn (455-459) and csNoZero (423-429) round inside, so the
//   residuals, RD, phi and APA's V are covered wherever they are
//   shown.  csAStr (444-452): a TYPED alpha below 0.0001 gets the
//   decimals it needs.  csPageExact (1811) no longer formats a
//   Fisher p below 0.0001 before replacing it with "(< 0.0001)".
//   p-values keep their own "p < 0.0001".  Counts (d = 0) and the
//   axes cannot reach the case (ticks counted up from 0; the
//   residual axis spans at least -3..3 in whole steps); the "was
//   rounded to" message in csEntry shows the count as typed and is
//   left as it was (device-tested).
//   Nothing else changed.
//
// WHAT CHANGED in beta 3b (17 Oct 2026): two menu lines, shortened.
//   On the emulator (HP Prime Virtual Calculator 2.4.2, build
//   2026-09-09; Emulator_Round1_Results_2026-10-17_v1.md) CHOOSE did not
//   wrap a line wider than its box: it cut it at the right edge.  Two
//   lines were cut: "Death penalty x defendant's rac[e]" in Examples
//   and "N / 10: every count divided by [10]" in the Results menu.
//   They are now "Death penalty x defendant" and "N / 10: counts
//   divided by 10"; the second example became "Death penalty x
//   victim" so the pair still reads as a pair (the table's own label,
//   shown with the results, still names the race).  Every other
//   CHOOSE line in the program is 27 characters or fewer.  Line
//   numbers are beta 3a's: csResMenu 1259-1263, csExamples 2951-2956.
//   Nothing else changed.
//
// WHAT CHANGED in beta 3a (17 Oct 2026): the CAS call form only.
//   CASProbe beta 1b, run on the HP Prime Virtual Calculator (build
//   2026-09-09), found that CAS(s) with s a variable hands the text
//   back UNEVALUATED, so beta 3's csCas (689-708) returned its own
//   input and every exact answer fell back to the decimal.  Its
//   IFERR line is now
//     STRING(EXPR("CAS(" + CHAR(34) + "string(" + s + ")" + CHAR(34) + ")"))
//   (BRIEF_CAS_v2), the quotes STRING() adds are dropped, and an
//   answer holding the infinity sign is refused like undef.  Every
//   answer this program reads is a whole number or a fraction, which
//   the Prime prints as desktop Giac does, so no reader changed
//   (csIsFr already takes digits, one "/" and a leading "-").  The
//   comments that said the call form was still to be decided were
//   updated.  Nothing else changed.
//
// beta 3 (16 Oct 2026): EXACT ANSWERS FROM THE CAS, built ON TOP OF
//   beta 2 (every beta 2 feature is kept).  Nothing about the CAS has
//   run on our calculators yet, so every CAS call goes through ONE
//   routine, csCas, and every exact answer keeps the decimal the
//   program already computes as its fallback; a badge in dodger
//   blue, "exact (CAS)" or "decimal", says which one is on screen.
//   csCompute and csFisher are untouched.  Line numbers are beta 2's.
//   (1) Fisher's exact p as a FRACTION, built in the CAS from comb()
//   with the table's own counts.  It adds exactly the tables csFisher
//   adds -- every table no more probable than yours, ties included,
//   with csFisher's own 1.0000001 tolerance -- so it is the same p:
//   "p = 17/35 = 0.4857" for the lady tasting tea, [[3,1],[1,3]].
//   Shown on results page 6 (csPage2x2, 1268; its lower lines move
//   down 4 to 7 px to make room) and on the Fisher plot (csFishDraw,
//   2162, whose title is shorter so the badge fits).  A fraction too
//   long for its line is never cut: the line says how many digits it
//   has, and keeps the decimal.
//   (2) A new LAST results page, "Where the numbers come from"
//   (csPageExact).  2 x 2: chi2 = N(ad - bc)^2 / (r1 r2 c1 c2) with
//   the student's counts put in and simplified by the CAS to one
//   fraction ("5549501668/421789635 = 13.157" for example 2), then
//   Fisher's p as a count of equally likely tables ("34 of the 70
//   ways, p = 34/70 = 17/35").  2 x 3 or 3 x 2: chi2 = sum (O - E)^2
//   / E with E = R x C / N, as one exact fraction.  The results now
//   have 7 pages (6 for a 2 x 3 or 3 x 2).
//   (3) Help page 5: what the CAS adds, and what the badge means.
//   (4) The CAS kit (csIStr, csCas) and its helpers (csIsFr, csFrDig,
//   csXDigC, csXKeyOf, csXFisher, csXChi, csXEnsure, csBadge,
//   csXPText, csXX2Txt).  csFishW moved up, unchanged, so they can
//   call it (PPL needs a routine defined before its first use).  The
//   CAS is called when a page OPENS and only if the table changed;
//   the answers are cached.  It is never called while a bar is
//   dragged, the critical line is dragged, or the morph plays.
//
// beta 2 (16 Oct 2026): touch and graphics, built ON TOP OF beta 1l.
//   The device-tested screens are kept; everything new is ADDED
//   (new routines, menu items and soft keys), and an old routine is
//   touched only where a feature needs it.  csCompute's arithmetic
//   is untouched.  Line numbers below are beta 1l's.
//   (1) Drag an observed bar (bars plot): press a SOLID bar and
//   drag up or down -- the count follows the finger, csCompute runs
//   on every sample, so every pale expected bar moves, and chi2 and
//   p (with the verdict colour) update under the plot.  The y-scale
//   is frozen while the finger is down and refitted on lift; on
//   lift csCheck runs, and a row or column left at 0 is refused
//   (the count goes back).  Up / Down do the same from the keys.
//   New: csBarDrag; csBars (991) freezes its scale, clips a bar to
//   the box and prints the verdict line; csPlotLoop (1140) routes
//   the press.  The Restore soft key puts the original counts back.
//   (2) Drag the dotted red critical line on the chi2 curve: alpha
//   = 1 - CHISQUARE_CDF(df, x) under the finger, the verdict flips
//   as the line crosses X2; on lift alpha snaps to .001 .005 .01
//   .02 .05 .10 .20 if within 10%, or stays as "custom alpha".  The
//   curve (1176-1249) is now a loop with the density cached once
//   per window (csCvFit) and a frame drawn from the cache
//   (csCvDraw); arrows step the ladder; Reset = 0.05.  csAStr (260)
//   shows a custom alpha with the decimals it needs.
//   (3) A soft-key strip -- Prev, Next, Bars, Mosaic, Curve, Menu --
//   on the results pages and every plot.  The page number moves to
//   a corner; the plot box ends at y 174 instead of 186 (csInit,
//   1375) so every plot's text stays above the strip at y 218.
//   (4) Results page 1: tap a cell for "E = 483 x 68 / 674 = 48.73
//   -- what it would hold if the variables were unrelated" (csGrid).
//   (5) Menu soft key: x10 N, N / 10 and Restore; x10 / divide
//   shows chi2, p and V before and after (csScaleN): V is the
//   effect size, p depends on N.
//   (6) The independence morph (Morph soft key or + on the mosaic):
//   tile heights glide from the row % to the column shares in ten
//   frames while the residual colours fade to grey (csMorph).
//   (7) New plot: adjusted residual per cell, blue up, red down,
//   dotted lines at +/-2 (csResBars).
//   (8) New plot (2 x 2): Fisher's exact distribution, one bar per
//   possible table; the observed in red, the tables no more
//   probable in magenta (their sum is the two-sided p); tap a bar
//   for its table (csFishW, csFishDraw, csFishPlot).
//   (9) Help page 4: why df = (r - 1)(c - 1), drawn on the table's
//   own shape (csDfPage).  (10) Examples: "Simpson (examples 1 +
//   2)" draws both death-penalty tables and the reversal (csSimpson).
//   (11) The help names every new gesture and soft key; the main
//   menu has 14 items (Quit is the last).  csView runs the results
//   pages and plots as one set of screens the soft keys move between.
//
// beta 1l (16 Oct 2026): four small fixes from the fleet review.
//   (1) The help pages named menu items by number ("menu 8",
//   "Mosaic (7)", "Bars (6)") and the "No table yet" message said
//   "(1-3)" -- correct today, stale the day an item moves.  They
//   now name the items.  (2) On the LAST results page a tap on the
//   right half, or Right, used to leave for the menu although the
//   footer says "tap = scroll output"; they now stay on that page,
//   whose footer says so, and Enter or Esc leaves.  (3) The curve
//   plot evaluated the chi-square density twice per pixel column
//   (once for the shaded tail, once for the curve); it is now
//   computed once into a list and read twice -- the same cache
//   NormExplorer uses (Lessons Learned 6).  (4) The df > 2 branch
//   of the curve's height is marked as kept for a future r x c
//   table.  The MAKELIST counter stays Home's K until GfxProbe T19
//   shows a LOCAL counter works.  Nothing else changed.
// beta 1k (10 Oct 2026): clean exits.  (1) Every "wait for the
//   finger to lift" loop now goes through csDrain(), which gives up
//   after one second, so a phantom touch cannot freeze the program.
//   (2) Quit returns the summary line to Home instead of PRINTing
//   it, so the Terminal no longer opens on the way out.  Nothing
//   else changed.
// beta 1j (10 Oct 2026): header only -- records the G1 run (soft
//   keys, Values form and plots all seen working).  No code
//   change from beta 1i.
// beta 1i (10 Oct 2026): the verdict in Roger's words -- "REJECT
//   H0 because the data show evidence of an association between
//   the variables" / "DO NOT REJECT H0 because the data do not
//   show an association between the variables", two lines each;
//   the interpretation page's sentence matches.
// beta 1h (10 Oct 2026): the verdict is two lines and names both
//   hypotheses -- "REJECT H0 because there is an association /
//   and ACCEPT H1: the variables are related", or "DO NOT REJECT
//   H0 because there is not enough / evidence of an association;
//   do not accept H1".  The interpretation page says the same.
//   The effect-size block moves down 6 px to make room.  Roger
//   confirms the results computed on the G2 (10 Oct 2026).
// beta 1g (10 Oct 2026): header only -- records the first run on
//   a G2.  No code change from beta 1f.
// beta 1f (10 Oct 2026): the decision reads "REJECT H0: there is
//   evidence of an association" or "DO NOT REJECT H0: there is
//   not enough evidence"; H0 and H1 are drawn with subscript
//   digits (CHAR(8320), CHAR(8321)) and alpha with its Greek
//   letter (CHAR(945)) wherever they appear on screen, in the
//   alpha dialog and in the menu.  (H1 rather than Ha: the
//   subscript 1 is a verified glyph, a subscript a is not.)
// beta 1e (10 Oct 2026): CHOOSE labels items 10, 11 and 12 as A,
//   B and C on the Prime, so the "no table yet" message now says
//   "load an example (A)"; the results footer reads "scroll
//   output" instead of "pages".
// beta 1d (10 Oct 2026): the soft key reads Run, not Done; the
//   menu items lose their numbers (CHOOSE numbers them) and are
//   kept short enough for the choose box (item 4 was cut off);
//   chi-square is now written with its own glyphs everywhere on
//   screen -- CHAR(967) + CHAR(178), i.e. chi and a superscript
//   two, kept in csX2S -- with a superscript two for G and the
//   squares in the formulas, and a multiplication sign for the
//   table shapes.  Code and comments stay ASCII; the Home export
//   names CS_X2 etc. are unchanged.
// beta 1c (10 Oct 2026): soft keys on the entry grid.  The bottom
//   strip now reads Edit | Values | Clear | (blank) | Cancel | Done,
//   drawn with DRAWMENU after each frame is blitted (DRAWMENU paints
//   the screen, not G1) and read back as a tap in the bottom strip,
//   y >= 218, in six 53-px slots -- the geometry CHOOSE_R, LibMenu
//   and PrimeFX use.  The labels never produce a key code (hp-prime-
//   kit), so the keys stay as shortcuts: Enter = Edit, + = Values,
//   Esc = Cancel.  Done runs the checks and, if the table passes,
//   computes; Cancel keeps the counts but marks the results stale.
// beta 1b (10 Oct 2026): first paste on the G2 stopped at
//   "LOCAL csR, csC, ..." with the cursor after csC.  Built-in
//   names are matched case-INsensitively: csC is CSC (cosecant),
//   so it cannot be a variable, and the program's own name,
//   ChiSquare, is CHISQUARE (the density the curve plot calls).
//   Renamed csC -> csNC, csR -> csNR and the program ChiSquare
//   -> ChiTable.  No change to the arithmetic or the screens.
// beta 1a (10 Oct 2026): after a library search on Rogers-HP.
//   Readouts and footers brought to house style (spaced
//   "label = value", comma-separated key legends); no change to
//   the arithmetic.  The constructs this program leans on were
//   found in the corpus: file-level LOCAL state (PrimeFX and
//   1,100 others), element assignment into an EXPORTed matrix
//   (GameOfLife's NextGen, conway), and INPUT label lists built
//   from variables (Fingerspelling) or concatenation (Foundation,
//   CostsReturn, Control Systems).  FEP v1.2 and ConTable2 v1.1
//   use the same Fisher, Yates, Haldane and Woolf conventions.
// ============================================================
//
// WHAT IT DOES
//   1  Pick the table shape from the menu: 2 x 2, 2 x 3, 3 x 2.
//   2  The empty table is drawn.  Tap a cell (or move with the
//      arrows and tap Edit) and type its OBSERVED count; Values
//      takes every count in one form; Clear zeroes the table.
//      Row and column totals update as you go.  Run computes.
//   3  Results, seven pages (six for a 2 x 3 or 3 x 2):
//        observed and EXPECTED COUNTS in every cell
//        row %, column % and % of N in every cell
//        X2, df, p, critical value, decision, G2, V, C
//        contribution to X2 and adjusted residual per cell
//        interpretation in words, with an APA-style line
//        2 x 2 only: Yates, Fisher's exact (also as a fraction),
//          z, RD, RR, OR
//        where the numbers come from: X2 and Fisher's p as EXACT
//          fractions on the student's counts (the CAS; beta 3)
//      On page 1 a tap on a cell shows how its E is made.
//   4  Five plots: observed vs expected bars (drag a solid bar to
//      change its count), a mosaic plot coloured by residual (with
//      the independence morph), the chi-square curve with the
//      p-value shaded and the critical value marked (drag the red
//      line to set alpha), the adjusted residual per cell, and for
//      a 2 x 2 Fisher's exact distribution.
//   5  The soft keys on the results pages and plots: Prev / Next
//      turn the page (or go to the next plot), Bars, Mosaic and
//      Curve jump straight there, Menu holds x10 N, N / 10 and
//      Restore.  On the bars plot the third key is Restore, on the
//      mosaic Morph, on the curve Reset (alpha back to 0.05).
//   6  Help: four pages, the last drawing why df = (r-1)(c-1).
//      Examples: four tables and the Simpson's paradox page.
//
// HOW THE SCREENS FIT TOGETHER (beta 2)
//   csView runs one "screen" after another: 1 the results pages,
//   2 bars, 3 mosaic, 4 curve, 5 residuals, 6 Fisher.  Each screen
//   routine returns the number of the next one (0 = main menu), so
//   a soft key on any screen can hand over to any other.
//
// THE ARITHMETIC
//   E(r,c) = (row total)(column total) / N
//   X2 = sum (O - E)^2 / E,  df = (rows - 1)(columns - 1)
//   p = the upper tail of the X2 curve (CHISQUARE alone is the pdf);
//     beta 3d: computed as 2 NORMALD_CDF(0, 1, -sqrt X2) for df 1 and
//     e^(-X2/2) for df 2 (csUpper), never as 1 - CHISQUARE_CDF, which
//     is exactly 0 on the Prime once p is below about 5E-13
//   G2 = 2 sum O ln(O / E)   (likelihood-ratio chi-square)
//   adjusted residual = (O - E) / sqrt(E (1 - R/N)(1 - C/N))
//     (Haberman); |2| or more marks a cell worth talking about
//   Cramer's V = sqrt(X2 / (N (min(r,c) - 1))); here min(r,c) = 2
//     so V = phi = Cohen's w: 0.1 small, 0.3 medium, 0.5 large
//   contingency coefficient C = sqrt(X2 / (X2 + N))
//   2 x 2: Yates X2 = sum (|O - E| - 0.5)^2 / E; Fisher's exact
//     two-sided p sums every table no more probable than the one
//     observed (the R / scipy convention); z = (p1 - p2) / SE
//     with the pooled SE, so z^2 = X2; risk difference with
//     Newcombe's hybrid score interval (beta 3d; beta 3c used Wald);
//     RR and OR with log (Woolf) intervals, +0.5 in every cell when
//     a cell is 0 (Haldane-Anscombe).
//   beta 3, EXACT (CAS, text for display only): for a 2 x 2,
//     X2 = N (ad - bc)^2 / (r1 r2 c1 c2), the same number as the sum
//     of (O - E)^2 / E; Fisher p = the sum, over the tables no more
//     probable than the observed one, of C(r1,a) C(r2,c1-a) / C(N,c1).
//     The decimals on screen still come from csCompute and csFisher.
//
// CONVENTION FOR THE 2 x 2 MEASURES
//   Rows are the groups (exposure), column 1 is the outcome of
//   interest.  p1 = O(1,1) / row 1 total, p2 = O(2,1) / row 2.
//
// HOME EXPORTS (after a run)
//   CS_X2  chi-square      CS_DF  degrees of freedom
//   CS_P   p-value         CS_V   Cramer's V
//   CS_N   grand total     CS_OBS observed matrix
//   CS_EXP expected matrix CS_RES adjusted residual matrix
//
// LAYOUT NOTE
//   Font 1 is about 6.5 px per character, font 2 about 8, font 3
//   about 9.5.  Text pages start at x = 12 and keep each line to
//   46 characters, and every TEXTOUT_P of variable text passes a
//   width so nothing can run off the right-hand edge.
// ============================================================


EXPORT CS_X2, CS_DF, CS_P, CS_V;
EXPORT CS_N, CS_OBS, CS_EXP, CS_RES;

// ---- the table ----
LOCAL csNR, csNC, csO, csHas, csSel, csAlpha;
//  csNR rows, csNC columns; csO the observed counts as ONE flat
//  list, cell (r,c) at index (r-1)*csNC + c; csHas = 1 once a
//  valid table has been computed; csSel the selected cell.
LOCAL csLbl, csLbl2, csFoot, csX2S, csSqS, csTimes;
LOCAL csH0, csH1, csAl;
//  csH0 and csH1 are H with a subscript 0 / 1, csAl is alpha.
//  csLbl names the data set, csLbl2 says what the rows and
//  columns are (examples only), csFoot is the grid footer (beta 2:
//  on the results pages it holds the page number, "3/6", drawn in
//  a corner because the soft keys take the bottom strip);
//  csX2S is the chi-square glyph pair, csSqS a superscript 2 and
//  csTimes the multiplication sign, all built with CHAR() so the
//  code itself stays ASCII.

// ---- the results (set in csCompute) ----
LOCAL csRT, csCT, csN, csE, csCon, csAdj;
LOCAL csX2, csDF, csP, csCrit, csG2, csPG;
LOCAL csV, csCC, csMinE, csN5, csN1, csBig;
// ---- 2 x 2 extras ----
LOCAL csPhi, csYat, csPY, csFish, csZ, csP1;
LOCAL csP2, csRD, csRDL, csRDH, csRR, csRRL;
LOCAL csRRH, csOdds, csORL, csORH, csHal;

// ---- grid geometry, shared by the drawing and the touch code ----
LOCAL gLX, gX0, gCW, gY0, gRH, gHY;
LOCAL gHH, gTH;
//  label column from gLX; data columns from gX0, gCW wide; data
//  rows from gY0, gRH tall; header at gHY, gHH tall; totals row
//  gTH tall.

// ---- plot box and the tiles of the last plot (for tapping) ----
LOCAL csPL, csPR, csPT, csPB;
LOCAL csTX1, csTY1, csTX2, csTY2;

// ---- house colours ----
LOCAL csBLUE, csMAG, csRED, csGRN, csORNG, csGRAY;
LOCAL csFOOT, csGRID, csLBLU, csLMAG, csLRED, csLGRN;
LOCAL csLORN, csLGRY, csMBLU, csMRED;

// ---- beta 2: the touch and graphics features ----
LOCAL csKeep, csYF, csYS, csYT, csBW, csMT;
//  csKeep: the counts as last computed from the grid or an example
//  (Restore puts them back).  csYF / csYS: the bars plot's y-top
//  and tick step frozen while a finger drags a bar (csYF = 0 means
//  "fit the scale"); csYT: the y-top of the last bars frame; csBW:
//  the width of one bar (the hit box of an observed bar).  csMT:
//  how far the independence morph has gone (0 = the observed
//  mosaic, 1 = every row looks like the "all" bar).
LOCAL csTapC, csDgBad, csFW, csFLo, csFHi, csFSel;
//  csTapC: the cell tapped on results page 1 (0 = none).  csDgBad
//  = 1 while a drag holds a row or column at 0.  csFW: Fisher's
//  probabilities for a = csFLo .. csFHi (a = Row 1 Col 1 count);
//  csFSel: the table shown in the Fisher plot's corner panel.
LOCAL csFDL, csFDH, csCvX, csCvY, csCvS, csCvDc;
LOCAL csCvP, csCvW;
//  csFDL .. csFDH: the a values drawn in the Fisher plot.  The
//  curve's window -- x top csCvX, y top csCvY, tick step csCvS with
//  csCvDc decimals -- and its cached pixel rows csCvP (one per
//  column, csCvW columns), computed once per window by csCvFit.

// ---- beta 3: the exact answers from the CAS, and their cache ----
LOCAL csCasOK, csXKey, csXFSt, csXCSt, csXFr, csXSum;
LOCAL csXX2, csXAd, csXPr, csXS, csXT, csXDg;
//  csCasOK: 1 = the last CAS answer was usable, 0 = it fell back.
//  csXKey: the table the cache belongs to ("2x2:3,1,1,3,"); when the
//  counts change, the cache is emptied and the CAS asked again.
//  csXFSt, the state of the exact Fisher p: 0 not asked yet, 1 the
//  CAS gave a fraction (csXFr), 2 the CAS did not answer, 3 too big
//  to ask (C(N, c1) has csXDg digits, more than 100), 4 none (not a
//  2 x 2, or more than 3,000 tables, csFisher's own guard).
//  csXSum: the CAS text of the sum of the ways, kept for csXS.
//  csXCSt: 1 once the exact page's answers were asked for.  Those
//  answers, as text ("" = the CAS did not give one): csXX2 chi2 as a
//  fraction, csXAd ad - bc, csXPr r1 r2 c1 c2, csXS the ways no
//  likelier than the observed table, csXT all the ways, C(N, c1).


// ------------------------------------------------------------
// SMALL HELPERS
// ------------------------------------------------------------

// Index of cell (r, c) in the flat list csO.
csIx(ixR, ixC)
BEGIN
  RETURN (ixR - 1) * csNC + ixC;
END;

// A "nice" step (1, 2 or 5 times a power of ten) at least as big
// as the value handed in -- Heckbert's rule, for axis ticks.
csNice(nnV)
BEGIN
  LOCAL nnE, nnF;
  IF nnV <= 0 THEN RETURN 1; END;
  nnE := FLOOR(LOG(nnV));
  nnF := nnV / 10^nnE;
  IF nnF <= 1 THEN RETURN 10^nnE; END;
  IF nnF <= 2 THEN RETURN 2 * 10^nnE; END;
  IF nnF <= 5 THEN RETURN 5 * 10^nnE; END;
  RETURN 10^(nnE + 1);
END;

// A p-value as text.  Tiny values are reported as "< 0.0001"
// because 1 - CDF loses its digits there anyway.
csPStr(psP)
BEGIN
  IF psP < 0.0001 THEN RETURN "p < 0.0001"; END;
  RETURN "p = " + STRING(psP, 2, 4);
END;

// A number in [0, 1] without its leading zero, APA style: "0.226"
// becomes ".226"; "1.000" is left alone.  MID(s, 2) drops the
// first character.
// beta 3c: rounded first -- STRING(x, 2, d) turns scientific when
// 0 < x < 10^-d (V = 0.004 would have printed "4.00E-3"); now ".00".
csNoZero(nzV, nzD)
BEGIN
  LOCAL nzS;
  nzS := STRING(ROUND(nzV, nzD), 2, nzD);
  IF LEFT(nzS, 1) == "0" THEN RETURN MID(nzS, 2); END;
  RETURN nzS;
END;

// The APA form of a p-value: p < .001, or three decimals.
csPApa(paP)
BEGIN
  IF paP < 0.001 THEN RETURN "p < .001"; END;
  IF paP > 0.999 THEN RETURN "p > .999"; END;
  RETURN "p = " + csNoZero(paP, 3);
END;

// alpha as text: two decimals, three when it is below 0.01.
// beta 2: a custom alpha (dragged on the curve, or typed) gets the
// decimals it needs -- 0.037 shows as 0.037, not 0.04, and 0.0037
// as 0.0037.  The usual values (0.05, 0.01, 0.001 ...) print
// exactly as before.
csAStr()
BEGIN
  LOCAL asD;
  // beta 3c: an alpha below 0.0001 can only be TYPED (Set alpha takes
  // any value between 0 and 1; the drag stops at 0.0001).  Four
  // decimals would print it in scientific form ("5.0000E-5"), so it
  // gets the decimals it needs: 0.00005.
  IF csAlpha < 0.0001 THEN
    asD := MIN(11, -FLOOR(LOG(csAlpha)));
    RETURN STRING(ROUND(csAlpha, asD), 2, asD);
  END;
  IF csAlpha < 0.01 THEN
    IF ABS(csAlpha - ROUND(csAlpha, 3)) > 0.0000001 THEN RETURN STRING(csAlpha, 2, 4); END;
    RETURN STRING(csAlpha, 2, 3);
  END;
  IF ABS(csAlpha - ROUND(csAlpha, 2)) > 0.0000001 THEN RETURN STRING(csAlpha, 2, 3); END;
  RETURN STRING(csAlpha, 2, 2);
END;

// A number with its sign: "+1.67" or "-3.63".
// beta 3c: rounded first, so a value nearer 0 than 10^-sgD reads
// "+0.00" instead of turning scientific ("+4.00E-3"); "+ 0" turns a
// rounded -0 into 0, which then takes the "+".
csSgn(sgV, sgD)
BEGIN
  LOCAL sgR;
  sgR := ROUND(sgV, sgD) + 0;
  IF sgR >= 0 THEN RETURN "+" + STRING(sgR, 2, sgD); END;
  RETURN STRING(sgR, 2, sgD);
END;

// Cohen's benchmarks for V (= w when min(r,c) = 2).
csVWord(vwV)
BEGIN
  IF vwV < 0.1 THEN RETURN "negligible"; END;
  IF vwV < 0.3 THEN RETURN "small"; END;
  IF vwV < 0.5 THEN RETURN "medium"; END;
  RETURN "large";
END;

// The colour of a column: 1 blue, 2 magenta, 3 green; and the
// pale version of it for the expected bars.
csColC(ccC)
BEGIN
  IF ccC == 1 THEN RETURN csBLUE; END;
  IF ccC == 2 THEN RETURN csMAG; END;
  RETURN csGRN;
END;

csPaleC(pcC)
BEGIN
  IF pcC == 1 THEN RETURN csLBLU; END;
  IF pcC == 2 THEN RETURN csLMAG; END;
  RETURN csLGRN;
END;

// Wait for the finger to lift -- but never for more than a second.
// MOUSE(1) is -1 while nothing touches the screen.  Without the
// time limit a stuck or phantom touch would hold the program here
// for ever, and the only way out would be a reset.
csDrain()
BEGIN
  LOCAL drT;
  drT := TICKS;                                  // milliseconds
  REPEAT UNTIL MOUSE(1) == -1 OR TICKS - drT > 1000;
END;

// Wait for any key or tap (drain, wait, drain).
csWaitAny()
BEGIN
  LOCAL waK, waM, waM1;
  REPEAT waK := GETKEY; UNTIL waK == -1;
  csDrain();
  REPEAT
    waK := GETKEY;
    waM := MOUSE;
    waM1 := waM(1);
  UNTIL waK > -1 OR SIZE(waM1) > 0;
  csDrain();
  REPEAT waK := GETKEY; UNTIL waK == -1;
END;

// A dotted vertical line (LINE_P has no dash style).
csDotV(dvG, dvX, dvY1, dvY2, dvCol)
BEGIN
  LOCAL dvI;
  FOR dvI FROM dvY1 TO dvY2 STEP 3 DO
    PIXON_P(dvG, dvX, dvI, dvCol);
  END;
END;

// beta 2: a dotted HORIZONTAL line on G1 (the residual plot's +/-2
// lines), the same 3-px dots as csDotV.
csDotH(dhX1, dhX2, dhY, dhCol)
BEGIN
  LOCAL dhI;
  FOR dhI FROM dhX1 TO dhX2 STEP 3 DO
    PIXON_P(G1, dhI, dhY, dhCol);
  END;
END;

// beta 2: a fresh copy of a list, element by element, so that a
// saved copy (csKeep) can never be changed through the original.
csCopy(cyL)
BEGIN
  LOCAL cyI, cyN;
  cyN := MAKELIST(0, K, 1, SIZE(cyL), 1);
  FOR cyI FROM 1 TO SIZE(cyL) DO
    cyN(cyI) := cyL(cyI);
  END;
  RETURN cyN;
END;

// Row and column totals and N, straight from the counts.  Used
// live while the table is being typed, and again by csCompute.
csTotals()
BEGIN
  LOCAL ttR, ttC, ttO;
  csRT := MAKELIST(0, K, 1, csNR, 1);
  csCT := MAKELIST(0, K, 1, csNC, 1);
  csN := 0;
  FOR ttR FROM 1 TO csNR DO
    FOR ttC FROM 1 TO csNC DO
      ttO := csO(csIx(ttR, ttC));
      csRT(ttR) := csRT(ttR) + ttO;
      csCT(ttC) := csCT(ttC) + ttO;
      csN := csN + ttO;
    END;
  END;
END;


// ------------------------------------------------------------
// FISHER'S EXACT TEST, two-sided, for the 2 x 2 table a b / c d
// The hypergeometric probability of every table with the same
// margins is built outward from the most probable table by
// ratios, so nothing overflows.  The two-sided p adds every
// table no more probable than the observed one (the convention
// R and scipy use).  Returns -1 when the table is too big for
// the loop (then the chi-square is excellent anyway).
// ------------------------------------------------------------
csFisher(fxA, fxB, fxC, fxD)
BEGIN
  LOCAL fxR1, fxR2, fxC1, fxN, fxLo, fxHi;
  LOCAL fxMode, fxK, fxW, fxTot, fxObs, fxLim;
  LOCAL fxSum;
  fxR1 := fxA + fxB; fxR2 := fxC + fxD; fxC1 := fxA + fxC;
  fxN := fxR1 + fxR2;
  fxLo := MAX(0, fxC1 - fxR2);
  fxHi := MIN(fxR1, fxC1);
  IF fxHi - fxLo > 3000 THEN RETURN -1; END;
  // the modal table: a = floor((R1+1)(C1+1)/(N+2)), kept in range
  fxMode := FLOOR((fxR1 + 1) * (fxC1 + 1) / (fxN + 2));
  fxMode := MIN(MAX(fxMode, fxLo), fxHi);
  // pass 1: the total weight and the observed table's weight
  fxTot := 1; fxObs := -1;
  IF fxA == fxMode THEN fxObs := 1; END;
  fxW := 1; fxK := fxMode;
  WHILE fxK < fxHi DO
    fxW := fxW * (fxR1 - fxK) * (fxC1 - fxK) / ((fxK + 1) * (fxR2 - fxC1 + fxK + 1));
    fxTot := fxTot + fxW;
    IF (fxK + 1) == fxA THEN fxObs := fxW; END;
    fxK := fxK + 1;
  END;
  fxW := 1; fxK := fxMode;
  WHILE fxK > fxLo DO
    fxW := fxW * fxK * (fxR2 - fxC1 + fxK) / ((fxR1 - fxK + 1) * (fxC1 - fxK + 1));
    fxTot := fxTot + fxW;
    IF (fxK - 1) == fxA THEN fxObs := fxW; END;
    fxK := fxK - 1;
  END;
  // pass 2: add every table at most as probable as the observed
  // one (a hair of tolerance for rounding, as R does)
  fxLim := fxObs * 1.0000001;
  fxSum := 0;
  IF 1 <= fxLim THEN fxSum := 1; END;
  fxW := 1; fxK := fxMode;
  WHILE fxK < fxHi DO
    fxW := fxW * (fxR1 - fxK) * (fxC1 - fxK) / ((fxK + 1) * (fxR2 - fxC1 + fxK + 1));
    IF fxW <= fxLim THEN fxSum := fxSum + fxW; END;
    fxK := fxK + 1;
  END;
  fxW := 1; fxK := fxMode;
  WHILE fxK > fxLo DO
    fxW := fxW * fxK * (fxR2 - fxC1 + fxK) / ((fxR1 - fxK + 1) * (fxC1 - fxK + 1));
    IF fxW <= fxLim THEN fxSum := fxSum + fxW; END;
    fxK := fxK - 1;
  END;
  RETURN MIN(1, fxSum / fxTot);
END;

// beta 3: moved here from the Fisher plot section (beta 2 line 2123),
// unchanged.  csXFisher and results page 6 use it, and PPL needs a
// routine defined before the first routine that calls it.
// The probabilities, by the same ratios outward from the most
// probable table that csFisher walks (so nothing overflows), kept
// in csFW for a = csFLo .. csFHi and scaled to add to 1.  Returns 0
// when there are more than 3,000 tables (csFisher's own guard).
csFishW()
BEGIN
  LOCAL fwR1, fwR2, fwC1, fwN, fwM, fwK;
  LOCAL fwW, fwT, fwI;
  fwR1 := csRT(1); fwR2 := csRT(2); fwC1 := csCT(1);
  fwN := fwR1 + fwR2;
  csFLo := MAX(0, fwC1 - fwR2);
  csFHi := MIN(fwR1, fwC1);
  IF csFHi - csFLo > 3000 THEN RETURN 0; END;
  // the modal table, as in csFisher
  fwM := FLOOR((fwR1 + 1) * (fwC1 + 1) / (fwN + 2));
  fwM := MIN(MAX(fwM, csFLo), csFHi);
  csFW := MAKELIST(0, K, 1, csFHi - csFLo + 1, 1);
  csFW(fwM - csFLo + 1) := 1;
  // upward from the mode: weight of a = fwK + 1 from that of fwK
  fwW := 1; fwK := fwM;
  WHILE fwK < csFHi DO
    fwW := fwW * (fwR1 - fwK) * (fwC1 - fwK) / ((fwK + 1) * (fwR2 - fwC1 + fwK + 1));
    csFW(fwK - csFLo + 2) := fwW;
    fwK := fwK + 1;
  END;
  // downward: weight of a = fwK - 1 from that of fwK
  fwW := 1; fwK := fwM;
  WHILE fwK > csFLo DO
    fwW := fwW * fwK * (fwR2 - fwC1 + fwK) / ((fwR1 - fwK + 1) * (fwC1 - fwK + 1));
    csFW(fwK - csFLo) := fwW;
    fwK := fwK - 1;
  END;
  // scale to probabilities
  fwT := 0;
  FOR fwI FROM 1 TO SIZE(csFW) DO fwT := fwT + csFW(fwI); END;
  FOR fwI FROM 1 TO SIZE(csFW) DO csFW(fwI) := csFW(fwI) / fwT; END;
  RETURN 1;
END;


// ------------------------------------------------------------
// BETA 3: EXACT ANSWERS FROM THE CAS, BEHIND THE FENCE
// (Lessons Learned v7g section 9b; the kit in BRIEF_CAS.md, tag cs)
//
// The CAS (the calculator's computer algebra system) can do what the
// rest of the program cannot: exact fractions and whole numbers of
// any size.  Students see that Fisher's test is COUNTING, and that
// chi2 is one exact number built from their own counts.
//
// The CAS has run only on the emulator (CASProbe beta 1b, 17 Oct
// 2026), not on our calculators, so the fence is strict:
//   - every CAS call goes through ONE routine, csCas, in the call
//     form CASProbe beta 1b found (beta 3a);
//   - every exact answer keeps the decimal the program already has
//     (csFish from csFisher, csX2 from csCompute) as its fallback,
//     so a CAS failure costs the student only the fraction, and the
//     badge then says "decimal" instead of "exact (CAS)";
//   - CAS text is built from WHOLE NUMBERS only, written digit by
//     digit by csIStr: Home's STRING() follows the Home number
//     format (digit-grouping marks, a special minus sign), which the
//     CAS cannot read, and there is never a decimal point in it;
//   - the CAS is asked when a page OPENS (results page 6, the exact
//     page, the Fisher plot) and only when the table has changed;
//     the answers are cached in csX... and every frame is drawn from
//     the cache.  No CAS call is made while a bar or the critical
//     line is dragged, or while the morph plays;
//   - the CAS's own variable is csj, a tagged name: a student may
//     have given x, n or k a value in the CAS view;
//   - CAS answers are TEXT for display.  Nothing is drawn from them.
// csCompute and csFisher are untouched: they remain the device-
// tested decimal engine.
// ------------------------------------------------------------

// A whole number as plain digits: no digit-grouping marks, no decimal point, no
// exponent.  Home's STRING() follows the Home number format (PyProbe saw grouping
// marks and a special minus sign), which the CAS cannot read, so CAS text is built
// only from this routine.
// (The kit calls the parameter xxN; here it is csIv, because csN is
// already this program's grand total.)
csIStr(csIv)
BEGIN
  LOCAL dS, dV, dD;
  dV := ROUND(ABS(csIv), 0);
  IF dV == 0 THEN RETURN "0"; END;
  dS := "";
  WHILE dV > 0 DO
    dD := dV MOD 10;
    dS := CHAR(48 + dD) + dS;
    dV := (dV - dD) / 10;
  END;
  IF csIv < 0 THEN dS := "-" + dS; END;
  RETURN dS;
END;

// The ONE place this program calls the CAS.  It returns the CAS answer as text,
// or "" if the CAS failed, gave undef, an error or an infinity.
// The call form is the one CASProbe beta 1b found on the HP Prime Virtual
// Calculator (build 2026-09-09, 17 Oct 2026): CAS(s) with s a variable hands
// the text back UNEVALUATED, so the text is built into a literal CAS("...")
// that EXPR reads at run time, and the CAS is asked for string(...) of the
// answer, so a big integer crosses into PPL whole, not as a 12-digit real.
csCas(csS)
BEGIN
  LOCAL cR;
  cR := "";
  IFERR
    cR := STRING(EXPR("CAS(" + CHAR(34) + "string(" + csS + ")" + CHAR(34) + ")"));
  THEN
    cR := "";
  END;
  // The answer is a PPL string, and STRING() of it adds one pair of quotes
  // (CASProbe beta 1b T12, emulator): drop that pair.
  IF DIM(cR) >= 2 THEN
    IF LEFT(cR, 1) == CHAR(34) AND MID(cR, DIM(cR), 1) == CHAR(34) THEN
      cR := MID(cR, 2, DIM(cR) - 2);
    END;
  END;
  // undef, an error, or an infinity (CHAR(8734) is the infinity sign; it
  // also catches the Prime's plus-minus infinity, 1/0): no usable answer
  // beta 3c: an empty answer is already a failure; INSTRING("", ...) is
  // "Invalid input" on the Prime (emulator, 17 Oct 2026), so the checks
  // run only on a non-empty answer
  IF DIM(cR) > 0 THEN
    IF INSTRING(cR, "undef") > 0 OR INSTRING(cR, "rror") > 0 OR INSTRING(cR, CHAR(8734)) > 0 THEN cR := ""; END;
  END;
  RETURN cR;
END;

// 1 if fsS is an exact whole number ("8116", "-33") or, with fsSl =
// 1, an exact fraction ("17/35"): digits, at most one "/" between
// them, and a "-" only in front.  Anything else is 0 -- a decimal
// such as "0.4857" (what the answer would look like if the CAS
// handed it back as a Home real), an unevaluated "sum(...)", or an
// empty string -- and the program shows its own decimal instead.
csIsFr(fsS, fsSl)
BEGIN
  LOCAL fsI, fsN, fsC, fsK, fsOK, fsBad;
  fsN := DIM(fsS);
  IF fsN == 0 THEN RETURN 0; END;
  fsOK := 1; fsK := 0;                       // fsK counts the "/" signs
  FOR fsI FROM 1 TO fsN DO
    fsC := MID(fsS, fsI, 1);
    IF INSTRING("0123456789", fsC) == 0 THEN
      // not a digit: allowed only as a "/" inside a fraction, or as
      // a "-" in front
      fsBad := 1;
      IF fsC == "/" AND fsSl == 1 AND fsI > 1 AND fsI < fsN THEN fsK := fsK + 1; fsBad := 0; END;
      IF fsC == "-" AND fsI == 1 AND fsN > 1 THEN fsBad := 0; END;
      IF fsBad == 1 THEN fsOK := 0; END;
    END;
  END;
  IF fsK > 1 THEN fsOK := 0; END;
  RETURN fsOK;
END;

// How many digits an exact answer has (its "/" and "-" not counted):
// what a line says when a fraction is too long to show.
csFrDig(fgS)
BEGIN
  LOCAL fgI, fgN;
  fgN := 0;
  FOR fgI FROM 1 TO DIM(fgS) DO
    IF INSTRING("0123456789", MID(fgS, fgI, 1)) > 0 THEN fgN := fgN + 1; END;
  END;
  RETURN fgN;
END;

// About how many digits C(dcN, dcM) has: log10 of the product
// (n - m + 1)/1 x (n - m + 2)/2 x ... x n/m, one LOG per factor.  A
// fraction p = (ways) / C(N, c1) can have no more digits below the
// line than this, so it tells, before the CAS is asked, whether the
// answer could ever fit on a screen.  More than 2,000 factors means
// hundreds of digits: 9999, without the loop.
csXDigC(dcN, dcM)
BEGIN
  LOCAL dcI, dcK, dcS;
  dcK := MIN(dcM, dcN - dcM);
  IF dcK <= 0 THEN RETURN 1; END;
  IF dcK > 2000 THEN RETURN 9999; END;
  dcS := 0;
  FOR dcI FROM 1 TO dcK DO
    dcS := dcS + LOG((dcN - dcK + dcI) / dcI);
  END;
  RETURN IP(dcS + 0.0000001) + 1;
END;

// The table the cache belongs to: its shape and every count, as text
// ("2x2:3,1,1,3,").  A changed count gives another key.
csXKeyOf()
BEGIN
  LOCAL kyS, kyI;
  kyS := csIStr(csNR) + "x" + csIStr(csNC) + ":";
  FOR kyI FROM 1 TO csNR * csNC DO
    kyS := kyS + csIStr(csO(kyI)) + ",";
  END;
  RETURN kyS;
END;

// Fisher's exact two-sided p as a FRACTION (2 x 2 only).  With the
// margins fixed, the table with top-left count a can happen in
// C(r1, a) x C(r2, c1 - a) of the C(N, c1) equally likely ways.  The
// two-sided p adds every table NO MORE PROBABLE than the observed
// one -- csFisher's rule, used here to the letter: csFishW gives
// every table's probability, and a table counts when its
// probability is at most the observed one's times 1.0000001 (the
// hair of tolerance csFisher and R use, so a tie always counts).
// These are exactly the bars csFishDraw paints red and magenta.
// Because the probabilities rise to one peak and fall again, the
// tables that count form one or two runs of a (the two tails); each
// run becomes one sum of seq(...) with whole-number bounds -- seq,
// not sum over a symbol, which sends Giac solving Gamma equations
// (Lessons Learned v7g 9b) -- and the CAS adds the ways and divides
// by C(N, c1).  The answer must be a fraction (csIsFr), or the
// decimal stays.  Sets csXFSt, csXFr, csXSum, csXDg and csCasOK.
csXFisher()
BEGIN
  LOCAL xfA, xfObs, xfIn, xfRun, xfT, xfS;
  LOCAL xfR1, xfR2, xfC1;
  csXFr := ""; csXSum := ""; csXFSt := 4;
  IF csNR <> 2 OR csNC <> 2 THEN RETURN 0; END;
  IF csFishW() == 0 THEN RETURN 0; END;      // more than 3,000 tables
  xfR1 := csRT(1); xfR2 := csRT(2); xfC1 := csCT(1);
  // too big to be worth asking (an answer of hundreds of digits)?
  csXDg := csXDigC(csN, xfC1);
  IF csXDg > 100 THEN csXFSt := 3; RETURN 0; END;
  xfObs := csFW(csO(1) - csFLo + 1);         // the observed table
  xfS := ""; xfRun := -1;                    // xfRun: where the open run began
  FOR xfA FROM csFLo TO csFHi DO
    xfIn := 0;
    IF csFW(xfA - csFLo + 1) <= xfObs * 1.0000001 THEN xfIn := 1; END;
    IF xfIn == 1 AND xfRun < 0 THEN xfRun := xfA; END;
    // a run ends at the first table that does not count, or at the end
    IF xfRun >= 0 AND (xfIn == 0 OR xfA == csFHi) THEN
      xfT := xfA;
      IF xfIn == 0 THEN xfT := xfA - 1; END;
      IF DIM(xfS) > 0 THEN xfS := xfS + "+"; END;
      xfS := xfS + "sum(seq(comb(" + csIStr(xfR1) + ",csj)*comb(" + csIStr(xfR2) + "," + csIStr(xfC1) + "-csj),csj," + csIStr(xfRun) + "," + csIStr(xfT) + "))";
      xfRun := -1;
    END;
  END;
  csXSum := xfS;
  csXFr := csCas("(" + xfS + ")/comb(" + csIStr(csN) + "," + csIStr(xfC1) + ")");
  IF csIsFr(csXFr, 1) == 0 THEN csXFr := ""; END;
  csCasOK := 0; csXFSt := 2;
  IF DIM(csXFr) > 0 THEN csCasOK := 1; csXFSt := 1; END;
  RETURN csCasOK;
END;

// The exact page's other answers.  chi2 as ONE fraction: for a 2 x 2
// the shortcut N (ad - bc)^2 / (r1 r2 c1 c2), with ad - bc and the
// product of the totals asked for too (they are whole numbers of any
// size); otherwise the sum over the cells of (O - E)^2 / E with every
// E written as R*C/N, so the CAS keeps every E exact.  Then, if the
// Fisher fraction worked, its two halves: the ways that count (the
// sum csXFisher built) and all the ways, C(N, c1).  Sets csXX2,
// csXAd, csXPr, csXS, csXT, csXCSt and csCasOK.
csXChi()
BEGIN
  LOCAL xcR, xcC, xcI, xcE, xcS, xcT;
  csXX2 := ""; csXAd := ""; csXPr := ""; csXS := ""; csXT := "";
  IF csNR == 2 AND csNC == 2 THEN
    xcS := csIStr(csO(1)) + "*" + csIStr(csO(4)) + "-" + csIStr(csO(2)) + "*" + csIStr(csO(3));
    xcT := csIStr(csRT(1)) + "*" + csIStr(csRT(2)) + "*" + csIStr(csCT(1)) + "*" + csIStr(csCT(2));
    csXAd := csCas(xcS);                     // ad - bc
    IF csIsFr(csXAd, 0) == 0 THEN csXAd := ""; END;
    csXPr := csCas(xcT);                     // r1 r2 c1 c2
    IF csIsFr(csXPr, 0) == 0 THEN csXPr := ""; END;
    csXX2 := csCas(csIStr(csN) + "*(" + xcS + ")^2/(" + xcT + ")");
  ELSE
    xcS := "";
    FOR xcR FROM 1 TO csNR DO
      FOR xcC FROM 1 TO csNC DO
        xcI := csIx(xcR, xcC);
        xcE := csIStr(csRT(xcR)) + "*" + csIStr(csCT(xcC)) + "/" + csIStr(csN);
        IF DIM(xcS) > 0 THEN xcS := xcS + "+"; END;
        xcS := xcS + "(" + csIStr(csO(xcI)) + "-" + xcE + ")^2/(" + xcE + ")";
      END;
    END;
    csXX2 := csCas(xcS);
  END;
  IF csIsFr(csXX2, 1) == 0 THEN csXX2 := ""; END;
  IF csXFSt == 1 THEN
    csXS := csCas(csXSum);
    IF csIsFr(csXS, 0) == 0 THEN csXS := ""; END;
    csXT := csCas("comb(" + csIStr(csN) + "," + csIStr(csCT(1)) + ")");
    IF csIsFr(csXT, 0) == 0 THEN csXT := ""; END;
  END;
  csCasOK := 0;
  IF DIM(csXX2) > 0 THEN csCasOK := 1; END;
  csXCSt := 1;
END;

// Make sure the cache holds the answers for the table now in csO.
// A different table empties it.  Then the Fisher fraction is asked
// for if it has not been yet, and with xeAll = 1 the exact page's
// answers too.  Called when a page opens: results page 6, the exact
// page, the Fisher plot.  The same table again costs no CAS call.
csXEnsure(xeAll)
BEGIN
  LOCAL xeK, xeNew;
  xeK := csXKeyOf();
  xeNew := 1;
  IF xeK == csXKey THEN xeNew := 0; END;
  IF xeNew == 1 THEN
    csXKey := xeK;
    csXFSt := 0; csXCSt := 0; csXFr := ""; csXSum := "";
    csXX2 := ""; csXAd := ""; csXPr := ""; csXS := ""; csXT := "";
    csXDg := 0;
  END;
  IF csXFSt == 0 THEN csXFisher(); END;
  IF xeAll == 1 AND csXCSt == 0 THEN csXChi(); END;
END;

// The badge beside an exact answer, in the footer's dodger blue:
// "exact (CAS)" when the line came from the CAS, "decimal" when it
// fell back to the program's own number.
csBadge(bdX, bdY, bdOK)
BEGIN
  IF bdOK == 1 THEN
    TEXTOUT_P("exact (CAS)", G1, bdX, bdY, 1, csFOOT, 76);
  ELSE
    TEXTOUT_P("decimal", G1, bdX, bdY, 1, csFOOT, 76);
  END;
END;

// Fisher's two-sided p for a line of xpMax characters:
//   "p = 17/35 = 0.4857"               the exact fraction fits;
//   "p = 0.0001 (178-digit fraction)"  it does not: never cut, the
//                                      digits are counted instead;
//   "p = 0.4857"                       no exact answer (decimal).
// The decimal is always csFisher's own value.
csXPText(xpMax)
BEGIN
  LOCAL xpD, xpS;
  IF csFish < 0.0001 THEN xpD := "(< 0.0001)"; ELSE xpD := "= " + STRING(csFish, 2, 4); END;
  IF csXFSt == 1 THEN
    xpS := "p = " + csXFr + " " + xpD;
    IF DIM(xpS) <= xpMax THEN RETURN xpS; END;
    RETURN csPStr(csFish) + " (" + STRING(csFrDig(csXFr), 2, 0) + "-digit fraction)";
  END;
  RETURN csPStr(csFish);
END;

// chi2 for a line of xtMax characters, the same three ways:
// "= 5549501668/421789635 = 13.157", "= 13.157 (41-digit fraction)",
// or "= 13.157".  The decimal is csCompute's csX2.
csXX2Txt(xtMax)
BEGIN
  LOCAL xtS;
  // beta 3c: csX2 rounded before STRING (a chi2 below 0.001 would print
  // in scientific form)
  IF DIM(csXX2) > 0 THEN
    xtS := "= " + csXX2 + " = " + STRING(ROUND(csX2, 3), 2, 3);
    IF DIM(xtS) <= xtMax THEN RETURN xtS; END;
    RETURN "= " + STRING(ROUND(csX2, 3), 2, 3) + " (" + STRING(csFrDig(csXX2), 2, 0) + "-digit fraction)";
  END;
  RETURN "= " + STRING(ROUND(csX2, 3), 2, 3);
END;


// ------------------------------------------------------------
// THE ARITHMETIC
// ------------------------------------------------------------

// beta 3d: the p-value of a chi-square, its upper tail P(X2 >= upX)
// with upDF degrees of freedom -- computed WITHOUT "1 - CDF".
// Why: the Prime keeps 12 significant digits, so CHISQUARE_CDF can
// be at most 0.999999999999 and 1 - CDF becomes exactly 0 once the
// true tail is below about 5E-13 (X2 above about 52 for df 1, 55 for
// df 2).  The page says "p < 0.0001" either way, but CS_P in Home was
// 0.  This program only ever has df 1 (2 x 2) or df 2 (2 x 3, 3 x 2),
// and for both the upper tail has a closed form that the Prime
// computes to its last digit, because it is built from a LOWER tail
// (a small number is stored with all its digits; 1 minus a number
// near 1 is not):
//   df 1: a chi-square with 1 df is z^2 for a standard normal z, so
//         P(X2 >= x) = P(|z| >= sqrt x) = 2 x P(z <= -sqrt x)
//   df 2: a chi-square with 2 df is an exponential with mean 2, so
//         P(X2 >= x) = e^(-x/2) exactly
// X2 = 0 (or a G2 a hair below 0 from rounding) gives p = 1, as the
// CDF did; the square root of a negative number is never taken.
csUpper(upDF, upX)
BEGIN
  IF upX <= 0 THEN RETURN 1; END;
  IF upDF == 1 THEN RETURN 2 * NORMALD_CDF(0, 1, -(upX^0.5)); END;
  IF upDF == 2 THEN RETURN EXP(-upX / 2); END;
  // kept for a future r x c table (df 3 or more): 1 - CDF
  RETURN 1 - CHISQUARE_CDF(upDF, upX);
END;

// beta 3d: one end of the WILSON score interval for a proportion,
// wsK successes out of wsN, at z = wsZ (1.96 for 95%); wsS = -1
// gives the lower end, +1 the upper end.  csCompute builds the risk
// difference's interval from these (Newcombe's method, below).
// Wilson's interval holds every p that a z test of "the true share
// is p" would NOT reject: it solves |phat - p| = z sqrt(p (1 - p) / n)
// for p.  Because the SE is taken at p itself and not at phat, it
// stays inside [0, 1] and never shrinks to width 0:
//   centre = (phat + z^2 / (2n)) / (1 + z^2 / n)
//   half   = z sqrt(phat (1 - phat) / n + z^2 / (4 n^2)) / (1 + z^2 / n)
// (wsQ below is z^2 / n, so wsQ / (4n) is z^2 / (4 n^2).)  At k = 0
// the lower end is exactly 0, and at k = n the upper end is exactly
// 1: those two are returned as such, so rounding in the last digit
// can never push an end past 0 or 1.
csWils(wsK, wsN, wsZ, wsS)
BEGIN
  LOCAL wsP, wsQ, wsC, wsH;
  IF wsK == 0 AND wsS < 0 THEN RETURN 0; END;
  IF wsK == wsN AND wsS > 0 THEN RETURN 1; END;
  wsP := wsK / wsN;
  wsQ := wsZ * wsZ / wsN;
  wsC := (wsP + wsQ / 2) / (1 + wsQ);
  wsH := wsZ * (wsP * (1 - wsP) / wsN + wsQ / (4 * wsN))^0.5 / (1 + wsQ);
  RETURN wsC + wsS * wsH;
END;

csCompute()
BEGIN
  LOCAL cpR, cpC, cpI, cpO, cpE, cpM;
  LOCAL cpA, cpB, cpCc, cpD, cpSe, cpPb;
  LOCAL cpZ, cpHal, cpL1, cpU1, cpL2, cpU2;
  csTotals();
  cpM := csNR * csNC;
  csE := MAKELIST(0, K, 1, cpM, 1);
  csCon := MAKELIST(0, K, 1, cpM, 1);
  csAdj := MAKELIST(0, K, 1, cpM, 1);
  csX2 := 0; csG2 := 0; csMinE := csN; csN5 := 0; csN1 := 0;
  FOR cpR FROM 1 TO csNR DO
    FOR cpC FROM 1 TO csNC DO
      cpI := csIx(cpR, cpC);
      cpO := csO(cpI);
      // expected count under independence
      cpE := csRT(cpR) * csCT(cpC) / csN;
      csE(cpI) := cpE;
      // this cell's contribution to X2
      csCon(cpI) := (cpO - cpE)^2 / cpE;
      csX2 := csX2 + csCon(cpI);
      // adjusted (Haberman) residual: roughly a z-score
      csAdj(cpI) := (cpO - cpE) / (cpE * (1 - csRT(cpR) / csN) * (1 - csCT(cpC) / csN))^0.5;
      // likelihood-ratio statistic; a 0 cell contributes 0
      IF cpO > 0 THEN csG2 := csG2 + 2 * cpO * LN(cpO / cpE); END;
      IF cpE < csMinE THEN csMinE := cpE; END;
      IF cpE < 5 THEN csN5 := csN5 + 1; END;
      IF cpE < 1 THEN csN1 := csN1 + 1; END;
    END;
  END;
  csDF := (csNR - 1) * (csNC - 1);
  // CHISQUARE(n, x) is the DENSITY; the p-value is the upper tail
  // beta 3d: csUpper, not 1 - CHISQUARE_CDF, so a tiny p keeps its
  // digits (CS_P in Home was exactly 0 past X2 = 52)
  csP := csUpper(csDF, csX2);
  csCrit := CHISQUARE_ICDF(csDF, 1 - csAlpha);
  csPG := csUpper(csDF, csG2);
  csV := (csX2 / (csN * (MIN(csNR, csNC) - 1)))^0.5;
  csCC := (csX2 / (csX2 + csN))^0.5;
  // the most surprising cell: largest |adjusted residual|
  csBig := 1;
  FOR cpI FROM 2 TO cpM DO
    IF ABS(csAdj(cpI)) > ABS(csAdj(csBig)) THEN csBig := cpI; END;
  END;

  // ---- 2 x 2 extras ----
  IF csNR == 2 AND csNC == 2 THEN
    cpA := csO(1); cpB := csO(2); cpCc := csO(3); cpD := csO(4);
    cpZ := 1.959964;
    csPhi := (cpA * cpD - cpB * cpCc) / (csRT(1) * csRT(2) * csCT(1) * csCT(2))^0.5;
    // Yates: shrink every |O - E| by 0.5 (never below 0)
    csYat := 0;
    FOR cpI FROM 1 TO 4 DO
      csYat := csYat + MAX(0, ABS(csO(cpI) - csE(cpI)) - 0.5)^2 / csE(cpI);
    END;
    // beta 3d: csUpper (Yates always has 1 df)
    csPY := csUpper(1, csYat);
    csFish := csFisher(cpA, cpB, cpCc, cpD);
    // two proportions: column 1 within each row
    csP1 := cpA / csRT(1);
    csP2 := cpCc / csRT(2);
    cpPb := csCT(1) / csN;
    csZ := (csP1 - csP2) / (cpPb * (1 - cpPb) * (1 / csRT(1) + 1 / csRT(2)))^0.5;
    csRD := csP1 - csP2;
    // beta 3d: the 95% CI for the risk difference is NEWCOMBE's hybrid
    // score interval (Newcombe 1998, method 10; Roger's decision, fleet
    // review 19 Oct 2026).  Beta 3c used the Wald interval, RD +/- 1.96
    // SE with SE = sqrt(p1 q1 / n1 + p2 q2 / n2).  Wald takes the SE at
    // the OBSERVED proportions, and that fails at small n:
    //   - at p = 0 or 1 a row's SE is 0, so with both rows at 0 or 1
    //     the interval has width 0: [5,0,0,5] gave (1.0000, 1.0000);
    //   - it can pass -1 or +1, which a difference of two proportions
    //     never can: the tea table [3,1,1,3] gave (-0.1001, 1.1001).
    //   Of all 7,812 2 x 2 tables with row totals up to 12, Wald put
    //   an end outside [-1, 1] in 1,388 (17.8%) and had width 0 in 288.
    // Newcombe starts from each row's own WILSON interval (csWils,
    // above), which never leaves [0, 1].  The RD's lower end lies below
    // p1 - p2 by p1's distance down to its lower end and p2's distance
    // up to its upper end, combined as independent errors are (square,
    // add, square root); the upper end uses the other two distances:
    //   lower = RD - sqrt((p1 - L1)^2 + (U2 - p2)^2)
    //   upper = RD + sqrt((U1 - p1)^2 + (p2 - L2)^2)
    // It never leaves [-1, 1] and never has width 0: the tea table
    // gives (-0.1355, 0.7891), [5,0,0,5] gives (0.3855, 1.0000).
    // The counts are the observed ones (no +0.5: that is only for RR
    // and OR, below).  A row of 0 never gets here: csCheck refuses it,
    // and a bar drag that empties a row does not compute.
    cpL1 := csWils(cpA, csRT(1), cpZ, -1); cpU1 := csWils(cpA, csRT(1), cpZ, 1);
    cpL2 := csWils(cpCc, csRT(2), cpZ, -1); cpU2 := csWils(cpCc, csRT(2), cpZ, 1);
    csRDL := csRD - ((csP1 - cpL1)^2 + (cpU2 - csP2)^2)^0.5;
    csRDH := csRD + ((cpU1 - csP1)^2 + (csP2 - cpL2)^2)^0.5;
    // a zero cell makes RR or OR 0 or infinite: add 0.5 to every
    // cell (Haldane-Anscombe) and say so on the page
    csHal := 0;
    IF MIN(cpA, cpB, cpCc, cpD) == 0 THEN csHal := 1; END;
    cpHal := 0.5 * csHal;
    cpA := cpA + cpHal; cpB := cpB + cpHal;
    cpCc := cpCc + cpHal; cpD := cpD + cpHal;
    csOdds := cpA * cpD / (cpB * cpCc);
    cpSe := (1 / cpA + 1 / cpB + 1 / cpCc + 1 / cpD)^0.5;
    csORL := EXP(LN(csOdds) - cpZ * cpSe);
    csORH := EXP(LN(csOdds) + cpZ * cpSe);
    csRR := (cpA / (cpA + cpB)) / (cpCc / (cpCc + cpD));
    cpSe := (1 / cpA - 1 / (cpA + cpB) + 1 / cpCc - 1 / (cpCc + cpD))^0.5;
    csRRL := EXP(LN(csRR) - cpZ * cpSe);
    csRRH := EXP(LN(csRR) + cpZ * cpSe);
  END;

  // ---- Home exports ----
  CS_X2 := csX2; CS_DF := csDF; CS_P := csP; CS_V := csV; CS_N := csN;
  CS_OBS := MAKEMAT(0, csNR, csNC);
  CS_EXP := MAKEMAT(0, csNR, csNC);
  CS_RES := MAKEMAT(0, csNR, csNC);
  FOR cpR FROM 1 TO csNR DO
    FOR cpC FROM 1 TO csNC DO
      cpI := csIx(cpR, cpC);
      CS_OBS(cpR, cpC) := csO(cpI);
      CS_EXP(cpR, cpC) := csE(cpI);
      CS_RES(cpR, cpC) := csAdj(cpI);
    END;
  END;
  csHas := 1;
  csSel := 1;
END;

// Checks that teach, then compute.  Returns 1 if the table is
// usable, 0 if not (and says why).
csCheck()
BEGIN
  LOCAL ckI, ckBad;
  ckBad := 0;
  FOR ckI FROM 1 TO csNR * csNC DO
    IF csO(ckI) < 0 THEN ckBad := 1; END;
  END;
  IF ckBad == 1 THEN
    MSGBOX("A count cannot be negative.");
    RETURN 0;
  END;
  csTotals();
  IF csN == 0 THEN
    MSGBOX("The table is empty: type the observed counts first.");
    RETURN 0;
  END;
  FOR ckI FROM 1 TO csNR DO
    IF csRT(ckI) == 0 THEN ckBad := 1; END;
  END;
  FOR ckI FROM 1 TO csNC DO
    IF csCT(ckI) == 0 THEN ckBad := 1; END;
  END;
  IF ckBad == 1 THEN
    MSGBOX("A row or column adds to 0, so its expected counts are 0 and (O - E)" + csSqS + " / E cannot be formed. Drop that category or add data.");
    RETURN 0;
  END;
  csCompute();
  RETURN 1;
END;


// ------------------------------------------------------------
// BETA 2 HELPERS: recompute, cells, soft keys, x10 N, restore,
// the morph's colours
// ------------------------------------------------------------

// csCompute ends by selecting cell 1; the touch features keep the
// cell the student is working on.  csCompute itself is untouched.
csRecalc()
BEGIN
  LOCAL rcS;
  rcS := csSel;
  csCompute();
  csSel := rcS;
END;

// The data cell of the results grid under a tap at (caX, caY), or
// 0 for none -- the same arithmetic the entry grid's tap uses.
csCellAt(caX, caY)
BEGIN
  LOCAL caR, caC;
  IF caX < gX0 OR caY < gY0 THEN RETURN 0; END;
  caC := IP((caX - gX0) / gCW) + 1;
  caR := IP((caY - gY0) / gRH) + 1;
  IF caC >= 1 AND caC <= csNC AND caR >= 1 AND caR <= csNR THEN
    RETURN csIx(caR, caC);
  END;
  RETURN 0;
END;

// How the expected count of cell esI is made, with the real
// numbers: "R1 C1:  E = 483 x 68 / 674 = 48.73".
csEStr(esI)
BEGIN
  LOCAL esR, esC;
  esR := IP((esI - 1) / csNC) + 1;
  esC := esI - (esR - 1) * csNC;
  RETURN "R" + STRING(esR, 2, 0) + " C" + STRING(esC, 2, 0) + ":  E = " + STRING(csRT(esR), 2, 0) + " " + csTimes + " " + STRING(csCT(esC), 2, 0) + " / " + STRING(csN, 2, 0) + " = " + STRING(ROUND(csE(esI), 2), 2, 2);
END;

// The soft-key strip of each screen (drawn after the blit, every
// frame -- DRAWMENU paints the screen, not G1).  skV is the screen:
// 1 results, 2 bars, 3 mosaic, 4 curve, 5 residuals, 6 Fisher.
// On a plot the key that would jump to the plot already showing
// does that plot's own job instead: Restore on the bars, Morph on
// the mosaic, Reset (alpha = 0.05) on the curve.
// (House convention, 16 Oct 2026: a screen that needs more than six
// actions makes slot 6 "More" and pages its labels.  Every screen
// here fits in six -- the rarer actions, x10 N, N / 10 and Restore,
// sit in the Menu list -- so no "More" page is used.)
csStrip(skV)
BEGIN
  CASE
    IF skV == 2 THEN DRAWMENU({"Prev", "Next", "Restore", "Mosaic", "Curve", "Menu"}); END;
    IF skV == 3 THEN DRAWMENU({"Prev", "Next", "Bars", "Morph", "Curve", "Menu"}); END;
    IF skV == 4 THEN DRAWMENU({"Prev", "Next", "Bars", "Mosaic", "Reset", "Menu"}); END;
    DEFAULT DRAWMENU({"Prev", "Next", "Bars", "Mosaic", "Curve", "Menu"});
  END;
END;

// Prev / Next on a plot: the screen before or after psV in the
// ring results -> bars -> mosaic -> curve -> residuals -> Fisher
// (2 x 2 only) -> results.
csPlotStep(psV, psD)
BEGIN
  LOCAL psN, psX;
  psN := 5;
  IF csNR == 2 AND csNC == 2 THEN psN := 6; END;
  psX := psV + psD;
  IF psX > psN THEN psX := 1; END;
  IF psX < 1 THEN psX := psN; END;
  RETURN psX;
END;

// Restore: the counts as they were last computed from the grid or
// an example (csKeep), whatever was dragged or scaled since.
csRestore()
BEGIN
  IF SIZE(csKeep) == csNR * csNC THEN
    csO := csCopy(csKeep);
    csRecalc();
  END;
END;

// x10 N (scF = 10) or N / 10 (scF = 0.1): every count scaled, then
// chi2, p and V shown before and after.  The row and column
// percentages do not change, so neither does V -- but chi2 is
// proportional to N, so p collapses.  N / 10 is refused if any
// count would round to 0 (every count must stay 1 or more).
// Returns 1 if the table changed.
csScaleN(scF)
BEGIN
  LOCAL scI, scM, scNew, scOK, scN, scX2;
  LOCAL scP, scV, scS, scY, scCol;
  scM := csNR * csNC;
  scNew := MAKELIST(0, K, 1, scM, 1);
  scOK := 1;
  FOR scI FROM 1 TO scM DO
    scNew(scI) := ROUND(csO(scI) * scF, 0);
    IF csO(scI) > 0 AND scNew(scI) < 1 THEN scOK := 0; END;
  END;
  IF scOK == 0 THEN
    MSGBOX("Not possible: a count would round to 0 when divided by 10, and every count must stay 1 or more.");
    RETURN 0;
  END;
  IF csN * scF > 9999999 THEN
    MSGBOX("N would pass 10,000,000: the counts would no longer fit in the table's cells.");
    RETURN 0;
  END;
  // keep the old values, change the table, recompute
  scN := csN; scX2 := csX2; scP := csP; scV := csV;
  csO := scNew;
  csRecalc();
  // ---- the page: before and after, side by side ----
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 4, 4, 315, 235, csMAG, csLMAG);
  IF scF > 1 THEN scS := "Same table, ten times the data"; ELSE scS := "Same table, a tenth of the data"; END;
  TEXTOUT_P(scS, G1, 12, 8, 2, csMAG, 296);
  TEXTOUT_P("before", G1, 150, 32, 1, csGRAY, 76);
  TEXTOUT_P("after", G1, 236, 32, 1, csGRAY, 76);
  TEXTOUT_P("N", G1, 12, 48, 1, #000000h, 130);
  TEXTOUT_P(STRING(scN, 2, 0), G1, 150, 48, 1, #000000h, 80);
  TEXTOUT_P(STRING(csN, 2, 0), G1, 236, 48, 1, #000000h, 76);
  TEXTOUT_P(csX2S + " (df = " + STRING(csDF, 2, 0) + ")", G1, 12, 64, 1, csMAG, 130);
  // beta 3c: chi2 and V rounded before STRING (scientific below 10^-d)
  TEXTOUT_P(STRING(ROUND(scX2, 2), 2, 2), G1, 150, 64, 1, csMAG, 80);
  TEXTOUT_P(STRING(ROUND(csX2, 2), 2, 2), G1, 236, 64, 1, csMAG, 76);
  TEXTOUT_P("p-value", G1, 12, 80, 1, csMAG, 130);
  TEXTOUT_P(csPStr(scP), G1, 150, 80, 1, csMAG, 80);
  TEXTOUT_P(csPStr(csP), G1, 236, 80, 1, csMAG, 76);
  TEXTOUT_P("Cramer's V (effect size)", G1, 12, 96, 1, csBLUE, 130);
  TEXTOUT_P(STRING(ROUND(scV, 3), 2, 3), G1, 150, 96, 1, csBLUE, 80);
  TEXTOUT_P(STRING(ROUND(csV, 3), 2, 3), G1, 236, 96, 1, csBLUE, 76);
  // the decision before and after, in the verdict colours
  TEXTOUT_P("at " + csAl + " = " + csAStr(), G1, 12, 112, 1, #000000h, 130);
  FOR scI FROM 1 TO 2 DO
    IF scI == 1 THEN scY := scP; ELSE scY := csP; END;
    IF scY < csAlpha THEN scS := "REJECT " + csH0; scCol := csRED; ELSE scS := "keep " + csH0; scCol := csGRN; END;
    TEXTOUT_P(scS, G1, 64 + 86 * scI, 112, 1, scCol, 80);
  END;
  TEXTOUT_P("V is the effect size; p depends on N.", G1, 12, 134, 2, csBLUE, 296);
  IF scF > 1 THEN
    TEXTOUT_P("Every count times 10: the row and column %", G1, 12, 158, 1, #000000h, 296);
    TEXTOUT_P("and V stay the same, " + csX2S + " grows ten-fold and", G1, 12, 171, 1, #000000h, 296);
    TEXTOUT_P("p collapses.  A tiny effect in a huge sample", G1, 12, 184, 1, #000000h, 296);
    TEXTOUT_P("is 'significant' -- and still tiny.", G1, 12, 197, 1, #000000h, 296);
  ELSE
    TEXTOUT_P("Every count divided by 10 (rounded): the %", G1, 12, 158, 1, #000000h, 296);
    TEXTOUT_P("and V hardly move, " + csX2S + " shrinks about ten-fold", G1, 12, 171, 1, #000000h, 296);
    TEXTOUT_P("and p grows.  A small sample can miss a real", G1, 12, 184, 1, #000000h, 296);
    TEXTOUT_P("effect.  Restore (Menu) brings the data back.", G1, 12, 197, 1, #000000h, 296);
  END;
  TEXTOUT_P("Any key or tap = back", G1, 12, 222, 1, csFOOT, 296);
  BLIT_P(G0, 0, 0, G1);
  csWaitAny();
  RETURN 1;
END;

// The Menu soft key: a short list on top of whatever screen is
// showing.  Returns 0 to go to the main menu, 1 to stay (the
// screen redraws with whatever changed).
csResMenu()
BEGIN
  LOCAL rmI, rmK;
  rmI := 0;
  // beta 3b: "N / 10: counts divided by 10" shortened -- CHOOSE cuts a
  // line wider than its box (the emulator showed "...divided by" with
  // the "10" cut off).
  // beta 3c: this comment used to sit INSIDE the list below, which is
  // a Syntax Error on the Prime (round 2): never put a comment inside
  // an open bracket.
  CHOOSE(rmI, "Results menu", {
    "Main menu",
    csTimes + "10 N: every count times 10",
    "N / 10: counts divided by 10",
    "Restore the original counts"});
  IF rmI == 1 THEN RETURN 0; END;
  IF rmI == 2 THEN csScaleN(10); END;
  IF rmI == 3 THEN csScaleN(0.1); END;
  IF rmI == 4 THEN csRestore(); END;
  // the tap or key that closed the list can arrive again on the
  // screen underneath, so drain both before the next frame
  REPEAT rmK := GETKEY; UNTIL rmK == -1;
  csDrain();
  RETURN 1;
END;

// The morph's colours.  csFade gives the colour a fraction fdT of
// the way from (fdR0, fdG0, fdB0) to (fdR1, fdG1, fdB1); fdT = 0 is
// the first colour, 1 the second.
csFade(fdR0, fdG0, fdB0, fdR1, fdG1, fdB1, fdT)
BEGIN
  RETURN RGB(IP(fdR0 + fdT * (fdR1 - fdR0) + 0.5), IP(fdG0 + fdT * (fdG1 - fdG0) + 0.5), IP(fdB0 + fdT * (fdB1 - fdB0) + 0.5));
END;

// A mosaic tile's fill for adjusted residual mfA, faded a fraction
// mfT of the way to the grey of the "all" bar (240, 240, 240).  The
// start colours are the mosaic's own: csMBLU, csLBLU, csMRED, csLRED.
csMFill(mfA, mfT)
BEGIN
  IF mfA >= 4 THEN RETURN csFade(158, 201, 240, 240, 240, 240, mfT); END;
  IF mfA >= 2 THEN RETURN csFade(235, 242, 255, 240, 240, 240, mfT); END;
  IF mfA <= -4 THEN RETURN csFade(245, 160, 160, 240, 240, 240, mfT); END;
  IF mfA <= -2 THEN RETURN csFade(255, 235, 235, 240, 240, 240, mfT); END;
  RETURN csLGRY;
END;

// ... and its border, from blue or red to the grey (90, 90, 90).
csMEdge(meA, meT)
BEGIN
  IF meA >= 2 THEN RETURN csFade(0, 60, 200, 90, 90, 90, meT); END;
  IF meA <= -2 THEN RETURN csFade(255, 0, 0, 90, 90, 90, meT); END;
  RETURN csGRAY;
END;


// ------------------------------------------------------------
// THE GRID: one drawing routine, four modes
//   1 entry (big counts, the selected cell framed in red)
//   2 observed and expected      3 row %, column %, % of N
//   4 contribution to X2 and adjusted residual
// The touch handler in csEntry uses the same gX0/gCW/gY0/gRH.
// ------------------------------------------------------------
csGrid(gmMode)
BEGIN
  LOCAL gmR, gmC, gmX, gmY, gmI, gmS;
  LOCAL gmXT, gmYT, gmCol, gmV, gmFr, gmFill;
  gmXT := gX0 + csNC * gCW;          // totals column, left edge
  gmYT := gY0 + csNR * gRH;          // totals row, top edge
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  CASE
    IF gmMode == 1 THEN TEXTOUT_P("Observed counts: " + STRING(csNR, 2, 0) + " " + csTimes + " " + STRING(csNC, 2, 0) + " table", G1, 6, 6, 2, csBLUE, 308); END;
    // beta 2: a three-row table has no room under it, so the
    // explanation of a tapped cell takes the title's place
    IF gmMode == 2 AND csTapC > 0 AND csNR == 3 THEN
      TEXTOUT_P(csEStr(csTapC), G1, 6, 3, 1, csMAG, 308);
      TEXTOUT_P("what it would hold if the variables were unrelated", G1, 6, 15, 1, csGRAY, 308);
    END;
    IF gmMode == 2 THEN TEXTOUT_P("Observed (black) over expected (blue)", G1, 6, 6, 2, csBLUE, 308); END;
    IF gmMode == 3 THEN TEXTOUT_P("Row %, column % and % of N, per cell", G1, 6, 6, 2, csBLUE, 308); END;
    DEFAULT TEXTOUT_P("Contribution to " + csX2S + " (top), adj. residual", G1, 6, 6, 2, csBLUE, 308);
  END;
  // ---- header row: column names ----
  FOR gmC FROM 1 TO csNC DO
    gmX := gX0 + (gmC - 1) * gCW;
    RECT_P(G1, gmX, gHY, gmX + gCW, gHY + gHH, csGRAY, csLGRY);
    TEXTOUT_P("Col " + STRING(gmC, 2, 0), G1, gmX + 15, gHY + 5, 1, csColC(gmC), 60);
  END;
  RECT_P(G1, gmXT, gHY, gmXT + 76, gHY + gHH, csORNG, csLORN);
  TEXTOUT_P("Total", G1, gmXT + 15, gHY + 5, 1, csORNG, 72);
  // ---- the data rows ----
  FOR gmR FROM 1 TO csNR DO
    gmY := gY0 + (gmR - 1) * gRH;
    RECT_P(G1, gLX, gmY, gX0 - 2, gmY + gRH, csGRAY, csLGRY);
    TEXTOUT_P("Row " + STRING(gmR, 2, 0), G1, gLX + 5, gmY + 17, 1, #000000h, 40);
    FOR gmC FROM 1 TO csNC DO
      gmX := gX0 + (gmC - 1) * gCW;
      gmI := csIx(gmR, gmC);
      // the frame: red for the selected cell while typing, and
      // red on the O/E page for an expected count below 5
      gmFr := csGRAY; gmFill := #FFFFFFh;
      IF gmMode == 1 AND gmI == csSel THEN gmFr := csRED; gmFill := csLRED; END;
      IF gmMode == 2 AND csE(gmI) < 5 THEN gmFr := csRED; gmFill := csLRED; END;
      RECT_P(G1, gmX, gmY, gmX + gCW, gmY + gRH, gmFr, gmFill);
      CASE
        IF gmMode == 1 THEN
          gmS := STRING(csO(gmI), 2, 0);
          TEXTOUT_P(gmS, G1, gmX + 32 - IP(5 * DIM(gmS)), gmY + 14, 3, #000000h, 62);
        END;
        // beta 3c: every decimal here is rounded before STRING (fixed
        // decimals turn scientific below 10^-d: an E of 0.005, a
        // contribution of 0.001, 1 of 2,000 as 0.05%)
        IF gmMode == 2 THEN
          TEXTOUT_P(STRING(csO(gmI), 2, 0), G1, gmX + 5, gmY + 6, 2, #000000h, 60);
          TEXTOUT_P(STRING(ROUND(csE(gmI), 2), 2, 2), G1, gmX + 5, gmY + 26, 1, csBLUE, 60);
        END;
        IF gmMode == 3 THEN
          TEXTOUT_P("row " + STRING(ROUND(100 * csO(gmI) / csRT(gmR), 1), 2, 1) + "%", G1, gmX + 5, gmY + 4, 1, csBLUE, 60);
          TEXTOUT_P("col " + STRING(ROUND(100 * csO(gmI) / csCT(gmC), 1), 2, 1) + "%", G1, gmX + 5, gmY + 16, 1, csMAG, 60);
          TEXTOUT_P("all " + STRING(ROUND(100 * csO(gmI) / csN, 1), 2, 1) + "%", G1, gmX + 5, gmY + 28, 1, csGRAY, 60);
        END;
        DEFAULT
          TEXTOUT_P(STRING(ROUND(csCon(gmI), 2), 2, 2), G1, gmX + 5, gmY + 6, 2, #000000h, 60);
          gmCol := csGRAY;
          IF csAdj(gmI) >= 2 THEN gmCol := csBLUE; END;
          IF csAdj(gmI) <= -2 THEN gmCol := csRED; END;
          TEXTOUT_P("adj " + csSgn(csAdj(gmI), 2), G1, gmX + 5, gmY + 25, 1, gmCol, 60);
      END;
    END;
    // the row total cell
    RECT_P(G1, gmXT, gmY, gmXT + 76, gmY + gRH, csORNG, csLORN);
    CASE
      IF gmMode == 3 THEN
        TEXTOUT_P(STRING(csRT(gmR), 2, 0), G1, gmXT + 5, gmY + 7, 1, csORNG, 72);
        TEXTOUT_P(STRING(ROUND(100 * csRT(gmR) / csN, 1), 2, 1) + "% of N", G1, gmXT + 5, gmY + 25, 1, csORNG, 72);
      END;
      IF gmMode == 4 THEN
        gmV := 0;
        FOR gmC FROM 1 TO csNC DO gmV := gmV + csCon(csIx(gmR, gmC)); END;
        TEXTOUT_P(STRING(ROUND(gmV, 3), 2, 3), G1, gmXT + 5, gmY + 17, 1, csORNG, 72);
      END;
      DEFAULT
        TEXTOUT_P(STRING(csRT(gmR), 2, 0), G1, gmXT + 5, gmY + 15, 2, csORNG, 72);
    END;
  END;
  // ---- the totals row ----
  RECT_P(G1, gLX, gmYT, gX0 - 2, gmYT + gTH, csORNG, csLORN);
  TEXTOUT_P("Total", G1, gLX + 5, gmYT + 11, 1, csORNG, 40);
  FOR gmC FROM 1 TO csNC DO
    gmX := gX0 + (gmC - 1) * gCW;
    RECT_P(G1, gmX, gmYT, gmX + gCW, gmYT + gTH, csORNG, csLORN);
    CASE
      IF gmMode == 3 THEN
        TEXTOUT_P(STRING(csCT(gmC), 2, 0), G1, gmX + 5, gmYT + 4, 1, csORNG, 60);
        TEXTOUT_P(STRING(ROUND(100 * csCT(gmC) / csN, 1), 2, 1) + "%", G1, gmX + 5, gmYT + 18, 1, csORNG, 60);
      END;
      IF gmMode == 4 THEN
        gmV := 0;
        FOR gmR FROM 1 TO csNR DO gmV := gmV + csCon(csIx(gmR, gmC)); END;
        TEXTOUT_P(STRING(ROUND(gmV, 3), 2, 3), G1, gmX + 5, gmYT + 11, 1, csORNG, 60);
      END;
      DEFAULT
        TEXTOUT_P(STRING(csCT(gmC), 2, 0), G1, gmX + 5, gmYT + 9, 2, csORNG, 60);
    END;
  END;
  // the grand total (or, on page 4, X2 itself: the sum of the cells)
  RECT_P(G1, gmXT, gmYT, gmXT + 76, gmYT + gTH, csORNG, csORNG);
  IF gmMode == 4 THEN
    TEXTOUT_P(csX2S + " = " + STRING(ROUND(csX2, 2), 2, 2), G1, gmXT + 4, gmYT + 11, 1, #FFFFFFh, 72);
  ELSE
    TEXTOUT_P("N = " + STRING(csN, 2, 0), G1, gmXT + 4, gmYT + 9, 2, #FFFFFFh, 72);
  END;
  // ---- beta 2: the tapped cell on page 1 gets a magenta frame ----
  IF gmMode == 2 AND csTapC > 0 THEN
    gmR := IP((csTapC - 1) / csNC) + 1;
    gmC := csTapC - (gmR - 1) * csNC;
    gmX := gX0 + (gmC - 1) * gCW;
    gmY := gY0 + (gmR - 1) * gRH;
    LINE_P(G1, gmX + 2, gmY + 2, gmX + gCW - 2, gmY + 2, csMAG);
    LINE_P(G1, gmX + 2, gmY + gRH - 2, gmX + gCW - 2, gmY + gRH - 2, csMAG);
    LINE_P(G1, gmX + 2, gmY + 2, gmX + 2, gmY + gRH - 2, csMAG);
    LINE_P(G1, gmX + gCW - 2, gmY + 2, gmX + gCW - 2, gmY + gRH - 2, csMAG);
  END;
  // ---- notes under the table (room only with two rows) ----
  IF csNR == 2 THEN
    CASE
      IF gmMode == 1 THEN TEXTOUT_P("Tap a cell to type its count.  Run: the test.", G1, 6, 184, 1, csGRAY, 308); END;
      // beta 2: a tapped cell's E, with its real numbers, in place
      // of the note and the example's label
      IF gmMode == 2 AND csTapC > 0 THEN
        TEXTOUT_P(csEStr(csTapC), G1, 6, 184, 1, csMAG, 308);
        TEXTOUT_P("what it would hold if the variables were unrelated", G1, 6, 200, 1, csGRAY, 308);
      END;
      IF gmMode == 2 THEN TEXTOUT_P("Tap a cell: how its E is made.  Red frame: E < 5.", G1, 6, 184, 1, csGRAY, 308); END;
      IF gmMode == 3 THEN TEXTOUT_P("Row % compares groups; col % profiles an outcome.", G1, 6, 184, 1, csGRAY, 308); END;
      DEFAULT TEXTOUT_P("Top: (O-E)" + csSqS + "/E, summing to " + csX2S + ".  |adj| >= 2: notable.", G1, 6, 184, 1, csGRAY, 308);
    END;
    IF DIM(csLbl2) > 0 AND (gmMode <> 2 OR csTapC == 0) THEN TEXTOUT_P(csLbl2, G1, 6, 200, 1, csGRN, 308); END;
  END;
  // beta 2: the soft keys take the bottom strip, so the results
  // pages' footer is just the page number, in the empty top-left
  // corner of the table (the entry grid leaves csFoot empty)
  IF DIM(csFoot) > 0 THEN TEXTOUT_P(csFoot, G1, gLX + 6, gHY + 5, 1, csGRAY, 36); END;
  BLIT_P(G0, 0, 0, G1);
END;


// ------------------------------------------------------------
// TYPING THE TABLE
// ------------------------------------------------------------

// Every count in one INPUT form (the Values soft key, or +): for
// anyone who would rather type than tap.  INPUT needs variable
// NAMES, so the values pass through the locals a1..a6.  Returns 1
// if the form was accepted, 0 if it was cancelled.
csAskAll()
BEGIN
  LOCAL a1, a2, a3, a4, a5, a6;
  LOCAL akOK, akI, akL, akH, akBig;
  a1 := csO(1); a2 := csO(2); a3 := csO(3); a4 := csO(4);
  a5 := 0; a6 := 0;
  IF (csNR * csNC) == 6 THEN a5 := csO(5); a6 := csO(6); END;
  akH := "Observed count, a whole number >= 0";
  akOK := 0;
  CASE
    IF csNR == 2 AND csNC == 2 THEN
      akOK := INPUT({a1, a2, a3, a4}, "All four counts",
        {"Row 1 Col 1:", "Row 1 Col 2:", "Row 2 Col 1:", "Row 2 Col 2:"},
        {akH, akH, akH, akH});
    END;
    IF csNR == 2 AND csNC == 3 THEN
      akOK := INPUT({a1, a2, a3, a4, a5, a6}, "All six counts",
        {"Row 1 Col 1:", "Row 1 Col 2:", "Row 1 Col 3:", "Row 2 Col 1:", "Row 2 Col 2:", "Row 2 Col 3:"},
        {akH, akH, akH, akH, akH, akH});
    END;
    DEFAULT
      akOK := INPUT({a1, a2, a3, a4, a5, a6}, "All six counts",
        {"Row 1 Col 1:", "Row 1 Col 2:", "Row 2 Col 1:", "Row 2 Col 2:", "Row 3 Col 1:", "Row 3 Col 2:"},
        {akH, akH, akH, akH, akH, akH});
  END;
  IF akOK == 0 THEN RETURN 0; END;
  akL := {a1, a2, a3, a4, a5, a6};
  akBig := 0;
  FOR akI FROM 1 TO csNR * csNC DO
    // beta 3d: a count above 9,999,999 is not taken (that cell keeps
    // its old count, and one message says why after the loop): the
    // table's cells and the exact page cannot show more digits, and
    // x10 N already stops N at 9,999,999.  Every other count is taken
    // as typed, rounded to a whole number.  A NEGATIVE count is no
    // longer turned into 0 without a word (beta 3c's MAX(0, ...)): it
    // stays in the grid, and Run refuses it with csCheck's "A count
    // cannot be negative.", as the cell editor and a bar drag do.
    IF ROUND(akL(akI), 0) > 9999999 THEN
      akBig := 1;
    ELSE
      csO(akI) := ROUND(akL(akI), 0);
    END;
  END;
  IF akBig == 1 THEN
    MSGBOX("Counts above 9,999,999 do not fit the table: enter the data divided by 10.");
  END;
  RETURN 1;
END;

// The touch grid.  Tap a cell, or move the red frame with the
// arrows, then Edit (or Enter) to type that cell's count.  The soft
// keys along the bottom are Edit, Values, Clear, Cancel and Run;
// Run runs the checks and computes.  Returns 1 if the table
// computed, 0 if the user cancelled.
csEntry()
BEGIN
  LOCAL enK, enM, enM1, enGo, enR, enC;
  LOCAL enV, enOK, enAct, enLbl, enDirty, enRet;
  enGo := 1; enDirty := 0; enRet := 0;
  IF csSel < 1 OR csSel > csNR * csNC THEN csSel := 1; END;
  csFoot := "";                              // the soft keys take the footer's place
  REPEAT enK := GETKEY; UNTIL enK == -1;
  csDrain();
  WHILE enGo == 1 DO
    csTotals();
    csGrid(1);
    // the soft keys, painted on the screen after the blit
    DRAWMENU({"Edit", "Values", "Clear", "", "Cancel", "Run"});
    REPEAT
      enK := GETKEY;
      enM := MOUSE;
      enM1 := enM(1);
    UNTIL enK > -1 OR SIZE(enM1) > 0;
    // enAct: 1 edit the selected cell, 2 the Values form, 3 clear,
    // 5 cancel, 6 run; 0 nothing
    enAct := 0;
    enR := IP((csSel - 1) / csNC) + 1;
    enC := csSel - (enR - 1) * csNC;
    IF SIZE(enM1) > 0 THEN
      csDrain();              // wait for the finger to lift
      IF enM1(2) >= 218 THEN
        // the soft-key strip: six 53-px slots, left to right
        enAct := MIN(6, IP(enM1(1) / 53.4) + 1);
        IF enAct == 4 THEN enAct := 0; END;     // the blank key
      ELSE
        IF enM1(1) >= gX0 AND enM1(2) >= gY0 THEN
          enC := IP((enM1(1) - gX0) / gCW) + 1;
          enR := IP((enM1(2) - gY0) / gRH) + 1;
          IF enC >= 1 AND enC <= csNC AND enR >= 1 AND enR <= csNR THEN
            csSel := csIx(enR, enC);
            enAct := 1;
          END;
        END;
      END;
    ELSE
      // the keys remain as shortcuts for the soft keys
      IF enK == 7 THEN enC := MAX(1, enC - 1); END;
      IF enK == 8 THEN enC := MIN(csNC, enC + 1); END;
      IF enK == 2 THEN enR := MAX(1, enR - 1); END;
      IF enK == 12 THEN enR := MIN(csNR, enR + 1); END;
      csSel := csIx(enR, enC);
      IF enK == 30 THEN enAct := 1; END;        // Enter = Edit
      IF enK == 50 THEN enAct := 2; END;        // +     = Values
      IF enK == 4 THEN enAct := 5; END;         // Esc   = Cancel
    END;
    // ---- Edit: one cell ----
    IF enAct == 1 THEN
      enR := IP((csSel - 1) / csNC) + 1;
      enC := csSel - (enR - 1) * csNC;
      enV := csO(csSel);
      enLbl := "Row " + STRING(enR, 2, 0) + ", Col " + STRING(enC, 2, 0) + ":";
      enOK := INPUT({enV}, "Observed count", {enLbl}, {"A whole number, 0 or more"});
      IF enOK == 1 THEN
        IF enV < 0 THEN
          MSGBOX("A count cannot be negative.");
        ELSE
          // beta 3d: a count above 9,999,999 is refused, and the cell
          // keeps its old count (a typed 1E9 ran out of its cell and off
          // the exact page; x10 N already stops N at 9,999,999)
          IF ROUND(enV, 0) > 9999999 THEN
            MSGBOX("Counts above 9,999,999 do not fit the table: enter the data divided by 10.");
          ELSE
            // beta 3d: the typed value is rounded to 2 decimals BEFORE
            // STRING.  STRING(x, 2, 2) prints scientific form when
            // 0 < |x| < 0.01, so a typed 0.004 read "4.00E-3 was rounded
            // to 0" (small-capital E); now "0.00 was rounded to 0".
            IF FP(enV) <> 0 THEN
              MSGBOX("Counts are whole numbers: " + STRING(ROUND(enV, 2), 2, 2) + " was rounded to " + STRING(ROUND(enV, 0), 2, 0) + ".");
            END;
            csO(csSel) := ROUND(enV, 0);
            enDirty := 1;
          END;
        END;
      END;
    END;
    // ---- Values: every count in one form ----
    IF enAct == 2 THEN
      IF csAskAll() == 1 THEN enDirty := 1; END;
    END;
    // ---- Clear: back to an empty table ----
    IF enAct == 3 THEN
      csO := MAKELIST(0, K, 1, csNR * csNC, 1);
      enDirty := 1;
    END;
    // ---- Cancel: keep the counts, leave without computing ----
    IF enAct == 5 THEN
      IF enDirty == 1 THEN csHas := 0; END;   // any old results are stale now
      enGo := 0; enRet := 0;
    END;
    // ---- Run: check, and compute if the table passes ----
    IF enAct == 6 THEN
      IF csCheck() == 1 THEN enGo := 0; enRet := 1; END;
    END;
    // the tap that closed a dialog can arrive again on the screen
    // underneath, so drain both before the next frame
    REPEAT enK := GETKEY; UNTIL enK == -1;
    csDrain();
  END;
  IF enRet == 1 THEN
    csLbl := "Your data: " + STRING(csNR, 2, 0) + " " + csTimes + " " + STRING(csNC, 2, 0) + " table";
    csLbl2 := "";
    csKeep := csCopy(csO);                  // beta 2: what Restore puts back
  END;
  RETURN enRet;
END;

// A fresh table of the chosen shape, then the grid.
csNewTable(ntR, ntC)
BEGIN
  csNR := ntR; csNC := ntC;
  csO := MAKELIST(0, K, 1, csNR * csNC, 1);
  csSel := 1; csHas := 0;
  csLbl := ""; csLbl2 := "";
  RETURN csEntry();
END;


// ------------------------------------------------------------
// RESULT PAGES (text)
// ------------------------------------------------------------

// Page 3: the test itself
csPageTest()
BEGIN
  LOCAL ptS;
  // beta 3c: chi2, the critical value, V, C, G2 and the smallest E are
  // rounded before STRING (fixed decimals turn scientific below 10^-d)
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 4, 4, 315, 235, csORNG, csLORN);
  TEXTOUT_P(csX2S + " test of independence", G1, 12, 8, 2, csBLUE, 296);
  TEXTOUT_P(STRING(csNR, 2, 0) + " " + csTimes + " " + STRING(csNC, 2, 0) + " table, N = " + STRING(csN, 2, 0) + ", df = (" + STRING(csNR, 2, 0) + "-1)(" + STRING(csNC, 2, 0) + "-1) = " + STRING(csDF, 2, 0), G1, 12, 30, 1, #000000h, 296);
  TEXTOUT_P(csH0 + ": row and column variables are independent", G1, 12, 46, 1, #000000h, 296);
  TEXTOUT_P(csH1 + ": they are associated (not independent)", G1, 12, 59, 1, #000000h, 296);
  TEXTOUT_P(csX2S + " = sum of (O - E)" + csSqS + " / E = " + STRING(ROUND(csX2, 3), 2, 3), G1, 12, 78, 1, csMAG, 296);
  TEXTOUT_P("p-value " + csPStr(csP) + "   (right tail, df = " + STRING(csDF, 2, 0) + ")", G1, 12, 91, 1, csMAG, 296);
  TEXTOUT_P("critical value at " + csAl + " = " + csAStr() + ": " + STRING(ROUND(csCrit, 3), 2, 3), G1, 12, 104, 1, #000000h, 296);
  IF csP < csAlpha THEN
    TEXTOUT_P("Decision: " + csX2S + " > critical, p < " + csAl, G1, 12, 117, 1, #000000h, 296);
    TEXTOUT_P("REJECT " + csH0 + " because the data show evidence of", G1, 12, 130, 1, csRED, 302);
    TEXTOUT_P("an association between the variables", G1, 12, 143, 1, csRED, 302);
  ELSE
    TEXTOUT_P("Decision: " + csX2S + " < critical, p >= " + csAl, G1, 12, 117, 1, #000000h, 296);
    TEXTOUT_P("DO NOT REJECT " + csH0 + " because the data do not", G1, 12, 130, 1, csGRN, 302);
    TEXTOUT_P("show an association between the variables", G1, 12, 143, 1, csGRN, 302);
  END;
  TEXTOUT_P("Effect size: Cramer's V = " + STRING(ROUND(csV, 3), 2, 3) + " (" + csVWord(csV) + ")", G1, 12, 155, 1, csBLUE, 296);
  TEXTOUT_P("(V = phi = Cohen's w here, since min(r,c) = 2)", G1, 12, 168, 1, csGRAY, 296);
  TEXTOUT_P("Contingency coefficient C = " + STRING(ROUND(csCC, 3), 2, 3), G1, 12, 181, 1, csBLUE, 296);
  TEXTOUT_P("Likelihood-ratio G" + csSqS + " = " + STRING(ROUND(csG2, 3), 2, 3) + ",  " + csPStr(csPG), G1, 12, 194, 1, csGRAY, 296);
  ptS := "Min expected " + STRING(ROUND(csMinE, 2), 2, 2) + "; cells with E < 5: " + STRING(csN5, 2, 0) + " of " + STRING(csNR * csNC, 2, 0);
  IF csN5 > 0 THEN
    TEXTOUT_P(ptS, G1, 12, 207, 1, csRED, 296);
  ELSE
    TEXTOUT_P(ptS, G1, 12, 207, 1, csGRN, 296);
  END;
  // beta 2: the page number, top right (the soft keys take the bottom)
  TEXTOUT_P(csFoot, G1, 282, 11, 1, csGRAY, 30);
  BLIT_P(G0, 0, 0, G1);
END;

// Page 5: what it means, in words
csPageInterp()
BEGIN
  LOCAL piR, piC, piS;
  piR := IP((csBig - 1) / csNC) + 1;
  piC := csBig - (piR - 1) * csNC;
  // beta 3c: chi2, V and E rounded before STRING (scientific below
  // 10^-d); csNoZero and csSgn round inside
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 4, 4, 315, 235, csGRN, csLGRN);
  TEXTOUT_P("What it means", G1, 12, 8, 2, csGRN, 296);
  TEXTOUT_P(csLbl, G1, 12, 28, 1, csGRAY, 296);
  // the APA-style line
  TEXTOUT_P(csX2S + "(" + STRING(csDF, 2, 0) + ", N = " + STRING(csN, 2, 0) + ") = " + STRING(ROUND(csX2, 2), 2, 2) + ", " + csPApa(csP) + ", V = " + csNoZero(csV, 2), G1, 12, 44, 1, csBLUE, 296);
  IF csP < csAlpha THEN
    TEXTOUT_P("p < " + csAl + " (" + csAStr() + "): REJECT " + csH0 + " and ACCEPT " + csH1 + ".", G1, 12, 62, 1, csRED, 296);
    TEXTOUT_P("The data show an association between the", G1, 12, 75, 1, #000000h, 296);
    TEXTOUT_P("row and column variables.", G1, 12, 88, 1, #000000h, 296);
  ELSE
    TEXTOUT_P("p >= " + csAl + " (" + csAStr() + "): DO NOT REJECT " + csH0 + "; " + csH1 + " not accepted.", G1, 12, 62, 1, csGRN, 296);
    TEXTOUT_P("The data do not show an association between", G1, 12, 75, 1, #000000h, 296);
    TEXTOUT_P("the row and column variables (at this n).", G1, 12, 88, 1, #000000h, 296);
  END;
  TEXTOUT_P("Strength: V = " + STRING(ROUND(csV, 2), 2, 2) + ", a " + csVWord(csV) + " effect (Cohen).", G1, 12, 106, 1, csBLUE, 296);
  TEXTOUT_P("Largest surprise: Row " + STRING(piR, 2, 0) + ", Col " + STRING(piC, 2, 0) + ":", G1, 12, 124, 1, #000000h, 296);
  IF csO(csBig) >= csE(csBig) THEN
    piS := "O = " + STRING(csO(csBig), 2, 0) + ", E = " + STRING(ROUND(csE(csBig), 2), 2, 2) + ": MORE than expected";
  ELSE
    piS := "O = " + STRING(csO(csBig), 2, 0) + ", E = " + STRING(ROUND(csE(csBig), 2), 2, 2) + ": FEWER than expected";
  END;
  TEXTOUT_P(piS, G1, 12, 137, 1, csMAG, 296);
  TEXTOUT_P("adj. residual = " + csSgn(csAdj(csBig), 2) + "  (|2| or more is notable)", G1, 12, 150, 1, csMAG, 296);
  IF csN5 == 0 THEN
    TEXTOUT_P("All expected counts >= 5: chi-square is fine.", G1, 12, 168, 1, csGRN, 296);
    TEXTOUT_P("Association is not causation: check the design.", G1, 12, 181, 1, csGRAY, 296);
  ELSE
    TEXTOUT_P(STRING(csN5, 2, 0) + " of " + STRING(csNR * csNC, 2, 0) + " expected counts are below 5:", G1, 12, 168, 1, csRED, 296);
    TEXTOUT_P("chi-square is shaky; Fisher (2x2) or merge cells.", G1, 12, 181, 1, csRED, 296);
  END;
  IF csNR == 2 AND csNC == 2 THEN
    TEXTOUT_P("Next page: Yates, Fisher, z, RD, RR and OR.", G1, 12, 199, 1, csGRAY, 296);
  ELSE
    TEXTOUT_P("Which cells drive " + csX2S + "?  See page 4 and the mosaic.", G1, 12, 199, 1, csGRAY, 296);
  END;
  // beta 2: the page number, top right (the soft keys take the bottom)
  TEXTOUT_P(csFoot, G1, 282, 11, 1, csGRAY, 30);
  BLIT_P(G0, 0, 0, G1);
END;

// Page 6 (2 x 2 only): the classical extras
// beta 3: Fisher's p takes two lines -- the label, then the p as an
// exact fraction from the CAS when there is one ("p = 17/35 =
// 0.4857"), with its badge -- so the lines under it move down 4 to
// 7 px (the last one still ends above the soft keys at 218).  The
// CAS is asked only when the table is new to the cache (csXEnsure).
csPage2x2()
BEGIN
  csXEnsure(0);                              // beta 3: the Fisher fraction, if not cached
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 4, 4, 315, 235, csMAG, csLMAG);
  TEXTOUT_P("More for a 2 " + csTimes + " 2 table", G1, 12, 8, 2, csMAG, 296);
  TEXTOUT_P("Convention: rows = groups, column 1 = outcome", G1, 12, 30, 1, csGRAY, 296);
  // beta 3c: every decimal on this page is rounded before STRING (a
  // p1 of 1/20,000, a z of 0.0004, an RR bound under 0.001 would have
  // printed in scientific form); csSgn rounds inside
  TEXTOUT_P("p1 = " + STRING(csO(1), 2, 0) + "/" + STRING(csRT(1), 2, 0) + " = " + STRING(ROUND(csP1, 4), 2, 4) + "   p2 = " + STRING(csO(3), 2, 0) + "/" + STRING(csRT(2), 2, 0) + " = " + STRING(ROUND(csP2, 4), 2, 4), G1, 12, 46, 1, #000000h, 296);
  TEXTOUT_P("z, two proportions = " + STRING(ROUND(csZ, 3), 2, 3) + ";  z" + csSqS + " = " + csX2S + " = " + STRING(ROUND(csX2, 2), 2, 2), G1, 12, 59, 1, #000000h, 296);
  TEXTOUT_P("Yates corrected " + csX2S + " = " + STRING(ROUND(csYat, 3), 2, 3) + ",  " + csPStr(csPY), G1, 12, 72, 1, csBLUE, 296);
  IF csFish >= 0 THEN
    // beta 3: label, then the p -- exact if the CAS gave a fraction
    // that fits (badge "exact (CAS)"), csFisher's decimal if not
    TEXTOUT_P("Fisher's exact test, two-sided:", G1, 12, 85, 1, csBLUE, 296);
    TEXTOUT_P(csXPText(33), G1, 12, 98, 1, csBLUE, 222);
    csBadge(236, 98, csXFSt == 1);
  ELSE
    TEXTOUT_P("Fisher's exact test: table too large here", G1, 12, 85, 1, csGRAY, 296);
  END;
  // beta 3: the rest moved down (104 117 130 143 162 180 193 became
  // 111 124 137 150 167 184 197)
  TEXTOUT_P("Risk diff. p1 - p2 = " + csSgn(csRD, 4) + " (" + STRING(ROUND(csRDL, 4), 2, 4) + ", " + STRING(ROUND(csRDH, 4), 2, 4) + ")", G1, 12, 111, 1, csMAG, 296);
  TEXTOUT_P("Relative risk p1/p2 = " + STRING(ROUND(csRR, 3), 2, 3) + " (" + STRING(ROUND(csRRL, 3), 2, 3) + ", " + STRING(ROUND(csRRH, 3), 2, 3) + ")", G1, 12, 124, 1, csMAG, 296);
  TEXTOUT_P("Odds ratio ad/bc = " + STRING(ROUND(csOdds, 3), 2, 3) + " (" + STRING(ROUND(csORL, 3), 2, 3) + ", " + STRING(ROUND(csORH, 3), 2, 3) + ")", G1, 12, 137, 1, csMAG, 296);
  // beta 3d: the RD interval is Newcombe's now (csCompute), so the
  // note names it.  "(95% CIs; Newcombe for RD, log method for RR and
  // OR)" is 52 characters, more than this page's lines (the longest
  // shown on the emulator was 48), so "method" goes.  With a zero cell
  // the note says where the +0.5 goes: only into RR and OR -- the RD
  // and its interval use the observed counts (beta 3c's "+0.5 added to
  // every cell" read as if it applied to RD too).
  IF csHal == 1 THEN
    TEXTOUT_P("(95% CIs; zero cell: +0.5 per cell for RR, OR)", G1, 12, 150, 1, csRED, 296);
  ELSE
    TEXTOUT_P("(95% CIs; Newcombe for RD, log for RR and OR)", G1, 12, 150, 1, csGRAY, 296);
  END;
  TEXTOUT_P("phi = " + csSgn(csPhi, 3) + "   (positive when a*d > b*c)", G1, 12, 167, 1, csBLUE, 296);
  TEXTOUT_P("Swapping rows and columns leaves " + csX2S + " and p alone;", G1, 12, 184, 1, csGRAY, 296);
  TEXTOUT_P("RD, RR and OR describe column 1 within each row.", G1, 12, 197, 1, csGRAY, 296);
  // beta 2: the page number, top right (the soft keys take the bottom)
  TEXTOUT_P(csFoot, G1, 282, 11, 1, csGRAY, 30);
  BLIT_P(G0, 0, 0, G1);
END;

// beta 3, the LAST results page: where the numbers come from, worked
// exactly by the CAS on the student's own counts.
//   2 x 2: chi2 = N(ad - bc)^2 / (r1 r2 c1 c2) -- the formula, the
//   counts put in, the whole numbers, ONE fraction, and the decimal
//   csCompute found; then Fisher's p as counting: the chance of top-
//   left count a is C(r1, a) C(r2, c1 - a) / C(N, c1), and the tables
//   no likelier than yours add up to "34 of the 70 equally likely
//   ways, p = 34/70 = 17/35 = 0.4857".
//   2 x 3 or 3 x 2: chi2 = sum (O - E)^2 / E with E = R x C / N as one
//   fraction (every E is itself a fraction, so chi2 is one too).
// Every line keeps the program's own decimal; a badge on each part
// says whether the CAS produced it.  The CAS is asked once per table
// (csXEnsure): turning pages back and forth asks it nothing new.
csPageExact()
BEGIN
  LOCAL pxS, pxA, pxB, pxC, pxD, pxQ;
  LOCAL pxR, pxN, pxC1;
  csXEnsure(1);
  pxQ := " " + csTimes + " ";
  pxN := STRING(csN, 2, 0); pxC1 := STRING(csCT(1), 2, 0);
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 4, 4, 315, 235, csBLUE, csLBLU);
  TEXTOUT_P("Where the numbers come from", G1, 12, 8, 2, csBLUE, 264);
  IF csNR == 2 AND csNC == 2 THEN
    pxA := STRING(csO(1), 2, 0); pxB := STRING(csO(2), 2, 0);
    pxC := STRING(csO(3), 2, 0); pxD := STRING(csO(4), 2, 0);
    // ---- 1. chi2 by the 2 x 2 shortcut: no E's needed ----
    TEXTOUT_P("1. " + csX2S + ", the 2" + pxQ + "2 shortcut:", G1, 12, 30, 1, #000000h, 222);
    csBadge(236, 30, DIM(csXX2) > 0);
    TEXTOUT_P(csX2S + " = N(ad - bc)" + csSqS + " / (r" + CHAR(8321) + " r" + CHAR(8322) + " c" + CHAR(8321) + " c" + CHAR(8322) + ")", G1, 12, 43, 1, csBLUE, 296);
    // the student's own counts, put in
    // beta 3d: built first and measured.  With big counts these two
    // lines ran past the page's right edge (1E9 counts: about 70
    // characters, clipped at 296 px), so when either is longer than a
    // line (46 characters, the same test as the lines below) the page
    // says so in words and the second line is left out; the CAS's whole
    // numbers and the fraction follow on the next lines either way.
    // (pxS and pxR are reused further down for other lines.)
    pxS := "  = " + pxN + pxQ + "(" + pxA + pxQ + pxD + " - " + pxB + pxQ + pxC + ")" + csSqS;
    pxR := "      / (" + STRING(csRT(1), 2, 0) + pxQ + STRING(csRT(2), 2, 0) + pxQ + pxC1 + pxQ + STRING(csCT(2), 2, 0) + ")";
    IF DIM(pxS) > 46 OR DIM(pxR) > 46 THEN
      pxS := "  (your counts are too long to write out here)";
      pxR := "";
    END;
    TEXTOUT_P(pxS, G1, 12, 56, 1, csBLUE, 296);
    IF DIM(pxR) > 0 THEN TEXTOUT_P(pxR, G1, 12, 69, 1, csBLUE, 296); END;
    // the whole numbers (CAS, any size): ad - bc and the product
    IF DIM(csXAd) > 0 AND DIM(csXPr) > 0 THEN
      pxS := "  = " + pxN + pxQ + csXAd + csSqS + " / " + csXPr;
      IF DIM(pxS) > 46 THEN pxS := "  where ad - bc = " + csXAd; END;
      TEXTOUT_P(pxS, G1, 12, 82, 1, csMAG, 296);
    END;
    // ONE fraction, then the decimal csCompute found
    TEXTOUT_P("  " + csXX2Txt(44), G1, 12, 95, 1, csMAG, 296);
    TEXTOUT_P("the same " + csX2S + " as the sum of (O - E)" + csSqS + "/E, page 3.", G1, 12, 108, 1, csGRAY, 296);
    // ---- 2. Fisher's p is counting ----
    TEXTOUT_P("2. Fisher's p counts tables:", G1, 12, 126, 1, #000000h, 222);
    csBadge(236, 126, csXFSt == 1);
    // the chance of top-left count a, with this table's totals put in
    // (in letters if the numbers would not fit on the line)
    pxS := "P(a) = C(" + STRING(csRT(1), 2, 0) + ", a)" + pxQ + "C(" + STRING(csRT(2), 2, 0) + ", " + pxC1 + " - a) / C(" + pxN + ", " + pxC1 + ")";
    IF DIM(pxS) > 46 THEN pxS := "P(a) = C(r" + CHAR(8321) + ", a)" + pxQ + "C(r" + CHAR(8322) + ", c" + CHAR(8321) + " - a) / C(N, c" + CHAR(8321) + ")"; END;
    TEXTOUT_P(pxS, G1, 12, 139, 1, csBLUE, 296);
    TEXTOUT_P("the chance of top-left count a, totals fixed.", G1, 12, 152, 1, #000000h, 296);
    TEXTOUT_P("Tables no likelier than yours (red + magenta):", G1, 12, 165, 1, #000000h, 296);
    // the decimal part, csFisher's value
    // beta 3c: a p below 0.0001 is never put through STRING (it would
    // be scientific there before "(< 0.0001)" replaced it)
    IF csFish < 0.0001 THEN pxR := "(< 0.0001)"; ELSE pxR := "= " + STRING(csFish, 2, 4); END;
    CASE
      IF csXFSt == 1 THEN
        IF DIM(csXS) > 0 AND DIM(csXT) > 0 AND DIM(csXS) + DIM(csXT) <= 15 THEN
          // small enough to show the counting itself
          TEXTOUT_P("  " + csXS + " of the " + csXT + " equally likely ways", G1, 12, 178, 1, #000000h, 296);
          pxS := "p = " + csXS + "/" + csXT;
          IF csXS + "/" + csXT == csXFr THEN
            pxS := pxS + " " + pxR;
          ELSE
            pxS := pxS + " = " + csXFr + " " + pxR;  // the CAS reduced it
          END;
          IF DIM(pxS) > 46 THEN pxS := csXPText(46); END;
        ELSE
          TEXTOUT_P("  their ways, over all C(" + pxN + ", " + pxC1 + ") ways:", G1, 12, 178, 1, #000000h, 296);
          pxS := csXPText(46);
        END;
      END;
      IF csXFSt == 3 THEN
        TEXTOUT_P("  C(" + pxN + ", " + pxC1 + ") has about " + STRING(csXDg, 2, 0) + " digits: too big", G1, 12, 178, 1, csGRAY, 296);
        pxS := csPStr(csFish) + " (the decimal)";
      END;
      IF csXFSt == 4 THEN
        TEXTOUT_P("  more than 3,000 tables: not computed here;", G1, 12, 178, 1, csGRAY, 296);
        pxS := "the " + csX2S + " approximation is excellent there.";
      END;
      DEFAULT
        TEXTOUT_P("  (the CAS did not answer: the decimal only)", G1, 12, 178, 1, csGRAY, 296);
        pxS := csPStr(csFish);
    END;
    TEXTOUT_P(pxS, G1, 12, 191, 1, csMAG, 296);
  ELSE
    // ---- a 2 x 3 or 3 x 2: chi2 = sum of (O - E)^2 / E, exactly ----
    TEXTOUT_P("1. " + csX2S + " for a " + STRING(csNR, 2, 0) + pxQ + STRING(csNC, 2, 0) + " table:", G1, 12, 30, 1, #000000h, 222);
    csBadge(236, 30, DIM(csXX2) > 0);
    TEXTOUT_P(csX2S + " = sum of (O - E)" + csSqS + " / E,  E = R" + pxQ + "C / N", G1, 12, 43, 1, csBLUE, 296);
    // the first cell written out, with the student's numbers
    pxS := STRING(csRT(1), 2, 0) + pxQ + pxC1 + " / " + pxN;
    TEXTOUT_P("first cell: (" + STRING(csO(1), 2, 0) + " - " + pxS + ")" + csSqS, G1, 12, 56, 1, csBLUE, 296);
    TEXTOUT_P("      / (" + pxS + "), and " + STRING(csNR * csNC - 1, 2, 0) + " more", G1, 12, 69, 1, csBLUE, 296);
    TEXTOUT_P("  " + csXX2Txt(44), G1, 12, 82, 1, csMAG, 296);
    TEXTOUT_P("Every E is a fraction, so " + csX2S + " is one too; the", G1, 12, 95, 1, csGRAY, 296);
    TEXTOUT_P("decimal is the same as on page 3.", G1, 12, 108, 1, csGRAY, 296);
    TEXTOUT_P("Fisher's exact p, as a fraction: 2" + pxQ + "2 only.", G1, 12, 126, 1, csGRAY, 296);
  END;
  // the page number, top right (the soft keys take the bottom)
  TEXTOUT_P(csFoot, G1, 282, 11, 1, csGRAY, 30);
  BLIT_P(G0, 0, 0, G1);
END;

// The page viewer: left/right arrows or a tap on the left/right
// half of the screen turn pages.  On the last page a tap on the
// right half, or Right, stays put (beta 1l: it used to leave for
// the menu, which the footer did not say); Enter or Esc leaves.
// beta 2: the soft keys Prev / Next turn pages too, Bars, Mosaic
// and Curve jump to that plot, Menu opens the results menu (x10 N,
// N / 10, Restore).  On page 1 a tap on a CELL explains its E
// instead of turning the page.  Returns the next screen for csView
// (0 = the main menu, 2 bars, 3 mosaic, 4 curve).
csResults()
BEGIN
  LOCAL rsPg, rsN, rsK, rsM, rsM1, rsGo;
  LOCAL rsSl, rsNx, rsC, rsOld;
  // beta 3: one more page at the end, "Where the numbers come from"
  // (csPageExact): 7 pages for a 2 x 2, 6 for a 2 x 3 or 3 x 2
  rsN := 6;
  IF csNR == 2 AND csNC == 2 THEN rsN := 7; END;
  rsPg := 1; rsGo := 1; rsNx := 0; csTapC := 0;
  REPEAT rsK := GETKEY; UNTIL rsK == -1;
  csDrain();
  WHILE rsGo == 1 DO
    // beta 2: the footer is only the page number now, drawn in a
    // corner of each page; the soft keys take the bottom strip
    csFoot := STRING(rsPg, 2, 0) + "/" + STRING(rsN, 2, 0);
    CASE
      IF rsPg == 1 THEN csGrid(2); END;
      IF rsPg == 2 THEN csGrid(3); END;
      IF rsPg == 3 THEN csPageTest(); END;
      IF rsPg == 4 THEN csGrid(4); END;
      IF rsPg == 5 THEN csPageInterp(); END;
      IF rsPg == 6 AND rsN == 7 THEN csPage2x2(); END;
      DEFAULT csPageExact();                 // beta 3: the last page
    END;
    csStrip(1);
    REPEAT
      rsK := GETKEY;
      rsM := MOUSE;
      rsM1 := rsM(1);
    UNTIL rsK > -1 OR SIZE(rsM1) > 0;
    rsOld := rsPg;
    IF SIZE(rsM1) > 0 THEN
      csDrain();
      IF rsM1(2) >= 218 THEN
        // the soft-key strip: six 53-px slots, left to right
        rsSl := MIN(6, IP(rsM1(1) / 53.4) + 1);
        IF rsSl == 1 THEN rsPg := rsPg - 1; END;
        IF rsSl == 2 THEN rsPg := MIN(rsPg + 1, rsN); END;
        IF rsSl >= 3 AND rsSl <= 5 THEN rsNx := rsSl - 1; rsGo := 0; END;
        IF rsSl == 6 THEN
          IF csResMenu() == 0 THEN rsGo := 0; END;
        END;
      ELSE
        rsC := 0;
        IF rsPg == 1 THEN rsC := csCellAt(rsM1(1), rsM1(2)); END;
        IF rsC > 0 THEN
          csTapC := rsC;                     // page 1: explain this cell's E
        ELSE
          IF rsM1(1) < 160 THEN rsPg := rsPg - 1; ELSE rsPg := MIN(rsPg + 1, rsN); END;
        END;
      END;
    ELSE
      IF rsK == 7 THEN rsPg := rsPg - 1; END;
      IF rsK == 8 THEN rsPg := MIN(rsPg + 1, rsN); END;     // Right stops at the last page
      IF rsK == 30 THEN rsPg := rsPg + 1; END;              // Enter past the last page = menu
      IF rsK == 4 THEN rsGo := 0; END;
    END;
    IF rsPg < 1 THEN rsPg := 1; END;
    IF rsPg > rsN THEN rsGo := 0; END;
    IF rsPg <> rsOld THEN csTapC := 0; END;   // the explanation belongs to page 1
  END;
  csFoot := ""; csTapC := 0;
  REPEAT rsK := GETKEY; UNTIL rsK == -1;
  RETURN rsNx;
END;


// ------------------------------------------------------------
// PLOT 1: observed beside expected, one pair of bars per cell
// ------------------------------------------------------------
csBars()
BEGIN
  LOCAL bcR, bcC, bcI, bcGW, bcPW, bcBW;
  LOCAL bcMax, bcStp, bcYmx, bcDec, bcV, bcY;
  LOCAL bcX, bcXE, bcH, bcCol, bcS, bcN;
  // ---- geometry ----
  bcGW := IP((csPR - csPL) / csNR);          // one group of bars per row
  bcPW := IP((bcGW - 10) / csNC);            // one pair (O, E) per column
  bcBW := IP(bcPW / 2) - 2;                 // one bar
  // ---- y scale from the tallest bar ----
  bcMax := 0;
  FOR bcI FROM 1 TO csNR * csNC DO
    bcMax := MAX(bcMax, csO(bcI), csE(bcI));
  END;
  bcStp := csNice(bcMax / 4);
  bcYmx := CEILING(bcMax / bcStp) * bcStp;
  IF bcYmx <= bcMax THEN bcYmx := bcYmx + bcStp; END;
  // beta 2: while a finger drags a bar the scale is FROZEN (csYF,
  // csYS, saved from the frame the drag began on), so the bar
  // follows the finger instead of the axis running away from it;
  // csBarDrag sets csYF back to 0 on lift, and the scale refits.
  IF csYF > 0 THEN bcYmx := csYF; bcStp := csYS; END;
  csYT := bcYmx; csYS := bcStp;              // for the next drag to freeze
  csBW := bcBW;                              // an observed bar's width, for its hit box
  bcDec := MAX(0, -FLOOR(LOG(bcStp)));
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  TEXTOUT_P("Observed (solid), expected (pale)", G1, 6, 4, 2, #000000h, 230);
  // the column key sits at the right end of the title row, clear of the bars
  TEXTOUT_P("C1", G1, 244, 7, 1, csBLUE, 22);
  TEXTOUT_P("C2", G1, 268, 7, 1, csMAG, 22);
  IF csNC == 3 THEN TEXTOUT_P("C3", G1, 292, 7, 1, csGRN, 22); END;
  // ---- grid and y ticks ----
  bcV := 0; bcN := 0;
  WHILE bcV <= bcYmx + bcStp / 1000 AND bcN < 12 DO
    bcY := csPB - IP(bcV / bcYmx * (csPB - csPT) + 0.5);
    LINE_P(G1, csPL, bcY, csPR, bcY, csGRID);
    TEXTOUT_P(STRING(bcV, 2, bcDec), G1, 4, bcY - 5, 1, csGRAY, 34);
    bcV := bcV + bcStp;
    bcN := bcN + 1;
  END;
  LINE_P(G1, csPL, csPT, csPL, csPB, #000000h);
  LINE_P(G1, csPL, csPB, csPR, csPB, #000000h);
  // ---- the bars ----
  FOR bcR FROM 1 TO csNR DO
    FOR bcC FROM 1 TO csNC DO
      bcI := csIx(bcR, bcC);
      bcX := csPL + (bcR - 1) * bcGW + 5 + (bcC - 1) * bcPW;
      bcXE := bcX + bcBW + 2;
      bcCol := csColC(bcC);
      // the hit box for a tap: the pair and the full plot height
      csTX1(bcI) := bcX; csTX2(bcI) := bcXE + bcBW;
      csTY1(bcI) := csPT; csTY2(bcI) := csPB;
      // observed, solid
      bcH := IP(csO(bcI) / bcYmx * (csPB - csPT) + 0.5);
      bcH := MIN(bcH, csPB - csPT);          // beta 2: a frozen scale can be outgrown
      IF bcH > 0 THEN
        RECT_P(G1, bcX, csPB - bcH, bcX + bcBW, csPB - 1, bcCol, bcCol);
      END;
      // expected, pale fill with the column's outline
      bcH := IP(csE(bcI) / bcYmx * (csPB - csPT) + 0.5);
      bcH := MIN(bcH, csPB - csPT);
      IF bcH > 0 THEN
        RECT_P(G1, bcXE, csPB - bcH, bcXE + bcBW, csPB - 1, bcCol, csPaleC(bcC));
      END;
      // the selected pair gets a red frame
      IF bcI == csSel THEN
        LINE_P(G1, bcX - 2, csPT, bcXE + bcBW + 2, csPT, csRED);
        LINE_P(G1, bcX - 2, csPB, bcXE + bcBW + 2, csPB, csRED);
        LINE_P(G1, bcX - 2, csPT, bcX - 2, csPB, csRED);
        LINE_P(G1, bcXE + bcBW + 2, csPT, bcXE + bcBW + 2, csPB, csRED);
      END;
    END;
    TEXTOUT_P("Row " + STRING(bcR, 2, 0), G1, csPL + (bcR - 1) * bcGW + IP(bcGW / 2) - 16, csPB + 4, 1, #000000h, 40);
  END;
  // ---- readout ----
  // beta 3c: E, (O-E)^2/E and chi2 rounded before STRING (scientific
  // below 10^-d)
  bcS := "R" + STRING(IP((csSel - 1) / csNC) + 1, 2, 0) + " C" + STRING(csSel - IP((csSel - 1) / csNC) * csNC, 2, 0) + ":  O = " + STRING(csO(csSel), 2, 0) + ",  E = " + STRING(ROUND(csE(csSel), 2), 2, 2) + ",  (O-E)" + csSqS + "/E = " + STRING(ROUND(csCon(csSel), 3), 2, 3);
  // beta 2: the readout moves up from y 206 to 192, and the footer
  // (y 222, now under the soft keys) becomes the verdict line at
  // 204: chi2 and p, red when p < alpha and green when not, so the
  // colour flips the moment a dragged bar pushes p across alpha
  TEXTOUT_P(bcS, G1, 6, 192, 1, csRED, 308);
  IF csDgBad == 1 THEN
    TEXTOUT_P("A row or column would add to 0: lift = refused", G1, 6, 204, 1, csRED, 308);
  ELSE
    bcS := csX2S + " = " + STRING(ROUND(csX2, 2), 2, 2) + ", " + csPStr(csP);
    IF csP < csAlpha THEN
      bcS := bcS + ": REJECT " + csH0; bcCol := csRED;
    ELSE
      bcS := bcS + ": keep " + csH0; bcCol := csGRN;
    END;
    TEXTOUT_P(bcS, G1, 6, 204, 1, bcCol, 240);
    IF DIM(bcS) <= 36 THEN TEXTOUT_P("drag bars", G1, 252, 204, 1, csGRAY, 56); END;
  END;
  BLIT_P(G0, 0, 0, G1);
END;

// beta 2: a finger pressed on the SOLID (observed) bar of cell dgI
// at (dgX0, dgY0).  While it stays down the loop reads MOUSE once
// per pass: after it has moved more than 8 px (press vs drag) the
// count becomes the y under the finger on the frozen scale, rounded
// and clamped at 0, and csCompute runs on every change -- so every
// pale expected bar moves, and chi2, p and the verdict colour
// follow.  A row or column that would add to 0 has no expected
// counts (0 / 0): such a sample is drawn but not computed, and the
// lift refuses it.  Any key also ends the drag (a way out if a
// touch reading ever sticks).  Returns 1 for a drag, 0 for a tap.
csBarDrag(dgI, dgX0, dgY0)
BEGIN
  LOCAL dgK, dgM, dgM1, dgGo, dgMv, dgV;
  LOCAL dgO0, dgR, dgC, dgRs, dgCs;
  dgO0 := csO(dgI);                          // the count before the drag
  dgR := IP((dgI - 1) / csNC) + 1;
  dgC := dgI - (dgR - 1) * csNC;
  dgRs := csRT(dgR) - dgO0;                  // the rest of its row ...
  dgCs := csCT(dgC) - dgO0;                  // ... and of its column
  csSel := dgI;
  csYF := csYT;                              // freeze the y-scale (csYS holds its step)
  dgGo := 1; dgMv := 0; csDgBad := 0; dgK := -1;
  WHILE dgGo == 1 DO
    dgK := GETKEY;
    dgM := MOUSE;
    dgM1 := dgM(1);
    IF dgK > -1 OR SIZE(dgM1) == 0 THEN
      dgGo := 0;                             // the finger lifted, or a key
    ELSE
      IF ABS(dgM1(2) - dgY0) > 8 OR ABS(dgM1(1) - dgX0) > 8 THEN dgMv := 1; END;
      IF dgMv == 1 THEN
        // the same mapping the bars are drawn with, run backwards
        dgV := ROUND((csPB - dgM1(2)) / (csPB - csPT) * csYF, 0);
        dgV := MIN(MAX(dgV, 0), csYF);
        IF dgV <> csO(dgI) THEN
          csO(dgI) := dgV;
          IF dgV == 0 AND (dgRs == 0 OR dgCs == 0) THEN
            csDgBad := 1;                    // no E without a row or column total
          ELSE
            csDgBad := 0;
            csRecalc();                      // EVERY expected count moves
          END;
          csBars();
          csStrip(2);
        END;
      END;
    END;
  END;
  IF dgK > -1 THEN REPEAT dgK := GETKEY; UNTIL dgK == -1; END;
  csDrain();
  csYF := 0;                                 // refit the scale on the next frame
  csDgBad := 0;
  IF dgMv == 0 THEN RETURN 0; END;           // no movement: it was a tap
  IF csO(dgI) <> dgO0 THEN
    IF csCheck() == 0 THEN
      // refused (csCheck has said why): the count goes back
      csO(dgI) := dgO0;
      csRecalc();
      REPEAT dgK := GETKEY; UNTIL dgK == -1;
      csDrain();
    END;
  END;
  csSel := dgI;
  RETURN 1;
END;


// ------------------------------------------------------------
// PLOT 2: a mosaic.  One bar per row, its width the row's share
// of N; the tiles in it are the columns, their heights the row
// percentages.  Under independence every bar would look like
// the grey "all" bar on the right.  Tile colour is the adjusted
// residual: blue = more than expected, red = fewer, darker
// beyond |4| (the shading Friendly and R's mosaicplot use).
// ------------------------------------------------------------
csMosaic()
BEGIN
  LOCAL moR, moC, moI, moX, moY, moW;
  LOCAL moH, moGap, moAW, moAH, moCol, moFill;
  LOCAL moXR, moS, moPR, moA;
  LOCAL moL;
  moGap := 4;
  moPR := csPR - 40;                        // room for the "all" bar
  moAW := (moPR - csPL) - moGap * (csNR - 1);
  moAH := (csPB - csPT) - moGap * (csNC - 1);
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  TEXTOUT_P("Mosaic: area = count, colour = residual", G1, 6, 4, 2, #000000h, 308);
  moX := csPL;
  FOR moR FROM 1 TO csNR DO
    moW := IP(moAW * csRT(moR) / csN + 0.5);
    moY := csPT;
    FOR moC FROM 1 TO csNC DO
      moI := csIx(moR, moC);
      moH := IP(moAH * csO(moI) / csRT(moR) + 0.5);
      // beta 2, the independence morph: a fraction csMT of the way
      // from this row's share (O / row total) to the column's share
      // of all N (C / N) -- which is E / row total, so at csMT = 1
      // every tile holds its expected count.  csMT = 0: as before.
      IF csMT > 0 THEN
        moH := IP(moAH * ((1 - csMT) * csO(moI) / csRT(moR) + csMT * csCT(moC) / csN) + 0.5);
      END;
      csTX1(moI) := moX; csTY1(moI) := moY;
      csTX2(moI) := moX + moW; csTY2(moI) := moY + moH;
      // colour by the adjusted residual
      moA := csAdj(moI);
      moCol := csGRAY; moFill := csLGRY;
      IF moA >= 2 THEN moCol := csBLUE; moFill := csLBLU; END;
      IF moA >= 4 THEN moFill := csMBLU; END;
      IF moA <= -2 THEN moCol := csRED; moFill := csLRED; END;
      IF moA <= -4 THEN moFill := csMRED; END;
      // beta 2: during the morph the colours fade to the grey of the
      // "all" bar, and no tile is singled out
      IF csMT > 0 THEN moFill := csMFill(moA, csMT); moCol := csMEdge(moA, csMT); END;
      IF moI == csSel AND csMT == 0 THEN moCol := csRED; END;
      IF moH > 0 AND moW > 0 THEN
        RECT_P(G1, moX, moY, moX + moW, moY + moH, moCol, moFill);
        IF moI == csSel AND csMT == 0 THEN
          RECT_P(G1, moX + 1, moY + 1, moX + moW - 1, moY + moH - 1, moCol, moFill);
        END;
        IF moW >= 30 AND moH >= 12 THEN
          // the count, or during the morph the count on its way to E
          moL := csO(moI);
          IF csMT > 0 THEN moL := ROUND((1 - csMT) * csO(moI) + csMT * csE(moI), 0); END;
          TEXTOUT_P("C" + STRING(moC, 2, 0) + " " + STRING(moL, 2, 0), G1, moX + 3, moY + 2, 1, #000000h, moW - 4);
        END;
      END;
      moY := moY + moH + moGap;
    END;
    TEXTOUT_P("Row " + STRING(moR, 2, 0), G1, moX + 2, csPB + 4, 1, #000000h, moW);
    moX := moX + moW + moGap;
  END;
  // ---- the reference bar: the column shares of all N ----
  moXR := csPR - 28;
  moY := csPT;
  FOR moC FROM 1 TO csNC DO
    moH := IP(moAH * csCT(moC) / csN + 0.5);
    IF moH > 0 THEN
      RECT_P(G1, moXR, moY, moXR + 28, moY + moH, csGRAY, csLGRY);
      IF moH >= 12 THEN TEXTOUT_P("C" + STRING(moC, 2, 0), G1, moXR + 4, moY + 2, 1, csGRAY, 24); END;
    END;
    moY := moY + moH + moGap;
  END;
  TEXTOUT_P("all", G1, moXR + 4, csPB + 4, 1, csGRAY, 28);
  // ---- legend and readout ----
  // beta 2: moved up from y 198 / 210 to 190 / 202, and the footer
  // (y 224) dropped -- the soft keys take the bottom strip.  During
  // the morph the two lines say what is happening instead.
  IF csMT > 0 THEN
    TEXTOUT_P("Independence (" + csH0 + "): every bar takes the shape", G1, 6, 190, 1, csMAG, 308);
    IF csMT < 1 THEN
      TEXTOUT_P("of the grey 'all' bar; O becomes E.  Tap = stop", G1, 6, 202, 1, csGRAY, 308);
    ELSE
      TEXTOUT_P("of the grey 'all' bar; tiles hold E.  Tap = back", G1, 6, 202, 1, csGRAY, 308);
    END;
  ELSE
    TEXTOUT_P("blue: more than expected; red: fewer (|adj|>=2)", G1, 6, 190, 1, csGRAY, 308);
    moS := "R" + STRING(IP((csSel - 1) / csNC) + 1, 2, 0) + " C" + STRING(csSel - IP((csSel - 1) / csNC) * csNC, 2, 0) + ":  O = " + STRING(csO(csSel), 2, 0) + ",  row " + STRING(ROUND(100 * csO(csSel) / csRT(IP((csSel - 1) / csNC) + 1), 1), 2, 1) + "%,  adj = " + csSgn(csAdj(csSel), 2);
    TEXTOUT_P(moS, G1, 6, 202, 1, csRED, 308);
  END;
  BLIT_P(G0, 0, 0, G1);
END;

// beta 2: the independence morph.  Ten frames, about 0.15 s each,
// carry every tile from its observed row % to the column shares of
// the grey "all" bar while the residual colours fade to grey -- H0
// made visible.  Any key or tap stops it at once; at the end the H0
// picture stays until a key or tap.  The frame delay is a TICKS
// loop that keeps polling the keys and the screen.
csMorph()
BEGIN
  LOCAL mfF, mfT, mfK, mfM, mfM1, mfStop;
  mfStop := 0;
  REPEAT mfK := GETKEY; UNTIL mfK == -1;
  csDrain();
  FOR mfF FROM 1 TO 10 DO
    IF mfStop == 0 THEN
      csMT := mfF / 10;
      csMosaic();
      mfT := TICKS;
      REPEAT
        mfK := GETKEY;
        mfM := MOUSE;
        mfM1 := mfM(1);
        IF mfK > -1 OR SIZE(mfM1) > 0 THEN mfStop := 1; END;
      UNTIL mfStop == 1 OR TICKS - mfT > 150;
    END;
  END;
  IF mfStop == 0 THEN csWaitAny(); END;      // hold the H0 picture
  csMT := 0;                                 // back to the observed mosaic
  REPEAT mfK := GETKEY; UNTIL mfK == -1;
  csDrain();
END;

// ------------------------------------------------------------
// beta 2, PLOT 4: the adjusted residual of every cell as a bar from
// a zero line -- blue up (more than expected), red down (fewer),
// solid beyond the dotted +/-2 lines, pale inside them.  Bars are
// grouped by row like the bars plot; a tap picks a bar (csPlotLoop).
// ------------------------------------------------------------
csResBars()
BEGIN
  LOCAL rbR, rbC, rbI, rbGW, rbBW, rbX;
  LOCAL rbMx, rbStp, rbV, rbY, rbY0, rbN;
  LOCAL rbH, rbCol, rbFil, rbS, rbDec, rbA;
  LOCAL rbHH;
  // ---- the scale: symmetric about 0, at least +/-3 so the +/-2
  // lines always show ----
  rbMx := 3;
  FOR rbI FROM 1 TO csNR * csNC DO
    rbMx := MAX(rbMx, ABS(csAdj(rbI)) * 1.1);
  END;
  rbStp := csNice(rbMx / 3);
  rbMx := CEILING(rbMx / rbStp) * rbStp;
  rbDec := MAX(0, -FLOOR(LOG(rbStp)));
  rbHH := IP((csPB - csPT) / 2);            // half the box: 0 to the top
  rbY0 := csPT + rbHH;                      // the zero line
  // ---- geometry: one group per row, one bar per cell ----
  rbGW := IP((csPR - csPL) / csNR);
  rbBW := IP((rbGW - 10) / csNC) - 4;
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  TEXTOUT_P("Adjusted residual per cell", G1, 6, 4, 2, #000000h, 308);
  // ---- grid and y ticks, both signs ----
  rbV := -rbMx; rbN := 0;
  WHILE rbV <= rbMx + rbStp / 1000 AND rbN < 14 DO
    rbY := rbY0 - ROUND(rbV / rbMx * rbHH, 0);
    LINE_P(G1, csPL, rbY, csPR, rbY, csGRID);
    TEXTOUT_P(STRING(rbV, 2, rbDec), G1, 4, rbY - 5, 1, csGRAY, 34);
    rbV := rbV + rbStp;
    rbN := rbN + 1;
  END;
  // ---- the +/-2 lines, dotted magenta ----
  csDotH(csPL, csPR, rbY0 - ROUND(2 / rbMx * rbHH, 0), csMAG);
  csDotH(csPL, csPR, rbY0 + ROUND(2 / rbMx * rbHH, 0), csMAG);
  LINE_P(G1, csPL, csPT, csPL, csPB, #000000h);
  LINE_P(G1, csPL, rbY0, csPR, rbY0, #000000h);     // the zero line
  // ---- the bars ----
  FOR rbR FROM 1 TO csNR DO
    FOR rbC FROM 1 TO csNC DO
      rbI := csIx(rbR, rbC);
      rbX := csPL + (rbR - 1) * rbGW + 5 + (rbC - 1) * (rbBW + 4);
      rbA := csAdj(rbI);
      rbH := ROUND(ABS(rbA) / rbMx * rbHH, 0);
      // colour: the sign; solid when |adj| >= 2
      IF rbA >= 0 THEN
        rbCol := csBLUE; rbFil := csLBLU;
        IF rbA >= 2 THEN rbFil := csBLUE; END;
      ELSE
        rbCol := csRED; rbFil := csLRED;
        IF rbA <= -2 THEN rbFil := csRED; END;
      END;
      // the hit box: the bar's column, the full height of the box
      csTX1(rbI) := rbX; csTX2(rbI) := rbX + rbBW;
      csTY1(rbI) := csPT; csTY2(rbI) := csPB;
      IF rbH > 0 THEN
        IF rbA >= 0 THEN
          RECT_P(G1, rbX, rbY0 - rbH, rbX + rbBW, rbY0, rbCol, rbFil);
        ELSE
          RECT_P(G1, rbX, rbY0, rbX + rbBW, rbY0 + rbH, rbCol, rbFil);
        END;
      ELSE
        LINE_P(G1, rbX, rbY0, rbX + rbBW, rbY0, rbCol);
      END;
      // the column name on the other side of the zero line
      IF rbA >= 0 THEN
        TEXTOUT_P("C" + STRING(rbC, 2, 0), G1, rbX + 2, rbY0 + 3, 1, csColC(rbC), rbBW);
      ELSE
        TEXTOUT_P("C" + STRING(rbC, 2, 0), G1, rbX + 2, rbY0 - 13, 1, csColC(rbC), rbBW);
      END;
      // the selected bar gets a red frame
      IF rbI == csSel THEN
        LINE_P(G1, rbX - 2, csPT, rbX + rbBW + 2, csPT, csRED);
        LINE_P(G1, rbX - 2, csPB, rbX + rbBW + 2, csPB, csRED);
        LINE_P(G1, rbX - 2, csPT, rbX - 2, csPB, csRED);
        LINE_P(G1, rbX + rbBW + 2, csPT, rbX + rbBW + 2, csPB, csRED);
      END;
    END;
    TEXTOUT_P("Row " + STRING(rbR, 2, 0), G1, csPL + (rbR - 1) * rbGW + IP(rbGW / 2) - 16, csPB + 4, 1, #000000h, 40);
  END;
  // ---- readout ----
  rbS := "R" + STRING(IP((csSel - 1) / csNC) + 1, 2, 0) + " C" + STRING(csSel - IP((csSel - 1) / csNC) * csNC, 2, 0) + ":  O = " + STRING(csO(csSel), 2, 0) + ",  E = " + STRING(ROUND(csE(csSel), 2), 2, 2) + ",  adj = " + csSgn(csAdj(csSel), 2);
  TEXTOUT_P(rbS, G1, 6, 192, 1, csRED, 308);
  TEXTOUT_P("Dotted lines at +2 and -2: past them, notable.", G1, 6, 204, 1, csMAG, 308);
  BLIT_P(G0, 0, 0, G1);
END;


// ------------------------------------------------------------
// THE PLOT LOOP shared by the bars and the mosaic (plWhich 1 or
// 2): arrows move the highlight through the cells, a tap picks
// the tile under the finger, Esc or Enter returns.
// beta 2: also the residual plot (plWhich 3); the soft-key strip
// (Prev / Next = the plot before or after, Bars / Mosaic / Curve,
// Menu; Restore on the bars, Morph on the mosaic); on the bars a
// press on a SOLID bar starts a drag (csBarDrag) and Up / Down
// change the selected count; on the mosaic + starts the morph.
// Returns the next screen for csView (0 = the main menu).
// ------------------------------------------------------------
csPlotLoop(plWhich)
BEGIN
  LOCAL plK, plM, plM1, plGo, plI, plN;
  LOCAL plVw, plNx, plSl, plHit, plS, plO;
  plN := csNR * csNC;
  IF csSel < 1 OR csSel > plN THEN csSel := 1; END;
  plGo := 1; plNx := 0;
  plVw := plWhich + 1;                       // the screen number: 2 bars, 3 mosaic
  IF plWhich == 3 THEN plVw := 5; END;       // 5 residuals
  REPEAT plK := GETKEY; UNTIL plK == -1;
  csDrain();
  WHILE plGo == 1 DO
    CASE
      IF plWhich == 1 THEN csBars(); END;
      IF plWhich == 2 THEN csMosaic(); END;
      DEFAULT csResBars();
    END;
    csStrip(plVw);                           // the soft keys, after the blit
    REPEAT
      plK := GETKEY;
      plM := MOUSE;
      plM1 := plM(1);
    UNTIL plK > -1 OR SIZE(plM1) > 0;
    IF SIZE(plM1) > 0 THEN
      // beta 2: on the bars plot, is the press on a SOLID bar?  (its
      // hit box: the observed bar's own width, the full plot height)
      plHit := 0;
      IF plWhich == 1 AND plM1(2) >= csPT AND plM1(2) <= csPB THEN
        FOR plI FROM 1 TO plN DO
          IF plM1(1) >= csTX1(plI) - 2 AND plM1(1) <= csTX1(plI) + csBW + 1 THEN plHit := plI; END;
        END;
      END;
      IF plHit > 0 THEN
        // a drag sets the count; a press that does not move selects
        // the cell, exactly as a tap always has
        IF csBarDrag(plHit, plM1(1), plM1(2)) == 0 THEN csSel := plHit; END;
      ELSE
        csDrain();
        IF plM1(2) >= 218 THEN
          // the soft-key strip: six 53-px slots, left to right
          plSl := MIN(6, IP(plM1(1) / 53.4) + 1);
          IF plSl == 1 THEN plNx := csPlotStep(plVw, -1); plGo := 0; END;
          IF plSl == 2 THEN plNx := csPlotStep(plVw, 1); plGo := 0; END;
          IF plSl == 3 THEN
            IF plWhich == 1 THEN csRestore(); ELSE plNx := 2; plGo := 0; END;
          END;
          IF plSl == 4 THEN
            IF plWhich == 2 THEN csMorph(); ELSE plNx := 3; plGo := 0; END;
          END;
          IF plSl == 5 THEN plNx := 4; plGo := 0; END;
          IF plSl == 6 THEN
            IF csResMenu() == 0 THEN plNx := 0; plGo := 0; END;
          END;
        ELSE
          FOR plI FROM 1 TO plN DO
            IF plM1(1) >= csTX1(plI) AND plM1(1) <= csTX2(plI) AND plM1(2) >= csTY1(plI) AND plM1(2) <= csTY2(plI) THEN
              csSel := plI;
            END;
          END;
        END;
      END;
    ELSE
      IF plK == 7 THEN csSel := MAX(1, csSel - 1); END;
      IF plK == 8 THEN csSel := MIN(plN, csSel + 1); END;
      IF plK == 4 OR plK == 30 THEN plGo := 0; END;
      // beta 2, bars: Up / Down change the selected count -- the
      // keyboard twin of dragging a bar -- by 1, 2, 5, 10 ... (about
      // 1% of the scale); csCheck refuses a row or column at 0
      IF plWhich == 1 AND (plK == 2 OR plK == 12) THEN
        plS := MAX(1, csNice(csYT / 100));
        plI := csSel;
        plO := csO(plI);
        IF plK == 2 THEN csO(plI) := plO + plS; ELSE csO(plI) := MAX(0, plO - plS); END;
        IF csCheck() == 0 THEN
          csO(plI) := plO;                   // refused: back as it was
          csRecalc();
          REPEAT plK := GETKEY; UNTIL plK == -1;
          csDrain();
        END;
        csSel := plI;
      END;
      // beta 2, mosaic: + plays the independence morph
      IF plWhich == 2 AND plK == 50 THEN csMorph(); END;
    END;
  END;
  REPEAT plK := GETKEY; UNTIL plK == -1;
  RETURN plNx;
END;


// ------------------------------------------------------------
// PLOT 3: the chi-square curve for df, the p-value shaded to the
// right of X2, the critical value as a dotted red line.
// beta 2: an interactive screen.  The window and the density are
// computed ONCE (csCvFit) and every frame is drawn from that cache
// (csCvDraw), so dragging the red line costs no CHISQUARE calls
// per pixel.  The window stays fixed while the curve is open (a
// live explorer locks its window, Lessons Learned 6); it refits
// only when the line would leave the plot.
// ------------------------------------------------------------

// The window and the cached curve: the same arithmetic as beta
// 1l's csCurve, but the cache holds each column's pixel row (the
// shaded tail and the curve both read it).
csCvFit()
BEGIN
  LOCAL cfRaw, cfPx, cfXv, cfPd;
  cfRaw := MAX(csCrit * 1.6, csX2 * 1.15, 3);
  csCvS := csNice(cfRaw / 5);
  csCvX := CEILING(cfRaw / csCvS) * csCvS;
  csCvDc := MAX(0, -FLOOR(LOG(csCvS)));
  // the height: the df = 1 curve rises without limit near 0, so
  // it is simply clipped; df = 2 starts at 0.5; others peak at df-2
  csCvY := 0.6;
  IF csDF == 2 THEN csCvY := 0.55; END;
  // (df is 1 or 2 in this program; the next line is kept for a
  // future r x c table and never runs today)
  IF csDF > 2 THEN csCvY := CHISQUARE(csDF, csDF - 2) * 1.15; END;
  // ---- the density, once per pixel column (Lessons Learned 6) ----
  // csCvP(i) is the pixel row of the curve at column csPL + i,
  // i = 1..csCvW.
  csCvW := csPR - csPL;
  csCvP := MAKELIST(0, K, 1, csCvW, 1);
  FOR cfPx FROM 1 TO csCvW DO
    cfXv := cfPx / csCvW * csCvX;
    cfPd := CHISQUARE(csDF, cfXv);
    csCvP(cfPx) := csPB - IP(MIN(cfPd / csCvY, 1) * (csPB - csPT));
  END;
END;

// 1 when alpha is one of the ladder's values, 0 for a custom alpha.
csOnLad()
BEGIN
  LOCAL olL, olI, olR;
  olL := {0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2};
  olR := 0;
  FOR olI FROM 1 TO 7 DO
    IF ABS(csAlpha - olL(olI)) < 0.0000001 THEN olR := 1; END;
  END;
  RETURN olR;
END;

// On lift: alpha snaps to the ladder value it is within 10% of
// (the windows do not overlap), or stays as a custom alpha rounded
// to 3 decimals (4 below 0.01); then the critical value is exact.
csSnapA()
BEGIN
  LOCAL snL, snI, snA, snHit;
  snL := {0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2};
  snHit := 0; snA := csAlpha;
  FOR snI FROM 1 TO 7 DO
    IF ABS(csAlpha - snL(snI)) <= 0.1 * snL(snI) THEN snA := snL(snI); snHit := 1; END;
  END;
  IF snHit == 0 THEN
    IF csAlpha >= 0.01 THEN snA := ROUND(csAlpha, 3); ELSE snA := ROUND(csAlpha, 4); END;
  END;
  csAlpha := snA;
  csCrit := CHISQUARE_ICDF(csDF, 1 - csAlpha);
END;

// The arrows on the curve: saD = 1 (Left) the next LARGER alpha on
// the ladder, so the line moves left; saD = -1 (Right) the next
// smaller one.  From a custom alpha, the nearest rung that way.
csStepA(saD)
BEGIN
  LOCAL saL, saI, saA;
  saL := {0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2};
  saA := csAlpha;
  FOR saI FROM 1 TO 7 DO
    IF saD > 0 THEN
      // walking down from 0.2: the last rung above alpha is the next one up
      IF saL(8 - saI) > csAlpha * 1.0000001 THEN saA := saL(8 - saI); END;
    ELSE
      // walking up from 0.001: the last rung below alpha is the next one down
      IF saL(saI) < csAlpha * 0.9999999 THEN saA := saL(saI); END;
    END;
  END;
  csAlpha := saA;
  csCrit := CHISQUARE_ICDF(csDF, 1 - csAlpha);
END;

// One frame of the curve from the cache.  cdDrag = 1 while the red
// line is being dragged (its label then shows alpha).
csCvDraw(cdDrag)
BEGIN
  LOCAL cdV, cdN, cdPx, cdX2, cdXc, cdLx;
  LOCAL cdPy, cdLast, cdS;
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  TEXTOUT_P(csX2S + " curve for df = " + STRING(csDF, 2, 0) + ", and the observed " + csX2S, G1, 6, 4, 2, #000000h, 308);
  // ---- x ticks and grid ----
  cdV := 0; cdN := 0;
  WHILE cdV <= csCvX + csCvS / 1000 AND cdN < 14 DO
    cdPx := csPL + IP(cdV / csCvX * (csPR - csPL) + 0.5);
    LINE_P(G1, cdPx, csPT, cdPx, csPB, csGRID);
    LINE_P(G1, cdPx, csPB, cdPx, csPB + 3, #000000h);
    TEXTOUT_P(STRING(cdV, 2, csCvDc), G1, cdPx - 6, csPB + 6, 1, csGRAY, 30);
    cdV := cdV + csCvS;
    cdN := cdN + 1;
  END;
  // ---- the p-value: shade the tail beyond X2, column by column ----
  cdX2 := csPL + IP(csX2 / csCvX * (csPR - csPL) + 0.5);
  FOR cdPx FROM cdX2 TO csPR DO
    IF cdPx > csPL THEN                       // x > 0: the density exists
      cdPy := csCvP(cdPx - csPL);
      IF cdPy < csPB THEN LINE_P(G1, cdPx, csPB - 1, cdPx, cdPy, csLMAG); END;
    END;
  END;
  // ---- the curve, pixel column by pixel column ----
  cdLast := -1;
  FOR cdPx FROM csPL + 1 TO csPR DO
    cdPy := csCvP(cdPx - csPL);
    IF cdLast >= 0 THEN LINE_P(G1, cdPx - 1, cdLast, cdPx, cdPy, csBLUE); END;
    cdLast := cdPy;
  END;
  LINE_P(G1, csPL, csPT, csPL, csPB, #000000h);
  LINE_P(G1, csPL, csPB, csPR, csPB, #000000h);
  // ---- the critical value, dotted red; X2 itself, solid magenta ----
  cdXc := csPL + IP(csCrit / csCvX * (csPR - csPL) + 0.5);
  cdXc := MIN(cdXc, csPR);
  csDotV(G1, cdXc, csPT, csPB, csRED);
  // the line's labels go to its left when it nears the right edge
  cdLx := cdXc + 3;
  IF cdXc > csPR - 72 THEN cdLx := cdXc - 72; END;
  // beta 3c: crit and chi2 rounded before STRING (scientific below 0.01)
  TEXTOUT_P("crit = " + STRING(ROUND(csCrit, 2), 2, 2), G1, cdLx, csPT + 2, 1, csRED, 70);
  IF cdDrag == 1 THEN
    TEXTOUT_P(csAl + " = " + csAStr(), G1, cdLx, csPT + 26, 1, csRED, 70);
  ELSE
    TEXTOUT_P(CHAR(8592) + " drag " + CHAR(8594), G1, cdLx, csPT + 26, 1, csRED, 70);
  END;
  LINE_P(G1, cdX2, csPT, cdX2, csPB, csMAG);
  TEXTOUT_P(csX2S + " = " + STRING(ROUND(csX2, 2), 2, 2), G1, cdX2 + 3, csPT + 14, 1, csMAG, 70);
  // ---- readout: moved up from y 204 / 216 to 192 / 204 (the soft
  // keys take the bottom strip and the old footer) ----
  TEXTOUT_P("Shaded tail = p-value: " + csPStr(csP), G1, 6, 192, 1, csMAG, 308);
  IF csOnLad() == 1 THEN
    IF csP < csAlpha THEN
      TEXTOUT_P("Past the critical value (" + csAl + " = " + csAStr() + "): REJECT " + csH0, G1, 6, 204, 1, csRED, 308);
    ELSE
      TEXTOUT_P("Short of the critical value (" + csAl + " = " + csAStr() + "): keep " + csH0, G1, 6, 204, 1, csGRN, 308);
    END;
  ELSE
    // a dragged (or typed) alpha off the ladder: say so
    cdS := "custom " + csAl + " = " + csAStr() + "): ";
    IF csP < csAlpha THEN
      TEXTOUT_P("Past crit (" + cdS + "REJECT " + csH0, G1, 6, 204, 1, csRED, 308);
    ELSE
      TEXTOUT_P("Short of crit (" + cdS + "keep " + csH0, G1, 6, 204, 1, csGRN, 308);
    END;
  END;
  BLIT_P(G0, 0, 0, G1);
END;

// The curve screen.  A press within 6 px of the red line, then a
// sideways drag (more than 8 px), moves the line: alpha becomes
// 1 - CHISQUARE_CDF(df, x) at the finger, so the verdict flips from
// green to red as the line crosses X2.  On lift alpha snaps to the
// ladder or stays custom.  Left / Right step the ladder; Esc,
// Enter or a tap outside the plot box = menu; soft keys as on every
// plot (Reset = alpha back to 0.05).  Returns the next screen.
csCurve()
BEGIN
  LOCAL cvK, cvM, cvM1, cvGo, cvNx, cvSl;
  LOCAL cvXc, cvX0, cvMv, cvDg, cvXv, cvA;
  csCvFit();
  cvGo := 1; cvNx := 0;
  REPEAT cvK := GETKEY; UNTIL cvK == -1;
  csDrain();
  WHILE cvGo == 1 DO
    csCvDraw(0);
    csStrip(4);
    REPEAT
      cvK := GETKEY;
      cvM := MOUSE;
      cvM1 := cvM(1);
    UNTIL cvK > -1 OR SIZE(cvM1) > 0;
    IF SIZE(cvM1) > 0 THEN
      cvXc := MIN(csPL + IP(csCrit / csCvX * (csPR - csPL) + 0.5), csPR);
      IF cvM1(2) >= csPT AND cvM1(2) <= csPB AND ABS(cvM1(1) - cvXc) <= 6 THEN
        // ---- a press on the red line: drag it ----
        cvX0 := cvM1(1); cvMv := 0; cvDg := 1;
        WHILE cvDg == 1 DO
          cvK := GETKEY;
          cvM := MOUSE;
          cvM1 := cvM(1);
          IF cvK > -1 OR SIZE(cvM1) == 0 THEN
            cvDg := 0;                       // lifted, or a key
          ELSE
            IF ABS(cvM1(1) - cvX0) > 8 THEN cvMv := 1; END;
            IF cvMv == 1 THEN
              // the finger's x, in the plot's units, kept inside the box
              cvXv := MIN(MAX(cvM1(1), csPL + 1), csPR);
              cvXv := (cvXv - csPL) / (csPR - csPL) * csCvX;
              cvA := 1 - CHISQUARE_CDF(csDF, cvXv);
              // keep alpha sensible: 0.0001 to 0.5
              IF cvA > 0.5 THEN cvA := 0.5; cvXv := CHISQUARE_ICDF(csDF, 0.5); END;
              IF cvA < 0.0001 THEN cvA := 0.0001; cvXv := CHISQUARE_ICDF(csDF, 0.9999); END;
              IF cvXv <> csCrit THEN
                csAlpha := cvA; csCrit := cvXv;
                csCvDraw(1);                 // nothing re-shaded: the line and the verdict move
                csStrip(4);
              END;
            END;
          END;
        END;
        IF cvK > -1 THEN REPEAT cvK := GETKEY; UNTIL cvK == -1; END;
        csDrain();
        IF cvMv == 1 THEN
          csSnapA();                         // the ladder, or a custom alpha
          IF csCrit > csCvX * 0.97 THEN csCvFit(); END;
        END;
      ELSE
        csDrain();
        IF cvM1(2) >= 218 THEN
          // the soft-key strip: six 53-px slots, left to right
          cvSl := MIN(6, IP(cvM1(1) / 53.4) + 1);
          IF cvSl == 1 THEN cvNx := csPlotStep(4, -1); cvGo := 0; END;
          IF cvSl == 2 THEN cvNx := csPlotStep(4, 1); cvGo := 0; END;
          IF cvSl == 3 THEN cvNx := 2; cvGo := 0; END;
          IF cvSl == 4 THEN cvNx := 3; cvGo := 0; END;
          IF cvSl == 5 THEN
            csAlpha := 0.05;                 // Reset
            csCrit := CHISQUARE_ICDF(csDF, 0.95);
            IF csCrit > csCvX * 0.97 THEN csCvFit(); END;
          END;
          IF cvSl == 6 THEN
            IF csResMenu() == 0 THEN cvGo := 0; ELSE csCvFit(); END;
          END;
        ELSE
          // a tap outside the plot box = menu; inside it, nothing
          IF cvM1(1) < csPL OR cvM1(1) > csPR OR cvM1(2) < csPT OR cvM1(2) > csPB THEN cvGo := 0; END;
        END;
      END;
    ELSE
      IF cvK == 4 OR cvK == 30 THEN cvGo := 0; END;
      IF cvK == 7 THEN csStepA(1); END;      // Left: a larger alpha, the line moves left
      IF cvK == 8 THEN csStepA(-1); END;     // Right: a smaller alpha, the line moves right
      IF cvK == 7 OR cvK == 8 THEN
        IF csCrit > csCvX * 0.97 THEN csCvFit(); END;
      END;
    END;
  END;
  REPEAT cvK := GETKEY; UNTIL cvK == -1;
  RETURN cvNx;
END;


// ------------------------------------------------------------
// beta 2, PLOT 5 (2 x 2 only): Fisher's exact distribution.  With
// the margins fixed, a table is set by its top-left count a; every
// possible a gets one bar, its hypergeometric probability.  Red:
// the observed table.  Magenta: the tables no more probable than
// it -- their sum (with the red) is the two-sided p csFisher gives.
// Pale grey: the rest.  A tap on a bar shows that table in a
// corner panel.
// ------------------------------------------------------------

// (beta 3: csFishW, the probabilities of every table, moved up to just
// after csFisher -- unchanged -- because the exact Fisher fraction
// (csXFisher) and results page 6 need it, and a PPL routine must be
// defined before its first use.)

csFishDraw()
BEGIN
  LOCAL fdI, fdA, fdP, fdObs, fdMx, fdMd;
  LOCAL fdNs, fdBw, fdX1, fdX2, fdH, fdCol;
  LOCAL fdFil, fdStp, fdYmx, fdDec, fdV, fdY;
  LOCAL fdN, fdS, fdPx, fdPy, fdB, fdC;
  LOCAL fdD;
  fdObs := csFW(csO(1) - csFLo + 1);         // the observed table's probability
  // the tallest bar and where it is
  fdMx := 0; fdMd := csFLo;
  FOR fdI FROM 1 TO SIZE(csFW) DO
    IF csFW(fdI) > fdMx THEN fdMx := csFW(fdI); fdMd := csFLo + fdI - 1; END;
  END;
  // the a values drawn: every table at least 1/100000 as probable
  // as the tallest, and always the observed one -- the rest would
  // be bars too thin and too short to see
  csFDL := csFHi; csFDH := csFLo;
  FOR fdI FROM 1 TO SIZE(csFW) DO
    IF csFW(fdI) >= fdMx * 0.00001 THEN
      csFDL := MIN(csFDL, csFLo + fdI - 1);
      csFDH := MAX(csFDH, csFLo + fdI - 1);
    END;
  END;
  csFDL := MIN(csFDL, csO(1)); csFDH := MAX(csFDH, csO(1));
  fdNs := csFDH - csFDL + 1;
  fdBw := (csPR - csPL) / fdNs;              // pixels per a (may be under 1)
  // ---- y scale from the tallest bar ----
  fdStp := csNice(fdMx / 4);
  fdYmx := CEILING(fdMx / fdStp) * fdStp;
  IF fdYmx <= fdMx THEN fdYmx := fdYmx + fdStp; END;
  fdDec := MAX(0, -FLOOR(LOG(fdStp)));
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  // beta 3: the title is shorter (was "Every 2 x 2 with these
  // margins") so the badge of the exact p fits beside it
  TEXTOUT_P("Tables with these margins", G1, 6, 4, 2, #000000h, 224);
  IF csFish >= 0 THEN csBadge(236, 7, csXFSt == 1); END;
  fdV := 0; fdN := 0;
  WHILE fdV <= fdYmx + fdStp / 1000 AND fdN < 12 DO
    fdY := csPB - IP(fdV / fdYmx * (csPB - csPT) + 0.5);
    LINE_P(G1, csPL, fdY, csPR, fdY, csGRID);
    TEXTOUT_P(STRING(fdV, 2, fdDec), G1, 4, fdY - 5, 1, csGRAY, 34);
    fdV := fdV + fdStp;
    fdN := fdN + 1;
  END;
  // ---- one bar per a ----
  FOR fdA FROM csFDL TO csFDH DO
    fdP := csFW(fdA - csFLo + 1);
    fdX1 := csPL + IP((fdA - csFDL) * fdBw) + 1;
    fdX2 := csPL + IP((fdA - csFDL + 1) * fdBw) - 1;
    IF fdX2 < fdX1 THEN fdX2 := fdX1; END;
    // red: observed; magenta: no more probable (counts in p); grey
    fdCol := csGRAY; fdFil := csLGRY;
    IF fdP <= fdObs * 1.0000001 THEN fdCol := csMAG; fdFil := csMAG; END;
    IF fdA == csO(1) THEN fdCol := csRED; fdFil := csRED; END;
    fdH := IP(fdP / fdYmx * (csPB - csPT) + 0.5);
    IF fdH > 0 THEN RECT_P(G1, fdX1, csPB - fdH, fdX2, csPB - 1, fdCol, fdFil); END;
    // a 3-px strip under the axis in the same colour, so a bar too
    // short to see still shows whether it counts toward p
    RECT_P(G1, fdX1, csPB + 1, fdX2, csPB + 3, fdCol, fdCol);
    // the table in the corner panel gets an orange frame
    IF fdA == csFSel THEN
      LINE_P(G1, fdX1 - 1, csPT, fdX1 - 1, csPB, csORNG);
      LINE_P(G1, fdX2 + 1, csPT, fdX2 + 1, csPB, csORNG);
      LINE_P(G1, fdX1 - 1, csPT, fdX2 + 1, csPT, csORNG);
    END;
  END;
  LINE_P(G1, csPL, csPT, csPL, csPB, #000000h);
  LINE_P(G1, csPL, csPB, csPR, csPB, #000000h);
  // ---- x ticks: whole a values ----
  fdS := MAX(1, csNice(fdNs / 6));
  fdV := CEILING(csFDL / fdS) * fdS; fdN := 0;
  WHILE fdV <= csFDH AND fdN < 12 DO
    fdPx := csPL + IP((fdV - csFDL + 0.5) * fdBw);
    TEXTOUT_P(STRING(fdV, 2, 0), G1, fdPx - 6, csPB + 6, 1, csGRAY, 30);
    fdV := fdV + fdS;
    fdN := fdN + 1;
  END;
  TEXTOUT_P("a =", G1, 4, csPB + 6, 1, csGRAY, 34);
  // ---- the corner panel: the selected table, opposite the peak ----
  fdPx := csPR - 98;
  IF csPL + (fdMd - csFDL + 0.5) * fdBw > (csPL + csPR) / 2 THEN fdPx := csPL + 4; END;
  fdPy := csPT + 2;
  fdB := csRT(1) - csFSel;                   // b = R1 - a
  fdC := csCT(1) - csFSel;                   // c = C1 - a
  fdD := csRT(2) - fdC;                      // d = R2 - c
  RECT_P(G1, fdPx, fdPy, fdPx + 96, fdPy + 50, csORNG, csLORN);
  TEXTOUT_P("a = " + STRING(csFSel, 2, 0) + ":", G1, fdPx + 4, fdPy + 2, 1, csORNG, 88);
  TEXTOUT_P(STRING(csFSel, 2, 0), G1, fdPx + 4, fdPy + 14, 1, #000000h, 28);
  TEXTOUT_P(STRING(fdB, 2, 0), G1, fdPx + 34, fdPy + 14, 1, #000000h, 28);
  TEXTOUT_P(STRING(csRT(1), 2, 0), G1, fdPx + 64, fdPy + 14, 1, csORNG, 30);
  TEXTOUT_P(STRING(fdC, 2, 0), G1, fdPx + 4, fdPy + 26, 1, #000000h, 28);
  TEXTOUT_P(STRING(fdD, 2, 0), G1, fdPx + 34, fdPy + 26, 1, #000000h, 28);
  TEXTOUT_P(STRING(csRT(2), 2, 0), G1, fdPx + 64, fdPy + 26, 1, csORNG, 30);
  TEXTOUT_P(STRING(csCT(1), 2, 0), G1, fdPx + 4, fdPy + 38, 1, csORNG, 28);
  TEXTOUT_P(STRING(csCT(2), 2, 0), G1, fdPx + 34, fdPy + 38, 1, csORNG, 28);
  TEXTOUT_P(STRING(csN, 2, 0), G1, fdPx + 64, fdPy + 38, 1, csORNG, 30);
  // ---- readout ----
  fdP := csFW(csFSel - csFLo + 1);
  IF fdP < 0.0001 THEN fdS := "P < 0.0001"; ELSE fdS := "P = " + STRING(fdP, 2, 4); END;
  IF csFSel == csO(1) THEN
    fdS := fdS + ": the observed table";
  ELSE
    IF fdP <= fdObs * 1.0000001 THEN fdS := fdS + ": no likelier than yours"; ELSE fdS := fdS + ": likelier than yours"; END;
  END;
  TEXTOUT_P("a = " + STRING(csFSel, 2, 0) + ", " + fdS, G1, 6, 192, 1, csRED, 308);
  // beta 3: red + magenta adds up to the two-sided p, as an exact
  // fraction from the CAS when it fits ("p = 17/35 = 0.4857"; the
  // fraction was asked for when the plot opened, never here)
  IF csFish >= 0 THEN
    TEXTOUT_P("red + magenta: " + csXPText(32), G1, 6, 204, 1, csMAG, 308);
  END;
  BLIT_P(G0, 0, 0, G1);
END;

// The Fisher screen: a tap on a bar (or Left / Right) picks the
// table shown in the corner; soft keys as on every plot.
csFishPlot()
BEGIN
  LOCAL fpK, fpM, fpM1, fpGo, fpNx, fpSl;
  LOCAL fpA;
  IF csNR <> 2 OR csNC <> 2 THEN
    MSGBOX("Fisher's exact distribution is drawn for a 2 " + csTimes + " 2 table only.");
    RETURN 0;
  END;
  IF csFishW() == 0 THEN
    MSGBOX("More than 3,000 tables have these margins: too many to draw, and the " + csX2S + " approximation is excellent here.");
    RETURN 0;
  END;
  csXEnsure(0);                              // beta 3: the exact p, once, as the plot opens
  csFSel := csO(1);                          // start on the observed table
  fpGo := 1; fpNx := 0;
  REPEAT fpK := GETKEY; UNTIL fpK == -1;
  csDrain();
  WHILE fpGo == 1 DO
    csFishDraw();
    csStrip(6);
    REPEAT
      fpK := GETKEY;
      fpM := MOUSE;
      fpM1 := fpM(1);
    UNTIL fpK > -1 OR SIZE(fpM1) > 0;
    IF SIZE(fpM1) > 0 THEN
      csDrain();
      IF fpM1(2) >= 218 THEN
        // the soft-key strip: six 53-px slots, left to right
        fpSl := MIN(6, IP(fpM1(1) / 53.4) + 1);
        IF fpSl == 1 THEN fpNx := csPlotStep(6, -1); fpGo := 0; END;
        IF fpSl == 2 THEN fpNx := csPlotStep(6, 1); fpGo := 0; END;
        IF fpSl >= 3 AND fpSl <= 5 THEN fpNx := fpSl - 1; fpGo := 0; END;
        IF fpSl == 6 THEN
          IF csResMenu() == 0 THEN
            fpGo := 0;
          ELSE
            // the counts may have changed (x10, N / 10, Restore)
            IF csFishW() == 0 THEN
              MSGBOX("More than 3,000 tables have these margins now: too many to draw.");
              fpGo := 0;
            END;
            csXEnsure(0);                    // beta 3: a new table, a new exact p
            csFSel := csO(1);
          END;
        END;
      ELSE
        // a tap on (or under) the bars picks that table
        IF fpM1(1) >= csPL AND fpM1(1) <= csPR AND fpM1(2) >= csPT AND fpM1(2) <= csPB + 4 THEN
          fpA := csFDL + IP((fpM1(1) - csPL) / ((csPR - csPL) / (csFDH - csFDL + 1)));
          csFSel := MIN(MAX(fpA, csFDL), csFDH);
        END;
      END;
    ELSE
      IF fpK == 4 OR fpK == 30 THEN fpGo := 0; END;
      IF fpK == 7 THEN csFSel := MAX(csFDL, csFSel - 1); END;
      IF fpK == 8 THEN csFSel := MIN(csFDH, csFSel + 1); END;
    END;
  END;
  REPEAT fpK := GETKEY; UNTIL fpK == -1;
  RETURN fpNx;
END;


// ------------------------------------------------------------
// beta 2: csView runs the results pages and the plots as one set
// of screens.  Each screen routine returns the number of the next
// (1 results, 2 bars, 3 mosaic, 4 curve, 5 residuals, 6 Fisher; 0
// back to the main menu), so a soft key on any of them can hand
// over to any other without the main menu in between.
// ------------------------------------------------------------
csView(vwW)
BEGIN
  LOCAL vwN;
  vwN := vwW;
  WHILE vwN > 0 DO
    CASE
      IF vwN == 1 THEN vwN := csResults(); END;
      IF vwN == 2 THEN vwN := csPlotLoop(1); END;
      IF vwN == 3 THEN vwN := csPlotLoop(2); END;
      IF vwN == 4 THEN vwN := csCurve(); END;
      IF vwN == 5 THEN vwN := csPlotLoop(3); END;
      DEFAULT vwN := csFishPlot();
    END;
  END;
END;


// ------------------------------------------------------------
// EXAMPLES, ALPHA, HELP
// ------------------------------------------------------------

// beta 2: one row of the Simpson page -- a label, a bar on a 0 to
// 25% track (60 px), and the percentage sbD / sbN.
csSmBar(sbX, sbY, sbLbl, sbD, sbN, sbCol, sbLW)
BEGIN
  LOCAL sbP, sbL, sbB, sbW;
  sbP := 100 * sbD / sbN;
  sbL := MIN(60, IP(sbP / 25 * 60 + 0.5));   // bar length in pixels
  sbB := sbX + sbLW;                         // where the bar starts
  sbW := sbLW - 2;                           // room for the label
  TEXTOUT_P(sbLbl, G1, sbX, sbY, 1, sbCol, sbW);
  RECT_P(G1, sbB, sbY + 1, sbB + 60, sbY + 9, csGRID, csGRID);
  IF sbL > 0 THEN RECT_P(G1, sbB, sbY + 1, sbB + sbL, sbY + 9, sbCol, sbCol); END;
  TEXTOUT_P(STRING(sbP, 2, 1) + "%", G1, sbB + 64, sbY, 1, sbCol, 46);
END;

// beta 2: Simpson's paradox, drawn from examples 1 and 2.  Top:
// the two tables in the file -- death sentences by defendant's
// race, and by victim's race -- as row percentages.  Below: the
// same 674 cases split by BOTH races (Radelet & Pierce 1991 via
// Agresti, the three-way table that examples 1 and 2 are the
// margins of: white victim 53/414 white def., 11/37 black def.;
// black victim 0/16 and 4/139).  Overall white defendants were
// sentenced to death more often; within each victim's race black
// defendants were.  Blue = white defendant, magenta = black.
csSimpson()
BEGIN
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  TEXTOUT_P("Simpson's paradox (examples 1 + 2)", G1, 6, 4, 2, #000000h, 308);
  // ---- examples 1 and 2: {53, 430, 15, 176} and {64, 451, 4, 155} ----
  TEXTOUT_P("Overall, by DEFENDANT", G1, 6, 26, 1, csGRAY, 150);
  TEXTOUT_P("Overall, by VICTIM", G1, 166, 26, 1, csGRAY, 150);
  csSmBar(6, 40, "white", 53, 53 + 430, csBLUE, 34);
  csSmBar(6, 54, "black", 15, 15 + 176, csMAG, 34);
  csSmBar(166, 40, "white", 64, 64 + 451, csGRN, 34);
  csSmBar(166, 54, "black", 4, 4 + 155, csORNG, 34);
  TEXTOUT_P("death: 53/483 vs 15/191", G1, 6, 68, 1, csGRAY, 150);
  TEXTOUT_P("death: 64/515 vs 4/159", G1, 166, 68, 1, csGRAY, 150);
  // ---- the same cases, split by both races ----
  TEXTOUT_P("Same victim's race, compare the defendants:", G1, 6, 84, 1, csMAG, 308);
  csSmBar(6, 98, "white vic., white def.", 53, 53 + 414, csBLUE, 146);
  csSmBar(6, 111, "white vic., black def.", 11, 11 + 37, csMAG, 146);
  csSmBar(6, 126, "black vic., white def.", 0, 0 + 16, csBLUE, 146);
  csSmBar(6, 139, "black vic., black def.", 4, 4 + 139, csMAG, 146);
  // ---- the reversal, in words ----
  TEXTOUT_P("Overall, white defendants got death MORE often.", G1, 6, 156, 1, #000000h, 308);
  TEXTOUT_P("Within each victim's race, black defendants did.", G1, 6, 169, 1, #000000h, 308);
  TEXTOUT_P("Why: a white victim made death far likelier, and", G1, 6, 186, 1, csGRAY, 308);
  TEXTOUT_P("most victims were of the killer's own race.", G1, 6, 199, 1, csGRAY, 308);
  TEXTOUT_P("Any key or tap = menu", G1, 6, 222, 1, csFOOT, 308);
  BLIT_P(G0, 0, 0, G1);
  csWaitAny();
END;

// Four built-in tables.  The death-penalty data are Florida
// murder cases 1976-87 (Radelet & Pierce 1991, as tabulated by
// Agresti): defendant's race alone shows little; victim's race
// shows a strong effect, and conditioning on it reverses the
// first table (Simpson's paradox).  The party data are the 1991
// General Social Survey (Agresti, Introduction to Categorical
// Data Analysis, Table 2.5), once as 2 x 3 and once transposed.
// beta 2: a fifth item draws the Simpson's paradox page.
csExamples()
BEGIN
  LOCAL exI;
  exI := 0;
  // beta 3b: the first two lines shortened (CHOOSE cut "...defendant's
  // rac" at the box's edge); the table's label names the race variable
  CHOOSE(exI, "Example tables", {
    "Death penalty " + csTimes + " defendant",
    "Death penalty " + csTimes + " victim",
    "Gender " + csTimes + " party ID (2 " + csTimes + " 3)",
    "Party ID " + csTimes + " gender (3 " + csTimes + " 2)",
    "Simpson (examples 1 + 2)"});
  IF exI == 0 THEN RETURN; END;
  IF exI == 5 THEN csSimpson(); RETURN; END;  // a page; the table is not changed
  CASE
    IF exI == 1 THEN
      csNR := 2; csNC := 2; csO := {53, 430, 15, 176};
      csLbl := "Death penalty by defendant's race, FL 1976-87";
      csLbl2 := "R1 white def., R2 black def.; C1 death, C2 not";
    END;
    IF exI == 2 THEN
      csNR := 2; csNC := 2; csO := {64, 451, 4, 155};
      csLbl := "Death penalty by victim's race, FL 1976-87";
      csLbl2 := "R1 white victim, R2 black; C1 death, C2 not";
    END;
    IF exI == 3 THEN
      csNR := 2; csNC := 3; csO := {762, 327, 468, 484, 239, 477};
      csLbl := "Party ID by gender (GSS 1991, Agresti)";
      csLbl2 := "R1 female, R2 male; C1 Dem, C2 Ind, C3 Rep";
    END;
    DEFAULT
      csNR := 3; csNC := 2; csO := {762, 484, 327, 239, 468, 477};
      csLbl := "Gender by party ID (same data, transposed)";
      csLbl2 := "R1 Dem, R2 Ind, R3 Rep; C1 female, C2 male";
  END;
  csSel := 1;
  csCompute();
  csKeep := csCopy(csO);                    // beta 2: what Restore puts back
  csView(1);                                // beta 2: the results pages, with the soft keys
END;

csSetAlpha()
BEGIN
  LOCAL alV, alLbl;
  alV := csAlpha;
  alLbl := csAl + ":";
  IF INPUT({alV}, "Significance level", {alLbl}, {"Between 0 and 1; 0.05 is usual, 0.01 is strict"}) == 1 THEN
    IF alV <= 0 OR alV >= 1 THEN
      MSGBOX(csAl + " must be strictly between 0 and 1.");
      RETURN;
    END;
    csAlpha := alV;
    IF csHas == 1 THEN csCrit := CHISQUARE_ICDF(csDF, 1 - csAlpha); END;
  END;
END;

// beta 2, help page 4: why df = (r - 1)(c - 1), drawn on the
// current table's own shape.  The totals (orange) are fixed; the
// white cells, (r - 1)(c - 1) of them, can be anything; every grey
// cell is then forced -- its row or column total minus the cells
// beside it.  With a computed table the counts are shown.
csDfPage()
BEGIN
  LOCAL dpR, dpC, dpX, dpY, dpI, dpS;
  LOCAL dpX0, dpY0, dpW, dpH, dpXT, dpYT;
  LOCAL dpDf;
  dpX0 := 64; dpY0 := 50; dpW := 56; dpH := 26;
  dpXT := dpX0 + csNC * dpW;                 // the totals column
  dpYT := dpY0 + csNR * dpH;                 // the totals row
  dpDf := (csNR - 1) * (csNC - 1);
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 4, 4, 315, 235, csBLUE, csLBLU);
  TEXTOUT_P("Why df = (rows - 1)(cols - 1)", G1, 12, 8, 2, csBLUE, 296);
  // ---- header row ----
  FOR dpC FROM 1 TO csNC DO
    dpX := dpX0 + (dpC - 1) * dpW;
    RECT_P(G1, dpX, 32, dpX + dpW, 48, csGRAY, csLGRY);
    TEXTOUT_P("Col " + STRING(dpC, 2, 0), G1, dpX + 12, 35, 1, csColC(dpC), 44);
  END;
  RECT_P(G1, dpXT, 32, dpXT + 56, 48, csORNG, csLORN);
  TEXTOUT_P("Total", G1, dpXT + 12, 35, 1, csORNG, 44);
  // ---- the cells: white = free, grey = forced ----
  FOR dpR FROM 1 TO csNR DO
    dpY := dpY0 + (dpR - 1) * dpH;
    RECT_P(G1, 12, dpY, dpX0 - 2, dpY + dpH, csGRAY, csLGRY);
    TEXTOUT_P("Row " + STRING(dpR, 2, 0), G1, 16, dpY + 8, 1, #000000h, 44);
    FOR dpC FROM 1 TO csNC DO
      dpX := dpX0 + (dpC - 1) * dpW;
      dpI := csIx(dpR, dpC);
      IF dpR < csNR AND dpC < csNC THEN
        RECT_P(G1, dpX, dpY, dpX + dpW, dpY + dpH, csBLUE, #FFFFFFh);
        dpS := "free";
        IF csHas == 1 THEN dpS := STRING(csO(dpI), 2, 0); END;
        TEXTOUT_P(dpS, G1, dpX + 8, dpY + 5, 2, csBLUE, 44);
      ELSE
        RECT_P(G1, dpX, dpY, dpX + dpW, dpY + dpH, csGRAY, #D2D2D2h);
        dpS := "forced";
        IF csHas == 1 THEN dpS := "= " + STRING(csO(dpI), 2, 0); END;
        TEXTOUT_P(dpS, G1, dpX + 6, dpY + 8, 1, #3C3C3Ch, 48);
      END;
    END;
    RECT_P(G1, dpXT, dpY, dpXT + 56, dpY + dpH, csORNG, csLORN);
    dpS := "fixed";
    IF csHas == 1 THEN dpS := STRING(csRT(dpR), 2, 0); END;
    TEXTOUT_P(dpS, G1, dpXT + 6, dpY + 8, 1, csORNG, 48);
  END;
  // ---- the totals row ----
  RECT_P(G1, 12, dpYT, dpX0 - 2, dpYT + dpH, csORNG, csLORN);
  TEXTOUT_P("Total", G1, 16, dpYT + 8, 1, csORNG, 44);
  FOR dpC FROM 1 TO csNC DO
    dpX := dpX0 + (dpC - 1) * dpW;
    RECT_P(G1, dpX, dpYT, dpX + dpW, dpYT + dpH, csORNG, csLORN);
    dpS := "fixed";
    IF csHas == 1 THEN dpS := STRING(csCT(dpC), 2, 0); END;
    TEXTOUT_P(dpS, G1, dpX + 6, dpYT + 8, 1, csORNG, 48);
  END;
  RECT_P(G1, dpXT, dpYT, dpXT + 56, dpYT + dpH, csORNG, csORNG);
  dpS := "N";
  IF csHas == 1 THEN dpS := "N = " + STRING(csN, 2, 0); END;
  TEXTOUT_P(dpS, G1, dpXT + 4, dpYT + 8, 1, #FFFFFFh, 50);
  // ---- the words ----
  dpY := dpYT + dpH + 8;
  dpS := "df = (" + STRING(csNR, 2, 0) + " - 1)(" + STRING(csNC, 2, 0) + " - 1) = " + STRING(dpDf, 2, 0) + ": fill ";
  IF dpDf == 1 THEN dpS := dpS + "this white cell"; ELSE dpS := dpS + "these " + STRING(dpDf, 2, 0) + " white cells"; END;
  TEXTOUT_P(dpS, G1, 12, dpY, 1, csBLUE, 296);
  TEXTOUT_P("and the rest follow.  With every total fixed, a", G1, 12, dpY + 13, 1, #000000h, 296);
  TEXTOUT_P("grey cell is its row (or column) total minus the", G1, 12, dpY + 26, 1, #000000h, 296);
  TEXTOUT_P("cells beside it: only df counts are free to vary.", G1, 12, dpY + 39, 1, #000000h, 296);
  TEXTOUT_P("Any key = page 5", G1, 12, 222, 1, csFOOT, 296);   // beta 3: was "= menu"
  BLIT_P(G0, 0, 0, G1);
END;

csHelp()
BEGIN
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 4, 4, 315, 235, csBLUE, csLBLU);
  TEXTOUT_P(csX2S + " for a two-way table", G1, 12, 8, 2, csBLUE, 296);
  TEXTOUT_P("Pick a table shape, then tap each cell and type", G1, 12, 30, 1, #000000h, 296);
  TEXTOUT_P("its OBSERVED count.  Run computes everything.", G1, 12, 43, 1, #000000h, 296);
  TEXTOUT_P("Expected count E = row total x col total / N:", G1, 12, 60, 1, csBLUE, 296);
  TEXTOUT_P("what the cell would hold if the two variables", G1, 12, 73, 1, csBLUE, 296);
  TEXTOUT_P("were independent.  " + csX2S + " adds (O - E)" + csSqS + " / E over", G1, 12, 86, 1, csBLUE, 296);
  TEXTOUT_P("the cells: a big " + csX2S + " means a big surprise.", G1, 12, 99, 1, csBLUE, 296);
  TEXTOUT_P("df = (rows - 1)(cols - 1).  p is the right tail", G1, 12, 116, 1, csMAG, 296);
  TEXTOUT_P("of the " + csX2S + " curve, drawn by the curve plot.", G1, 12, 129, 1, csMAG, 296);
  // beta 3d: the assumption as the pages apply it.  Beta 3c said "5 or
  // more (at least 80% of cells, none below 1)", but pages 1, 3 and 5
  // flag ANY expected count below 5 (page 5: "chi-square is shaky"),
  // so a 2 x 3 with one E of 4.5 passed the help's rule and failed the
  // page's.  Which rule to teach is Roger's decision (fleet review 19
  // Oct 2026, D8); until then the help states the rule the code uses.
  TEXTOUT_P("Assumption: every expected count is 5 or more", G1, 12, 146, 1, csRED, 296);
  TEXTOUT_P("(the pages flag any E below 5).  If not: use", G1, 12, 159, 1, csRED, 296);
  TEXTOUT_P("Fisher's exact (2x2) or merge sparse categories.", G1, 12, 172, 1, csRED, 296);
  TEXTOUT_P("Pages: O and E, percentages, the test, the cell", G1, 12, 189, 1, csGRAY, 296);
  TEXTOUT_P("contributions, meaning, 2x2, exact answers.", G1, 12, 202, 1, csGRAY, 296);
  TEXTOUT_P("Any key = page 2", G1, 12, 222, 1, csFOOT, 296);
  BLIT_P(G0, 0, 0, G1);
  csWaitAny();

  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 4, 4, 315, 235, csBLUE, csLBLU);
  TEXTOUT_P("Conventions, plots and Home", G1, 12, 8, 2, csBLUE, 296);
  TEXTOUT_P("Rows = the groups (explanatory variable), cols =", G1, 12, 30, 1, #000000h, 296);
  TEXTOUT_P("the outcome.  Row % compares the groups and is", G1, 12, 43, 1, #000000h, 296);
  TEXTOUT_P("the percentage to report; col % profiles an", G1, 12, 56, 1, #000000h, 296);
  TEXTOUT_P("outcome; % of N is the joint distribution.", G1, 12, 69, 1, #000000h, 296);
  TEXTOUT_P("2x2 extras take column 1 as the outcome: p1, p2,", G1, 12, 86, 1, csMAG, 296);
  TEXTOUT_P("z, risk difference, RR, OR (95% CI), Yates, Fisher.", G1, 12, 99, 1, csMAG, 296);
  TEXTOUT_P("Mosaic plot: bar width = row size, tile height =", G1, 12, 116, 1, csBLUE, 296);
  TEXTOUT_P("row %; blue = more than expected, red = fewer.", G1, 12, 129, 1, csBLUE, 296);
  TEXTOUT_P("Bars plot: solid observed beside pale expected.", G1, 12, 142, 1, csBLUE, 296);
  TEXTOUT_P("Home after a run: CS_X2, CS_DF, CS_P, CS_V, CS_N,", G1, 12, 159, 1, csGRAY, 296);
  TEXTOUT_P("CS_OBS, CS_EXP, CS_RES (adjusted residuals).", G1, 12, 172, 1, csGRAY, 296);
  TEXTOUT_P("The built-in route: Inference app > " + csX2S + " test >", G1, 12, 189, 1, csGRAY, 296);
  TEXTOUT_P("2-way, matrix ObsMat; this program adds the rest.", G1, 12, 202, 1, csGRAY, 296);
  TEXTOUT_P("Any key = page 3", G1, 12, 222, 1, csFOOT, 296);
  BLIT_P(G0, 0, 0, G1);
  csWaitAny();

  // ---- beta 2: page 3, the touch features and the new plots ----
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 4, 4, 315, 235, csBLUE, csLBLU);
  TEXTOUT_P("Touch, soft keys and the new plots", G1, 12, 8, 2, csBLUE, 296);
  TEXTOUT_P("Soft keys on the results pages and plots: Prev", G1, 12, 30, 1, #000000h, 296);
  TEXTOUT_P("and Next turn the page (or go to the next plot);", G1, 12, 43, 1, #000000h, 296);
  TEXTOUT_P("Bars, Mosaic, Curve jump there.  Menu: " + csTimes + "10 N,", G1, 12, 56, 1, #000000h, 296);
  TEXTOUT_P("N / 10 (V stays, p moves), Restore the counts.", G1, 12, 69, 1, #000000h, 296);
  TEXTOUT_P("Bars: drag a SOLID bar up or down -- every pale", G1, 12, 84, 1, csBLUE, 296);
  TEXTOUT_P("E bar moves, " + csX2S + " and p follow; Up / Down do it too.", G1, 12, 97, 1, csBLUE, 296);
  TEXTOUT_P("Mosaic: Morph (or +) melts O into E: " + csH0 + " itself.", G1, 12, 110, 1, csBLUE, 296);
  TEXTOUT_P("Curve: drag the red dotted line to set " + csAl + "; near", G1, 12, 125, 1, csMAG, 296);
  TEXTOUT_P(".001 .005 .01 .02 .05 .10 .20 it snaps to them;", G1, 12, 138, 1, csMAG, 296);
  TEXTOUT_P(CHAR(8592) + " " + CHAR(8594) + " step that ladder; Reset = 0.05.", G1, 12, 151, 1, csMAG, 296);
  TEXTOUT_P("Results page 1: tap a cell to see how E is made.", G1, 12, 166, 1, #000000h, 296);
  TEXTOUT_P("Residual plot: one bar per cell, dotted at +/-2.", G1, 12, 179, 1, csGRAY, 296);
  TEXTOUT_P("Fisher plot (2" + csTimes + "2): every table with these", G1, 12, 192, 1, csGRAY, 296);
  TEXTOUT_P("margins; red = yours, red + magenta = p.", G1, 12, 205, 1, csGRAY, 296);
  TEXTOUT_P("Any key = page 4", G1, 12, 222, 1, csFOOT, 296);
  BLIT_P(G0, 0, 0, G1);
  csWaitAny();

  // ---- beta 2: page 4, why df = (r - 1)(c - 1) ----
  csDfPage();
  csWaitAny();

  // ---- beta 3: page 5, the exact answers and the badge ----
  DIMGROB_P(G1, 320, 240, #FFFFFFh);
  RECT_P(G1, 4, 4, 315, 235, csBLUE, csLBLU);
  TEXTOUT_P("Exact answers (CAS)", G1, 12, 8, 2, csBLUE, 296);
  TEXTOUT_P("Results page 6, the last page and the Fisher", G1, 12, 30, 1, #000000h, 296);
  TEXTOUT_P("plot ask the CAS for EXACT answers on your own", G1, 12, 43, 1, #000000h, 296);
  TEXTOUT_P("counts: " + csX2S + " as one fraction (from N(ad - bc)" + csSqS, G1, 12, 56, 1, #000000h, 296);
  TEXTOUT_P("/ (r1 r2 c1 c2) for a 2" + csTimes + "2, or from the sum of", G1, 12, 69, 1, #000000h, 296);
  TEXTOUT_P("(O - E)" + csSqS + "/E), and Fisher's p as a fraction.", G1, 12, 82, 1, #000000h, 296);
  TEXTOUT_P("Fisher's test is COUNTING: with the totals", G1, 12, 99, 1, csMAG, 296);
  TEXTOUT_P("fixed, top-left count a happens in C(r1,a)", G1, 12, 112, 1, csMAG, 296);
  TEXTOUT_P("C(r2,c1-a) of the C(N,c1) equally likely ways.", G1, 12, 125, 1, csMAG, 296);
  TEXTOUT_P("Add the tables no likelier than yours.  Tea", G1, 12, 138, 1, csMAG, 296);
  TEXTOUT_P("tasting [[3,1],[1,3]]: 34 of 70, p = 17/35.", G1, 12, 151, 1, csMAG, 296);
  TEXTOUT_P("Badge 'exact (CAS)': the CAS worked it out.", G1, 12, 168, 1, csGRAY, 296);
  TEXTOUT_P("'decimal': it could not, or the fraction is", G1, 12, 181, 1, csGRAY, 296);
  TEXTOUT_P("too long: you see the program's own decimal.", G1, 12, 194, 1, csGRAY, 296);
  TEXTOUT_P("Any key = menu", G1, 12, 222, 1, csFOOT, 296);
  BLIT_P(G0, 0, 0, G1);
  csWaitAny();
END;


// ------------------------------------------------------------
// INIT AND MAIN
// ------------------------------------------------------------
csInit()
BEGIN
  csBLUE := #003CC8h; csMAG := #C800C8h; csRED := #FF0000h;
  csGRN := #008C3Ch; csORNG := #FF4500h; csGRAY := #5A5A5Ah;
  csFOOT := #1E90FFh; csGRID := #E1E4EBh; csLBLU := #EBF2FFh;
  csLMAG := #FAEBFAh; csLRED := #FFEBEBh; csLGRN := #E8F6EBh;
  csLORN := #FFF0DCh; csLGRY := #F0F0F0h;
  csMBLU := #9EC9F0h; csMRED := #F5A0A0h;
  // grid: label column 4..46, data from 48 in 64-px columns, a 76-px
  // totals column; header 28..48, data rows from 50 in 44-px rows,
  // totals row 32 px
  gLX := 4; gX0 := 48; gCW := 64; gY0 := 50; gRH := 44; gHY := 28;
  gHH := 20; gTH := 32;
  // plot box.  beta 2: the bottom is 174 (was 186) -- every plot's
  // text under the box must end above the soft-key strip at y 218
  csPL := 40; csPR := 306; csPT := 26; csPB := 174;
  csTX1 := MAKELIST(0, K, 1, 6, 1); csTY1 := MAKELIST(0, K, 1, 6, 1);
  csTX2 := MAKELIST(0, K, 1, 6, 1); csTY2 := MAKELIST(0, K, 1, 6, 1);
  // an empty 2 x 2 to start with
  csNR := 2; csNC := 2; csO := {0, 0, 0, 0};
  csHas := 0; csSel := 1; csAlpha := 0.05;
  csLbl := ""; csLbl2 := ""; csFoot := "";
  csFish := -1; csHal := 0;
  // beta 2: the touch features start idle
  csKeep := {0, 0, 0, 0};
  csYF := 0; csYS := 1; csYT := 1; csBW := 1; csMT := 0;
  csTapC := 0; csDgBad := 0;
  csFW := {1}; csFLo := 0; csFHi := 0; csFSel := 0;
  csFDL := 0; csFDH := 0;
  csCvX := 1; csCvY := 1; csCvS := 1; csCvDc := 0;
  csCvP := {0}; csCvW := 1;
  // beta 3: the CAS cache starts empty (no table yet)
  csCasOK := 0; csXKey := ""; csXFSt := 0; csXCSt := 0;
  csXFr := ""; csXSum := ""; csXX2 := ""; csXAd := "";
  csXPr := ""; csXS := ""; csXT := ""; csXDg := 0;
  // display glyphs: chi (U+03C7) + superscript two (U+00B2), and x (U+00D7)
  csX2S := CHAR(967) + CHAR(178);
  csSqS := CHAR(178);
  csTimes := CHAR(215);
  // H with subscript 0 and 1 (U+2080, U+2081); alpha (U+03B1)
  csH0 := "H" + CHAR(8320);
  csH1 := "H" + CHAR(8321);
  csAl := CHAR(945);
END;

EXPORT ChiTable()
BEGIN
  LOCAL mnI, mnRun;
  csInit();
  mnRun := 1;
  WHILE mnRun == 1 DO
    mnI := 0;
    CHOOSE(mnI, csX2S + " for a two-way table", {
      "New table: 2 " + csTimes + " 2",
      "New table: 2 " + csTimes + " 3",
      "New table: 3 " + csTimes + " 2",
      "Edit the counts",
      "Results: expected, %, " + csX2S + ", p",
      "Plot: observed vs expected",
      "Plot: mosaic of the table",
      "Plot: " + csX2S + " curve and p",
      "Plot: adjusted residuals",
      "Plot: Fisher exact (2 " + csTimes + " 2)",
      "Set " + csAl + " (now " + csAStr() + ")",
      "Examples",
      "Help",
      "Quit"});
    // beta 2: 14 items (CHOOSE shows 10-14 as A-E); the results and
    // every plot open through csView, so their soft keys can move
    // between them.  The index variable is tested, never CHOOSE's
    // return value.
    CASE
      IF mnI == 0 OR mnI == 14 THEN mnRun := 0; END;
      IF mnI == 1 THEN IF csNewTable(2, 2) == 1 THEN csView(1); END; END;
      IF mnI == 2 THEN IF csNewTable(2, 3) == 1 THEN csView(1); END; END;
      IF mnI == 3 THEN IF csNewTable(3, 2) == 1 THEN csView(1); END; END;
      IF mnI == 4 THEN IF csEntry() == 1 THEN csView(1); END; END;
      IF mnI >= 5 AND mnI <= 10 AND csHas == 0 THEN
        MSGBOX("No table yet.  Pick a New table shape and type the counts, or load one from Examples.");
      END;
      // results = screen 1, bars 2, mosaic 3, curve 4, residuals 5, Fisher 6
      IF mnI >= 5 AND mnI <= 10 AND csHas == 1 THEN csView(mnI - 4); END;
      IF mnI == 11 THEN csSetAlpha(); END;
      IF mnI == 12 THEN csExamples(); END;
      IF mnI == 13 THEN csHelp(); END;
    END;
  END;
  // Hand the summary back as the program's value: Home shows it
  // in the history, and no Terminal screen opens on the way out.
  // The numbers stay in CS_X2 CS_DF CS_P CS_V CS_N CS_OBS CS_EXP CS_RES.
  IF csHas == 1 THEN
    RETURN "ChiTable: " + csX2S + "(" + STRING(csDF, 2, 0) + ", N = " + STRING(csN, 2, 0) + ") = " + STRING(ROUND(csX2, 4), 2, 4) + ", " + csPStr(csP) + ", V = " + STRING(ROUND(csV, 4), 2, 4);
  END;
  RETURN "ChiTable: no table";
END;
