@inflexa-ai/harness 0.42.1 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/README.md +2 -2
  2. package/dist/agents/conversation-agent.js +5 -1
  3. package/dist/agents/sandbox/drug-repurposing-agent.js +1 -0
  4. package/dist/agents/sandbox/shared.d.ts +1 -6
  5. package/dist/agents/sandbox/shared.js +3 -16
  6. package/dist/agents/sandbox/types.d.ts +1 -1
  7. package/dist/app/chat-turn.js +13 -1
  8. package/dist/config/environment-stores.d.ts +4 -2
  9. package/dist/contracts/chat-events.d.ts +9 -1
  10. package/dist/contracts/chat-frame.d.ts +3 -3
  11. package/dist/contracts/chat-frame.js +8 -3
  12. package/dist/contracts/chat-parts.d.ts +4 -1
  13. package/dist/contracts/data-profile.d.ts +4 -6
  14. package/dist/contracts/index.d.ts +1 -1
  15. package/dist/contracts/report-blocks.d.ts +1364 -75
  16. package/dist/contracts/report-blocks.js +221 -29
  17. package/dist/contracts/report-reference.js +1 -1
  18. package/dist/contracts/schemas/chat-events.d.ts +13 -0
  19. package/dist/contracts/schemas/chat-events.js +5 -0
  20. package/dist/contracts/schemas/chat-parts.d.ts +2 -2
  21. package/dist/contracts/schemas/index.d.ts +1 -1
  22. package/dist/contracts/schemas/index.js +1 -1
  23. package/dist/execution/input-signature.d.ts +7 -13
  24. package/dist/execution/input-signature.js +0 -0
  25. package/dist/execution/run-canceler.d.ts +3 -2
  26. package/dist/execution/run-canceler.js +3 -2
  27. package/dist/execution/run-event-stream.d.ts +16 -2
  28. package/dist/execution/run-event-stream.js +54 -3
  29. package/dist/index.d.ts +1 -8
  30. package/dist/index.js +5 -16
  31. package/dist/input-scan/enrich.d.ts +0 -2
  32. package/dist/input-scan/enrich.js +0 -1
  33. package/dist/lib/metrics.js +6 -7
  34. package/dist/lib/otel-spans.d.ts +4 -9
  35. package/dist/lib/otel-spans.js +3 -8
  36. package/dist/loop/run-agent.d.ts +4 -2
  37. package/dist/loop/run-agent.js +38 -23
  38. package/dist/memory/thread-history.d.ts +1 -1
  39. package/dist/prompts/report-session.d.ts +1 -1
  40. package/dist/prompts/report-session.js +17 -0
  41. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  42. package/dist/prompts/sandbox/drug-repurposing-agent.js +7 -0
  43. package/dist/provenance/exec-frame.d.ts +1 -1
  44. package/dist/report-model/authoring-grammar.d.ts +416 -17
  45. package/dist/report-model/authoring-grammar.js +25 -4
  46. package/dist/report-model/block-walk.d.ts +20 -4
  47. package/dist/report-model/block-walk.js +64 -7
  48. package/dist/report-model/draft-finish.d.ts +6 -2
  49. package/dist/report-model/draft-finish.js +6 -1
  50. package/dist/report-model/production-resolver.d.ts +8 -0
  51. package/dist/report-model/production-resolver.js +1 -1
  52. package/dist/report-model/validate.d.ts +20 -10
  53. package/dist/report-model/validate.js +29 -13
  54. package/dist/report-render/chart-export.d.ts +52 -0
  55. package/dist/report-render/chart-export.js +192 -0
  56. package/dist/report-render/chart-presets.d.ts +46 -47
  57. package/dist/report-render/chart-presets.js +31 -101
  58. package/dist/report-render/chart-renderers.d.ts +163 -0
  59. package/dist/report-render/chart-renderers.js +1055 -0
  60. package/dist/report-render/chart-toolbox.d.ts +124 -0
  61. package/dist/report-render/chart-toolbox.js +595 -0
  62. package/dist/report-render/chart.d.ts +60 -27
  63. package/dist/report-render/chart.js +2323 -524
  64. package/dist/report-render/design.d.ts +476 -16
  65. package/dist/report-render/design.js +390 -56
  66. package/dist/report-render/figures/common.d.ts +321 -0
  67. package/dist/report-render/figures/common.js +759 -0
  68. package/dist/report-render/figures/dendrogram.d.ts +36 -0
  69. package/dist/report-render/figures/dendrogram.js +130 -0
  70. package/dist/report-render/figures/dense.d.ts +111 -0
  71. package/dist/report-render/figures/dense.js +204 -0
  72. package/dist/report-render/figures/dotplot.d.ts +28 -0
  73. package/dist/report-render/figures/dotplot.js +234 -0
  74. package/dist/report-render/figures/embedding.d.ts +43 -0
  75. package/dist/report-render/figures/embedding.js +360 -0
  76. package/dist/report-render/figures/equal-units.d.ts +101 -0
  77. package/dist/report-render/figures/equal-units.js +215 -0
  78. package/dist/report-render/figures/forest.d.ts +20 -0
  79. package/dist/report-render/figures/forest.js +398 -0
  80. package/dist/report-render/figures/gsea.d.ts +30 -0
  81. package/dist/report-render/figures/gsea.js +391 -0
  82. package/dist/report-render/figures/heatmap.d.ts +40 -0
  83. package/dist/report-render/figures/heatmap.js +395 -0
  84. package/dist/report-render/figures/index.d.ts +135 -0
  85. package/dist/report-render/figures/index.js +100 -0
  86. package/dist/report-render/figures/km.d.ts +16 -0
  87. package/dist/report-render/figures/km.js +406 -0
  88. package/dist/report-render/figures/label-room.d.ts +130 -0
  89. package/dist/report-render/figures/label-room.js +341 -0
  90. package/dist/report-render/figures/locuszoom.d.ts +50 -0
  91. package/dist/report-render/figures/locuszoom.js +675 -0
  92. package/dist/report-render/figures/lollipop.d.ts +16 -0
  93. package/dist/report-render/figures/lollipop.js +247 -0
  94. package/dist/report-render/figures/ma.d.ts +20 -0
  95. package/dist/report-render/figures/ma.js +114 -0
  96. package/dist/report-render/figures/manhattan.d.ts +60 -0
  97. package/dist/report-render/figures/manhattan.js +343 -0
  98. package/dist/report-render/figures/oncoprint.d.ts +41 -0
  99. package/dist/report-render/figures/oncoprint.js +420 -0
  100. package/dist/report-render/figures/pca.d.ts +37 -0
  101. package/dist/report-render/figures/pca.js +241 -0
  102. package/dist/report-render/figures/qq.d.ts +30 -0
  103. package/dist/report-render/figures/qq.js +182 -0
  104. package/dist/report-render/figures/roc.d.ts +17 -0
  105. package/dist/report-render/figures/roc.js +116 -0
  106. package/dist/report-render/figures/sankey.d.ts +48 -0
  107. package/dist/report-render/figures/sankey.js +353 -0
  108. package/dist/report-render/figures/upset.d.ts +60 -0
  109. package/dist/report-render/figures/upset.js +316 -0
  110. package/dist/report-render/figures/volcano.d.ts +25 -0
  111. package/dist/report-render/figures/volcano.js +166 -0
  112. package/dist/report-render/hybrid-svg.d.ts +75 -0
  113. package/dist/report-render/hybrid-svg.js +504 -0
  114. package/dist/report-render/number-format.d.ts +19 -0
  115. package/dist/report-render/number-format.js +25 -1
  116. package/dist/report-render/page.d.ts +41 -4
  117. package/dist/report-render/page.js +247 -12
  118. package/dist/report-render/render.d.ts +13 -4
  119. package/dist/report-render/render.js +37 -6
  120. package/dist/report-render/types.d.ts +23 -2
  121. package/dist/report-render/value-bridge.d.ts +38 -3
  122. package/dist/report-render/value-bridge.js +137 -6
  123. package/dist/report-render/views/chart-view.d.ts +42 -6
  124. package/dist/report-render/views/chart-view.js +70 -9
  125. package/dist/report-render/views/page-view.js +9 -10
  126. package/dist/runtime/assemble.js +2 -2
  127. package/dist/sandbox/client.d.ts +23 -32
  128. package/dist/sandbox/client.js +3 -11
  129. package/dist/sandbox/create-sandbox.d.ts +9 -24
  130. package/dist/sandbox/create-sandbox.js +15 -30
  131. package/dist/sandbox/docker-client.d.ts +12 -22
  132. package/dist/sandbox/docker-client.js +29 -43
  133. package/dist/sandbox/exec.d.ts +58 -0
  134. package/dist/sandbox/exec.js +197 -0
  135. package/dist/sandbox/identity.d.ts +5 -5
  136. package/dist/sandbox/identity.js +7 -10
  137. package/dist/sandbox/k8s-client.d.ts +4 -10
  138. package/dist/sandbox/k8s-client.js +5 -12
  139. package/dist/sandbox/liveness.d.ts +5 -12
  140. package/dist/sandbox/liveness.js +5 -12
  141. package/dist/sandbox/notification-sweep.d.ts +4 -7
  142. package/dist/sandbox/notification-sweep.js +4 -7
  143. package/dist/sandbox/reaper.d.ts +3 -3
  144. package/dist/sandbox/reaper.js +3 -3
  145. package/dist/sandbox/sandbox-error.d.ts +10 -11
  146. package/dist/sandbox/sandbox-error.js +10 -11
  147. package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
  148. package/dist/sandbox/sandbox-step-translate.js +1 -1
  149. package/dist/sandbox/types.d.ts +31 -99
  150. package/dist/sandbox/types.js +17 -84
  151. package/dist/state/active-sandboxes.d.ts +7 -32
  152. package/dist/state/active-sandboxes.js +11 -46
  153. package/dist/state/index.d.ts +1 -2
  154. package/dist/state/index.js +1 -1
  155. package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.d.ts +7 -0
  156. package/dist/state/migrations/20261005120000_drop_the_active_exec_tracking.js +12 -0
  157. package/dist/state/migrations/index.js +2 -0
  158. package/dist/state/schema.d.ts +1 -5
  159. package/dist/state/schema.js +1 -6
  160. package/dist/state/step-executions.js +1 -2
  161. package/dist/tasks/data-profile.js +7 -20
  162. package/dist/tasks/derive-table-exec.d.ts +4 -14
  163. package/dist/tasks/derive-table-exec.js +9 -24
  164. package/dist/tasks/extract-values.d.ts +2 -2
  165. package/dist/tasks/extract-values.js +4 -7
  166. package/dist/tools/bio/index.d.ts +2 -0
  167. package/dist/tools/bio/index.js +3 -0
  168. package/dist/tools/bio/keys.d.ts +11 -0
  169. package/dist/tools/bio/keys.js +15 -0
  170. package/dist/tools/bio/search-clinical-trials.d.ts +1 -1
  171. package/dist/tools/bio/search-patents.d.ts +46 -0
  172. package/dist/tools/bio/search-patents.js +134 -0
  173. package/dist/tools/bio/search-regulatory-approvals.d.ts +49 -0
  174. package/dist/tools/bio/search-regulatory-approvals.js +158 -0
  175. package/dist/tools/define-tool.d.ts +56 -13
  176. package/dist/tools/define-tool.js +28 -1
  177. package/dist/tools/execute-analysis.js +3 -3
  178. package/dist/tools/lib/amass-client.d.ts +586 -0
  179. package/dist/tools/lib/amass-client.js +492 -0
  180. package/dist/tools/lib/amass-config.d.ts +2 -0
  181. package/dist/tools/lib/amass-config.js +10 -0
  182. package/dist/tools/report-session/derive-table.d.ts +8 -9
  183. package/dist/tools/report-session/derive-table.js +3 -4
  184. package/dist/tools/report-session/preview-report.js +4 -49
  185. package/dist/tools/sandbox/list-available-packages.d.ts +1 -1
  186. package/dist/tools/sandbox/list-available-packages.js +5 -4
  187. package/dist/tools/workspace/edit-file.d.ts +5 -4
  188. package/dist/tools/workspace/edit-file.js +10 -13
  189. package/dist/tools/workspace/execute-command.d.ts +27 -42
  190. package/dist/tools/workspace/execute-command.js +42 -38
  191. package/dist/tools/workspace/index.d.ts +5 -6
  192. package/dist/tools/workspace/index.js +5 -6
  193. package/dist/tools/workspace/mutator.d.ts +43 -8
  194. package/dist/tools/workspace/mutator.js +45 -34
  195. package/dist/tools/workspace/run-exec.d.ts +3 -12
  196. package/dist/tools/workspace/run-exec.js +7 -24
  197. package/dist/tools/workspace/scan-inputs.d.ts +0 -3
  198. package/dist/tools/workspace/scan-inputs.js +0 -1
  199. package/dist/tools/workspace/write-file.d.ts +12 -3
  200. package/dist/tools/workspace/write-file.js +17 -13
  201. package/dist/workflows/sandbox-step.d.ts +7 -11
  202. package/dist/workflows/sandbox-step.js +24 -27
  203. package/package.json +3 -1
  204. package/dist/sandbox/await-exec.d.ts +0 -143
  205. package/dist/sandbox/await-exec.js +0 -417
  206. package/dist/sandbox/deliver-exec-event.d.ts +0 -25
  207. package/dist/sandbox/deliver-exec-event.js +0 -29
  208. package/dist/sandbox/digest.d.ts +0 -14
  209. package/dist/sandbox/digest.js +0 -17
  210. package/dist/sandbox/exec-id.d.ts +0 -12
  211. package/dist/sandbox/exec-id.js +0 -20
  212. package/dist/sandbox/hmac.d.ts +0 -62
  213. package/dist/sandbox/hmac.js +0 -72
  214. package/dist/sandbox/submit-exec.d.ts +0 -38
  215. package/dist/sandbox/submit-exec.js +0 -80
  216. package/dist/sandbox/watchdog.d.ts +0 -71
  217. package/dist/sandbox/watchdog.js +0 -161
@@ -0,0 +1,341 @@
1
+ /**
2
+ * The place of the names that a figure of one unit draws on its data: the sample names of a PCA, and the
3
+ * category names of an embedding.
4
+ *
5
+ * The chart runtime moves a label that overlaps another one only where the label states its own absolute
6
+ * place, and the option holds no function that could state one. Thus the derivation places each name itself,
7
+ * before the draw, in the units of the data. The square of the figure spans one length of data on each axis,
8
+ * thus one estimate of the square size converts a text size in pixels to a size in data units on both axes.
9
+ *
10
+ * The estimate reads a square smaller than the square of the page, because the column export draws a smaller
11
+ * square at a text size that shrinks less. A name that clears its neighbor at the estimate clears it at each
12
+ * size. The place is a pure function of the points and the names, thus the same rows give the same places.
13
+ */
14
+ import { CHART_EXPORT_SIZES, CHART_PAGE_TEXT_PX } from "../design.js";
15
+ /** The square side, in pixels, at which the derivation measures a name. */
16
+ const ESTIMATE_SQUARE_PX = 240;
17
+ /** The width of one character, and the height of one line, as a share of the text size. */
18
+ const CHARACTER_SHARE = 0.6;
19
+ const LINE_SHARE = 1.3;
20
+ /** The gap between a point and its name, in pixels. */
21
+ export const NAME_GAP_PX = 4;
22
+ /** True when two boxes share an area. */
23
+ export function overlaps(a, b) {
24
+ return a.left < b.right && b.left < a.right && a.bottom < b.top && b.bottom < a.top;
25
+ }
26
+ /**
27
+ * True when a name on one side of its point stays inside the plot on that side. A name at the left never
28
+ * crosses the y axis, and a name under its point never crosses the x axis. A name over or under its point
29
+ * centers on it, thus it can pass a side edge by a part of its width.
30
+ */
31
+ function inside(box, side, plot) {
32
+ switch (side) {
33
+ case "right":
34
+ return box.right <= plot.x.max;
35
+ case "left":
36
+ return box.left >= plot.x.min;
37
+ case "top":
38
+ return box.top <= plot.y.max;
39
+ case "bottom":
40
+ return box.bottom >= plot.y.min;
41
+ }
42
+ }
43
+ /** The size of one name in data units, for an axis of the given length. */
44
+ function nameSize(text, length) {
45
+ const unit = length / ESTIMATE_SQUARE_PX;
46
+ return { width: text.length * CHARACTER_SHARE * CHART_PAGE_TEXT_PX * unit, height: LINE_SHARE * CHART_PAGE_TEXT_PX * unit };
47
+ }
48
+ /** The four sides of a point where its name can sit, in the order that the figure tries them. */
49
+ export const NAME_SIDES = ["right", "left", "top", "bottom"];
50
+ /**
51
+ * The side of each point where its name sits, in the order of the points.
52
+ *
53
+ * Each name takes the first side, in the order right, left, top, bottom, where it stays inside the plot on that
54
+ * side and covers no earlier name and no point. A name that finds no free side sits at the right. `plot` is the range of
55
+ * the two axes in data units, and `pointPx` is the diameter of a point in pixels.
56
+ */
57
+ export function pointNameSides(points, plot, pointPx) {
58
+ const length = plot.x.max - plot.x.min;
59
+ const unit = length / ESTIMATE_SQUARE_PX;
60
+ const reach = (pointPx / 2) * unit;
61
+ const gap = NAME_GAP_PX * unit;
62
+ const marks = points.map((point) => ({ left: point.x - reach, right: point.x + reach, bottom: point.y - reach, top: point.y + reach }));
63
+ const placed = [];
64
+ return points.map((point, index) => {
65
+ const { width, height } = nameSize(point.text, length);
66
+ const boxes = {
67
+ right: { left: point.x + reach + gap, right: point.x + reach + gap + width, bottom: point.y - height / 2, top: point.y + height / 2 },
68
+ left: { left: point.x - reach - gap - width, right: point.x - reach - gap, bottom: point.y - height / 2, top: point.y + height / 2 },
69
+ top: { left: point.x - width / 2, right: point.x + width / 2, bottom: point.y + reach + gap, top: point.y + reach + gap + height },
70
+ bottom: { left: point.x - width / 2, right: point.x + width / 2, bottom: point.y - reach - gap - height, top: point.y - reach - gap },
71
+ };
72
+ const free = NAME_SIDES.find((side) => {
73
+ const box = boxes[side];
74
+ return (inside(box, side, plot) && !placed.some((other) => overlaps(box, other)) && !marks.some((mark, other) => other !== index && overlaps(box, mark)));
75
+ });
76
+ const side = free ?? "right";
77
+ placed.push(boxes[side]);
78
+ return side;
79
+ });
80
+ }
81
+ /**
82
+ * The place of each category name on the data, in the order of the names, or `undefined` for a name that
83
+ * finds no free place.
84
+ *
85
+ * A name sits centered on its anchor, for example the median of its cells. A heavier name places first, thus
86
+ * the name of a large cluster keeps its anchor. A name that covers an earlier name moves along y by whole
87
+ * lines, up first, while its box stays inside the y range of `plot`, until it covers none. A name that covers
88
+ * an earlier name at each such place draws nothing, thus no two names overlap. `weights` gives the weight of
89
+ * each name, for example its cell count, and the x range of `plot` gives the length that sizes a name.
90
+ */
91
+ export function nameAnchors(names, weights, plot) {
92
+ const length = plot.x.max - plot.x.min;
93
+ const order = names.map((_name, index) => index).sort((a, b) => weights[b] - weights[a] || a - b);
94
+ const line = nameSize("", length).height;
95
+ const out = names.map(() => undefined);
96
+ // A box of no height covers nothing, and a grid of such cells has no key.
97
+ if (!(line > 0))
98
+ return names.map((name) => ({ x: name.x, y: name.y }));
99
+ const placed = boxGrid(line, plot);
100
+ const lines = Math.ceil((plot.y.max - plot.y.min) / line);
101
+ for (const index of order) {
102
+ const name = names[index];
103
+ const { width, height } = nameSize(name.text, length);
104
+ const boxAt = (y) => ({ left: name.x - width / 2, right: name.x + width / 2, bottom: y - height / 2, top: y + height / 2 });
105
+ const free = (y, moved) => {
106
+ const box = boxAt(y);
107
+ return (!moved || (box.bottom >= plot.y.min && box.top <= plot.y.max)) && !placed.covers(box);
108
+ };
109
+ let chosen = free(name.y, false) ? name.y : undefined;
110
+ for (let step = 1; chosen === undefined && step <= lines; step += 1) {
111
+ chosen = [name.y + step * height, name.y - step * height].find((y) => free(y, true));
112
+ }
113
+ if (chosen === undefined)
114
+ continue;
115
+ placed.add(boxAt(chosen));
116
+ out[index] = { x: name.x, y: Number(chosen.toPrecision(12)) };
117
+ }
118
+ return out;
119
+ }
120
+ /**
121
+ * The placed boxes of one plot, indexed by a grid of square cells of the given side from the lower left corner of
122
+ * the plot. A box enters each cell that it spans, thus a test of one box reads the boxes of its own cells alone,
123
+ * and a plot of many thousand names costs no scan of each earlier name.
124
+ *
125
+ * The cells count from the corner and not from zero. A cell number of a far coordinate loses its whole part to
126
+ * the float precision, and the cells of one box could then span a billion numbers.
127
+ */
128
+ function boxGrid(side, plot) {
129
+ const cells = new Map();
130
+ const column = (x) => Math.floor((x - plot.x.min) / side);
131
+ const row = (y) => Math.floor((y - plot.y.min) / side);
132
+ const keys = (box) => {
133
+ const out = [];
134
+ for (let across = column(box.left); across <= column(box.right); across += 1) {
135
+ for (let up = row(box.bottom); up <= row(box.top); up += 1)
136
+ out.push(`${across}:${up}`);
137
+ }
138
+ return out;
139
+ };
140
+ return {
141
+ add: (box) => {
142
+ for (const key of keys(box)) {
143
+ const cell = cells.get(key);
144
+ if (cell === undefined)
145
+ cells.set(key, [box]);
146
+ else
147
+ cell.push(box);
148
+ }
149
+ },
150
+ covers: (box) => keys(box).some((key) => cells.get(key)?.some((other) => overlaps(box, other)) ?? false),
151
+ };
152
+ }
153
+ /** The width of one text in pixels at one text size: the estimate of each figure that places its own text. */
154
+ export function textWidthPx(text, textPx) {
155
+ return text.length * CHARACTER_SHARE * textPx;
156
+ }
157
+ /** The frame of the page: the plot inside the 900 by 400 pixel chart body of a window 1280 pixels wide. */
158
+ export const PAGE_PLOT_FRAME = { widthPx: 760, heightPx: 285, textPx: CHART_PAGE_TEXT_PX };
159
+ /** The frame of the single-column export: the plot inside the 336 by 253 pixel chart, and the 7 pt text. */
160
+ export const COLUMN_PLOT_FRAME = { widthPx: 260, heightPx: 160, textPx: CHART_EXPORT_SIZES.single.textPx };
161
+ /** The frame of the double-column export: the plot inside the 692 by 348 pixel chart, and the 7 pt text. */
162
+ export const DOUBLE_COLUMN_PLOT_FRAME = { widthPx: 580, heightPx: 245, textPx: CHART_EXPORT_SIZES.double.textPx };
163
+ /** The frame of the slide export: the plot inside the 1920 by 1080 pixel chart, and the slide text. */
164
+ export const SLIDE_PLOT_FRAME = { widthPx: 1600, heightPx: 760, textPx: CHART_EXPORT_SIZES.slide.textPx };
165
+ /** The frame of each export size. A new export size joins with the frame of its plot. */
166
+ export const EXPORT_PLOT_FRAMES = {
167
+ single: COLUMN_PLOT_FRAME,
168
+ double: DOUBLE_COLUMN_PLOT_FRAME,
169
+ slide: SLIDE_PLOT_FRAME,
170
+ };
171
+ /** The most lines that a name moves away from its point before the figure gives it up. */
172
+ const LEADER_STEPS = 12;
173
+ /**
174
+ * The anchor of each name, in the order of the names, or `undefined` for a name that finds no free place.
175
+ *
176
+ * The names come in the order of their weight, thus the most important name places first. Each name tries the
177
+ * sides in the given order, first beside its point, then one line further away at each step, up before down.
178
+ * A leader line joins each point to the anchor of its name. A place is free when the name stays inside the
179
+ * plot, when the name and its leader line cross no earlier name and no earlier leader line, and when they cover
180
+ * no point. Where no place is free, the name takes the first place clear of the earlier names and leader lines
181
+ * that covers the fewest points. A name that finds no place clear of them draws nothing.
182
+ *
183
+ * The geometry runs in pixels of `frame`, and each point enters a grid of cells one line high. Thus a test of
184
+ * one place reads the points of a few cells alone, and a table of many thousand points costs one pass to index.
185
+ * `plot` is the range of the two axes in data units. `top` is the highest y in data units that the text of a
186
+ * name can reach, because the chart runtime can end the y axis at the largest point. `reserved` holds the boxes
187
+ * in data units that other text of the figure takes, for example the label of a guide line, and no name covers
188
+ * one. A leader line can pass one, because a point can lie under the text.
189
+ */
190
+ export function placeLeaderNames(names, obstacles, plot, sides, top, frame = COLUMN_PLOT_FRAME, reserved = []) {
191
+ const xSpan = plot.x.max - plot.x.min;
192
+ const ySpan = plot.y.max - plot.y.min;
193
+ if (!(xSpan > 0) || !(ySpan > 0))
194
+ return names.map(() => undefined);
195
+ const toX = (x) => ((x - plot.x.min) / xSpan) * frame.widthPx;
196
+ const toY = (y) => ((y - plot.y.min) / ySpan) * frame.heightPx;
197
+ const lineHeight = LINE_SHARE * frame.textPx;
198
+ const reach = obstacles.pointPx / 2;
199
+ const ceiling = Math.min(frame.heightPx, toY(top) + lineHeight / 2);
200
+ const cells = new Map();
201
+ const cellOf = (value) => Math.floor(value / lineHeight);
202
+ for (let index = 0; index < obstacles.xs.length; index += 1) {
203
+ const x = obstacles.xs[index];
204
+ const y = obstacles.ys[index];
205
+ if (x === null || y === null)
206
+ continue;
207
+ const point = { x: toX(x), y: toY(y) };
208
+ const key = `${cellOf(point.x)}:${cellOf(point.y)}`;
209
+ const cell = cells.get(key);
210
+ if (cell === undefined)
211
+ cells.set(key, [point]);
212
+ else
213
+ cell.push(point);
214
+ }
215
+ const coveredPoints = (box) => {
216
+ let count = 0;
217
+ for (let column = cellOf(box.left - reach); column <= cellOf(box.right + reach); column += 1) {
218
+ for (let row = cellOf(box.bottom - reach); row <= cellOf(box.top + reach); row += 1) {
219
+ const cell = cells.get(`${column}:${row}`);
220
+ if (cell === undefined)
221
+ continue;
222
+ for (const point of cell) {
223
+ if (point.x + reach > box.left && point.x - reach < box.right && point.y + reach > box.bottom && point.y - reach < box.top)
224
+ count += 1;
225
+ }
226
+ }
227
+ }
228
+ return count;
229
+ };
230
+ // The points that a leader line passes, sampled at each point radius along the line. The named point and
231
+ // the points that touch it sit at the start of the line, thus the count starts one point width away.
232
+ const crossedPoints = (from, to) => {
233
+ const length = Math.hypot(to.x - from.x, to.y - from.y);
234
+ const spacing = Math.max(reach, 1);
235
+ let count = 0;
236
+ for (let along = 2 * reach + NAME_GAP_PX; along < length; along += spacing) {
237
+ const at = { x: from.x + ((to.x - from.x) * along) / length, y: from.y + ((to.y - from.y) * along) / length };
238
+ count += coveredPoints({ left: at.x, right: at.x, bottom: at.y, top: at.y });
239
+ }
240
+ return count;
241
+ };
242
+ const kept = reserved.map((box) => ({ left: toX(box.left), right: toX(box.right), bottom: toY(box.bottom), top: toY(box.top) }));
243
+ const placed = [];
244
+ const leaders = [];
245
+ return names.map((name) => {
246
+ const width = textWidthPx(name.text, frame.textPx);
247
+ const point = { x: toX(name.x), y: toY(name.y) };
248
+ let best;
249
+ search: for (let step = 0; step <= LEADER_STEPS; step += 1) {
250
+ for (const side of sides) {
251
+ for (const direction of side === "top" || step === 0 ? [1] : [1, -1]) {
252
+ const anchor = leaderAnchor(point, side, step, direction, reach, lineHeight, width, frame.widthPx);
253
+ const box = leaderBox(anchor, side, width, lineHeight);
254
+ const inside = box.left >= 0 && box.right <= frame.widthPx && box.bottom >= 0 && box.top <= ceiling;
255
+ if (!inside || kept.some((other) => overlaps(box, other)))
256
+ continue;
257
+ if (placed.some((other) => overlaps(box, other) || crosses(point, anchor, other)))
258
+ continue;
259
+ if (leaders.some(([from, to]) => crosses(from, to, box)))
260
+ continue;
261
+ const covered = coveredPoints(box) + crossedPoints(point, anchor);
262
+ if (best === undefined || covered < best.covered)
263
+ best = { anchor, side, box, covered };
264
+ if (covered === 0)
265
+ break search;
266
+ }
267
+ }
268
+ }
269
+ if (best === undefined)
270
+ return undefined;
271
+ placed.push(best.box);
272
+ leaders.push([point, best.anchor]);
273
+ const x = plot.x.min + (best.anchor.x / frame.widthPx) * xSpan;
274
+ const y = plot.y.min + (best.anchor.y / frame.heightPx) * ySpan;
275
+ return { x: Number(x.toPrecision(12)), y: Number(y.toPrecision(12)), side: best.side };
276
+ });
277
+ }
278
+ /**
279
+ * True when the segment between two places passes through a box: the clip of Liang and Barsky, which keeps the
280
+ * part of the segment inside each of the four edges.
281
+ */
282
+ function crosses(from, to, box) {
283
+ let enter = 0;
284
+ let leave = 1;
285
+ const dx = to.x - from.x;
286
+ const dy = to.y - from.y;
287
+ for (const [p, q] of [
288
+ [-dx, from.x - box.left],
289
+ [dx, box.right - from.x],
290
+ [-dy, from.y - box.bottom],
291
+ [dy, box.top - from.y],
292
+ ]) {
293
+ if (p === 0) {
294
+ if (q < 0)
295
+ return false;
296
+ continue;
297
+ }
298
+ const ratio = q / p;
299
+ if (p < 0)
300
+ enter = Math.max(enter, ratio);
301
+ else
302
+ leave = Math.min(leave, ratio);
303
+ if (enter > leave)
304
+ return false;
305
+ }
306
+ return true;
307
+ }
308
+ /**
309
+ * The anchor of a name in pixels: beside the point at the right or at the left, or over it, then one line
310
+ * further away at each step. A name over a point near a side edge of the plot moves along x until it fits
311
+ * inside the plot, and its leader line then leans.
312
+ */
313
+ function leaderAnchor(point, side, step, direction, reach, lineHeight, width, plotWidth) {
314
+ const gap = NAME_GAP_PX;
315
+ switch (side) {
316
+ case "right":
317
+ return { x: point.x + reach + gap, y: point.y + direction * step * lineHeight };
318
+ case "left":
319
+ return { x: point.x - reach - gap, y: point.y + direction * step * lineHeight };
320
+ case "top":
321
+ return { x: Math.min(Math.max(point.x, width / 2), plotWidth - width / 2), y: point.y + reach + gap + step * lineHeight };
322
+ }
323
+ }
324
+ /** The box of a name in pixels at its anchor. */
325
+ function leaderBox(anchor, side, width, height) {
326
+ switch (side) {
327
+ case "right":
328
+ return { left: anchor.x, right: anchor.x + width, bottom: anchor.y - height / 2, top: anchor.y + height / 2 };
329
+ case "left":
330
+ return { left: anchor.x - width, right: anchor.x, bottom: anchor.y - height / 2, top: anchor.y + height / 2 };
331
+ case "top":
332
+ return { left: anchor.x - width / 2, right: anchor.x + width / 2, bottom: anchor.y, top: anchor.y + height };
333
+ }
334
+ }
335
+ /** The box of one placed name in data units, for a test that a name covers no neighbor and no point. */
336
+ export function leaderNameBox(name, text, plot, frame = COLUMN_PLOT_FRAME) {
337
+ const xUnit = (plot.x.max - plot.x.min) / frame.widthPx;
338
+ const yUnit = (plot.y.max - plot.y.min) / frame.heightPx;
339
+ const box = leaderBox({ x: 0, y: 0 }, name.side, textWidthPx(text, frame.textPx), LINE_SHARE * frame.textPx);
340
+ return { left: name.x + box.left * xUnit, right: name.x + box.right * xUnit, bottom: name.y + box.bottom * yUnit, top: name.y + box.top * yUnit };
341
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The regional association plot, as LocusZoom draws it (Pruim et al. 2010): the variants of one region of one
3
+ * chromosome, with the position on x and the transformed p on y.
4
+ *
5
+ * - Each point takes the LocusZoom bin of its r² with the lead variant, and a point of no r² draws gray under
6
+ * the others. One legend over the plot states the bins and the gray.
7
+ * - The lead variant is the row with the largest transformed p. It draws as a purple diamond with its name on
8
+ * top. A tie keeps the row at the smaller position, then the first row.
9
+ * - The recombination rate draws as a blue line on a right axis in cM/Mb, from the rows in position order.
10
+ * - A guide line marks the genome-wide significance at 5 × 10⁻⁸. Its name sits over the line at the first free
11
+ * spot from the left end, where no variant and no stretch of the recombination line meets the name.
12
+ * - The x axis reads in megabases, with round ticks, and it ends at the first and the last position of the rows.
13
+ * - The genes of the track draw under the plot, each one as a line with its name under it. The lane of a gene
14
+ * is the first lane where its line and its name overlap no gene before it, in the order of the gene starts.
15
+ * - A variant whose stored p is 0 draws an upward triangle at the top of the plotted range, and it leads.
16
+ *
17
+ * The points and the recombination line come from the composition machinery, thus a window of many thousand
18
+ * variants reads the shared payload. The r² rides the points as a continuous color, and a piecewise map bins
19
+ * it. The composition drops a row whose color is not numeric, thus the points of no r² draw in a series of
20
+ * their own after the composition, with their rows inline. The tick names, the lead, and the genes follow them.
21
+ *
22
+ * The chart runtime takes no function in the option, thus the axis cannot print a position divided by 10⁶.
23
+ * The axis hides its own ticks and labels, and a series of named points draws each tick in megabases, as the
24
+ * Manhattan figure prints its chromosome names.
25
+ */
26
+ import type { FigureModule } from "./index.js";
27
+ /** One LD bin: the r² range, lower end included, and its color. The highest bin includes 1. */
28
+ export interface LdBin {
29
+ readonly low: number;
30
+ readonly high: number;
31
+ readonly color: string;
32
+ }
33
+ /** The five r² bins of LocusZoom, in ascending order, with its colors: navy, sky blue, green, orange, and red. */
34
+ export declare const LD_BINS: readonly LdBin[];
35
+ /** The gray of a variant with no r², and the name of its series and its legend entry. */
36
+ export declare const LD_MISSING_COLOR = "#8c8c8c";
37
+ export declare const LD_MISSING_NAME = "No r\u00B2";
38
+ /** The purple of the lead variant, as LocusZoom draws it, and the name of its series. */
39
+ export declare const LEAD_VARIANT_COLOR = "#9632b8";
40
+ export declare const LEAD_VARIANT_NAME = "Lead variant";
41
+ /** The blue of the recombination line, and the name of its series. */
42
+ export declare const RECOMBINATION_COLOR = "#1f4fd8";
43
+ export declare const RECOMBINATION_NAME = "Recombination rate";
44
+ /** The name of the series that prints the tick names in megabases, and of the series of the genes. */
45
+ export declare const POSITION_TICKS = "Position ticks";
46
+ export declare const GENE_NAMES = "Genes";
47
+ /** The name of the series that prints the name of the genome-wide line. */
48
+ export declare const GUIDE_LABEL = "Genome-wide line";
49
+ /** The locuszoom figure module. */
50
+ export declare const LOCUSZOOM_FIGURE: FigureModule;