@almadar/std 16.174.0 → 16.176.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 (45) hide show
  1. package/behaviors/lolo/ui/core/molecules/ui-table-view.lolo +4 -9
  2. package/behaviors/lolo/ui/learning/atoms/std-learn-classify.lolo +132 -22
  3. package/behaviors/lolo/ui/learning/atoms/std-learn-derivation.lolo +428 -0
  4. package/behaviors/lolo/ui/learning/atoms/std-learn-doser.lolo +520 -0
  5. package/behaviors/lolo/ui/learning/atoms/std-learn-scatter-fit.lolo +805 -0
  6. package/behaviors/lolo/ui/learning/atoms/std-learn-tour-3d.lolo +31 -2
  7. package/behaviors/lolo/ui/learning/atoms/std-learn-transport.lolo +186 -0
  8. package/behaviors/lolo/ui/learning/atoms/std-learn-tree.lolo +522 -0
  9. package/behaviors/lolo/ui/learning/atoms/ui-algo-graph-canvas.lolo +192 -0
  10. package/behaviors/lolo/ui/learning/atoms/ui-algorithm-canvas.lolo +111 -2
  11. package/behaviors/lolo/ui/learning/atoms/ui-biology-canvas.lolo +150 -1
  12. package/behaviors/lolo/ui/learning/atoms/ui-chemistry-canvas.lolo +132 -1
  13. package/behaviors/lolo/ui/learning/atoms/ui-learning-canvas.lolo +48 -1
  14. package/behaviors/lolo/ui/learning/atoms/ui-math-canvas.lolo +141 -1
  15. package/behaviors/lolo/ui/learning/atoms/ui-physics-canvas.lolo +199 -1
  16. package/behaviors/registry/ui/core/molecules/ui-table-view.orb +4 -39
  17. package/behaviors/registry/ui/learning/atoms/std-learn-classify.orb +1288 -197
  18. package/behaviors/registry/ui/learning/atoms/std-learn-derivation.orb +2512 -0
  19. package/behaviors/registry/ui/learning/atoms/std-learn-doser.orb +3678 -0
  20. package/behaviors/registry/ui/learning/atoms/std-learn-scatter-fit.orb +6539 -0
  21. package/behaviors/registry/ui/learning/atoms/std-learn-tour-3d.orb +173 -2
  22. package/behaviors/registry/ui/learning/atoms/std-learn-transport.orb +986 -0
  23. package/behaviors/registry/ui/learning/atoms/std-learn-tree.orb +3333 -0
  24. package/behaviors/registry/ui/learning/atoms/ui-algo-graph-canvas.orb +716 -0
  25. package/behaviors/registry/ui/learning/atoms/ui-algorithm-canvas.orb +506 -1
  26. package/behaviors/registry/ui/learning/atoms/ui-biology-canvas.orb +684 -0
  27. package/behaviors/registry/ui/learning/atoms/ui-chemistry-canvas.orb +634 -0
  28. package/behaviors/registry/ui/learning/atoms/ui-learning-canvas.orb +256 -0
  29. package/behaviors/registry/ui/learning/atoms/ui-math-canvas.orb +736 -12
  30. package/behaviors/registry/ui/learning/atoms/ui-physics-canvas.orb +1061 -33
  31. package/dist/behaviors/exports-reader.js +2448 -2184
  32. package/dist/behaviors/functions/index.d.ts +800 -13
  33. package/dist/behaviors/functions/index.js +2431 -2185
  34. package/dist/behaviors/index.d.ts +1 -1
  35. package/dist/behaviors/index.js +2449 -2185
  36. package/dist/behaviors/query.js +2448 -2184
  37. package/dist/behaviors-embeddings.hash.json +3 -3
  38. package/dist/behaviors-embeddings.json +318 -294
  39. package/dist/behaviors-registry.json +4391 -1298
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.js +2449 -2185
  42. package/dist/knob-embeddings.hash.json +3 -3
  43. package/dist/knob-embeddings.json +1 -1
  44. package/dist/registry/factory-signatures.json +1 -1
  45. package/package.json +2 -2
@@ -0,0 +1,805 @@
1
+ app std-learn-scatter-fit "1.0.0"
2
+ "Generic scatter-plot fitting instrument — points on a math-canvas (config-declared axis ranges/labels/colors) with a live least-squares refit: slope, intercept, r, and R² recompute as points are added (button or auto-add tick, up to maxPoints) or dragged. An optional guess-then-reveal mode lets the learner drag slope/intercept sliders to propose a line, scored against the fit until Reveal Fit; dashed residual segments and the r/R² readout are config-toggleable. Run/Pause drives a fixed-interval auto-add tick, Reset reseeds from the configured point roster. SCATTER_FIT_UPDATED broadcasts the fit after every learner action, tick, or reset. Every button label, caption, curve label/color, and narration phrase is config-declared so the same mechanism serves any scatter-fit domain (correlation, physics data fitting, trend lines, and beyond) on a math-canvas. Standalone by default; behind a lab router set matchMode and it renders only when MODE_SELECTED matches."
3
+ orbital LearnScatterFitOrbital {
4
+ type LearnScatterFitPoint = {
5
+ id : string
6
+ x : number!
7
+ y : number!
8
+ label : string
9
+ color : string
10
+ radius : number
11
+ }
12
+ type LearnScatterFitCurve = {
13
+ label : string
14
+ color : string
15
+ dash : "dashed" | "dotted"
16
+ samples : [LearnScatterFitCurvePoint]
17
+ }
18
+ type LearnScatterFitCurvePoint = {
19
+ x : number!
20
+ y : number!
21
+ }
22
+
23
+ entity LearnScatterFitScene [runtime] {
24
+ id : string!
25
+ active : boolean = false
26
+ title : string
27
+ points : [LearnScatterFitPoint]
28
+ slope : number
29
+ intercept : number
30
+ r : number
31
+ rSquared : number
32
+ guessSlope : number
33
+ guessIntercept : number
34
+ guessScore : number
35
+ revealed : boolean = false
36
+ running : boolean = false
37
+ done : boolean = false
38
+ curves : [LearnScatterFitCurve]
39
+ narration : string
40
+ }
41
+
42
+ trait LearnScatterFitSim -> @rebindable LearnScatterFitScene [interaction, instance] {
43
+ initial: idle
44
+
45
+ state idle {
46
+ INIT -> idle when (== @config.matchMode "")
47
+ (set @entity.active true)
48
+ (set @entity.title @config.title)
49
+ (set @entity.points @config.initialPoints)
50
+ (set @entity.guessSlope 0)
51
+ (set @entity.guessIntercept (/ (+ @config.axisYMin @config.axisYMax) 2))
52
+ (set @entity.revealed false)
53
+ (set @entity.running @config.autoRun)
54
+ (let (
55
+ (pts @config.initialPoints)
56
+ (n (array/len @pts))
57
+ (meanX (if (> @n 0) (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p x)))) @n) 0))
58
+ (meanY (if (> @n 0) (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p y)))) @n) 0))
59
+ (sxx (array/reduce @pts 0 (fn (acc p) (let ((dx (- (object/get @p x) @meanX))) (+ @acc (* @dx @dx))))))
60
+ (syy (array/reduce @pts 0 (fn (acc p) (let ((dy (- (object/get @p y) @meanY))) (+ @acc (* @dy @dy))))))
61
+ (sxy (array/reduce @pts 0 (fn (acc p) (+ @acc (* (- (object/get @p x) @meanX) (- (object/get @p y) @meanY))))))
62
+ (newSlope (if (> @sxx 0) (/ @sxy @sxx) 0))
63
+ (newIntercept (- @meanY (* @newSlope @meanX)))
64
+ (newR (if (and (> @sxx 0) (> @syy 0)) (/ @sxy (math/sqrt (* @sxx @syy))) 0))
65
+ (fitCurve { label: @config.fitLabel, color: @config.fitColor, samples: [
66
+ { x: @config.axisXMin, y: (math/clamp @newIntercept @config.axisYMin @config.axisYMax) },
67
+ { x: @config.axisXMax, y: (math/clamp (+ (* @newSlope @config.axisXMax) @newIntercept) @config.axisYMin @config.axisYMax) }
68
+ ]})
69
+ (residualCurves (if @config.showResiduals
70
+ (array/map @pts (fn p { label: "", color: @config.residualColor, dash: @config.residualDash, samples: [ { x: (object/get @p x), y: (object/get @p y) }, { x: (object/get @p x), y: (+ (* @newSlope (object/get @p x)) @newIntercept) } ] }))
71
+ []))
72
+ )
73
+ (do
74
+ (set @entity.slope @newSlope)
75
+ (set @entity.intercept @newIntercept)
76
+ (set @entity.r @newR)
77
+ (set @entity.rSquared (* @newR @newR))
78
+ (set @entity.guessScore 0)
79
+ (set @entity.curves (array/concat (array/append [] @fitCurve) @residualCurves))
80
+ (set @entity.done (>= @n @config.maxPoints))
81
+ (set @entity.narration (str/concat (str/concat "" @n) " " @config.narrationLoadedTemplate " — r = " (str/concat "" (math/round @newR 3)) ", R² = " (str/concat "" (math/round (* @newR @newR) 3))))
82
+ )
83
+ )
84
+ (render-ui main {
85
+ type: box
86
+ padding: md
87
+ children: [
88
+ { type: stack, direction: vertical, gap: md, children: [
89
+ { type: typography, variant: h4, content: @entity.title },
90
+ { type: stack, direction: horizontal, gap: sm, children: [
91
+ { type: button, action: ADD_POINT, label: @config.addPointLabel, variant: primary },
92
+ { type: button, action: REVEAL_FIT, label: @config.revealLabel, variant: secondary },
93
+ { type: button, action: TOGGLE_RUN, label: (if @entity.running @config.pauseLabel @config.runLabel), variant: secondary },
94
+ { type: button, action: RESET, label: @config.resetLabel, variant: ghost }
95
+ ] },
96
+ { type: typography, variant: caption, content: (str/concat @config.guessSlopeCaption (math/round @entity.guessSlope 2)) },
97
+ { type: range-slider, min: @config.guessSlopeMin, max: @config.guessSlopeMax, step: @config.guessSlopeStep, value: @entity.guessSlope, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "slope" } },
98
+ { type: typography, variant: caption, content: (str/concat @config.guessInterceptCaption (math/round @entity.guessIntercept 1)) },
99
+ { type: range-slider, min: @config.guessInterceptMin, max: @config.guessInterceptMax, step: @config.guessInterceptStep, value: @entity.guessIntercept, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "intercept" } },
100
+ { type: math-canvas, width: @config.width, height: @config.height, xMin: @config.axisXMin, xMax: @config.axisXMax, yMin: @config.axisYMin, yMax: @config.axisYMax, showAxes: @config.showAxes, showGrid: @config.showGrid, gridStep: @config.gridStep, showTickLabels: @config.showTickLabels, curves: @entity.curves, points: @entity.points, vectors: [], interactive: @config.interactive, animate: @config.animate },
101
+ { type: typography, variant: body, content: (if @config.showR (str/concat "r = " (math/round @entity.r 3) " — R² = " (math/round @entity.rSquared 3) " — guess score " @entity.guessScore "%") "") },
102
+ { type: typography, variant: body, content: @entity.narration }
103
+ ] }
104
+ ]
105
+ })
106
+ (emit LEARN_SCATTER_FIT_UPDATED { slope: @entity.slope, intercept: @entity.intercept, r: @entity.r, rSquared: @entity.rSquared, pointCount: (array/len @entity.points), narration: @entity.narration })
107
+
108
+ MODE_SELECTED -> idle when (== ?mode @config.matchMode)
109
+ (set @entity.active true)
110
+ (set @entity.title @config.title)
111
+ (set @entity.points @config.initialPoints)
112
+ (set @entity.guessSlope 0)
113
+ (set @entity.guessIntercept (/ (+ @config.axisYMin @config.axisYMax) 2))
114
+ (set @entity.revealed false)
115
+ (set @entity.running @config.autoRun)
116
+ (let (
117
+ (pts @config.initialPoints)
118
+ (n (array/len @pts))
119
+ (meanX (if (> @n 0) (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p x)))) @n) 0))
120
+ (meanY (if (> @n 0) (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p y)))) @n) 0))
121
+ (sxx (array/reduce @pts 0 (fn (acc p) (let ((dx (- (object/get @p x) @meanX))) (+ @acc (* @dx @dx))))))
122
+ (syy (array/reduce @pts 0 (fn (acc p) (let ((dy (- (object/get @p y) @meanY))) (+ @acc (* @dy @dy))))))
123
+ (sxy (array/reduce @pts 0 (fn (acc p) (+ @acc (* (- (object/get @p x) @meanX) (- (object/get @p y) @meanY))))))
124
+ (newSlope (if (> @sxx 0) (/ @sxy @sxx) 0))
125
+ (newIntercept (- @meanY (* @newSlope @meanX)))
126
+ (newR (if (and (> @sxx 0) (> @syy 0)) (/ @sxy (math/sqrt (* @sxx @syy))) 0))
127
+ (fitCurve { label: @config.fitLabel, color: @config.fitColor, samples: [
128
+ { x: @config.axisXMin, y: (math/clamp @newIntercept @config.axisYMin @config.axisYMax) },
129
+ { x: @config.axisXMax, y: (math/clamp (+ (* @newSlope @config.axisXMax) @newIntercept) @config.axisYMin @config.axisYMax) }
130
+ ]})
131
+ (residualCurves (if @config.showResiduals
132
+ (array/map @pts (fn p { label: "", color: @config.residualColor, dash: @config.residualDash, samples: [ { x: (object/get @p x), y: (object/get @p y) }, { x: (object/get @p x), y: (+ (* @newSlope (object/get @p x)) @newIntercept) } ] }))
133
+ []))
134
+ )
135
+ (do
136
+ (set @entity.slope @newSlope)
137
+ (set @entity.intercept @newIntercept)
138
+ (set @entity.r @newR)
139
+ (set @entity.rSquared (* @newR @newR))
140
+ (set @entity.guessScore 0)
141
+ (set @entity.curves (array/concat (array/append [] @fitCurve) @residualCurves))
142
+ (set @entity.done (>= @n @config.maxPoints))
143
+ (set @entity.narration (str/concat (str/concat "" @n) " " @config.narrationLoadedTemplate " — r = " (str/concat "" (math/round @newR 3)) ", R² = " (str/concat "" (math/round (* @newR @newR) 3))))
144
+ )
145
+ )
146
+ (render-ui main {
147
+ type: box
148
+ padding: md
149
+ children: [
150
+ { type: stack, direction: vertical, gap: md, children: [
151
+ { type: typography, variant: h4, content: @entity.title },
152
+ { type: stack, direction: horizontal, gap: sm, children: [
153
+ { type: button, action: ADD_POINT, label: @config.addPointLabel, variant: primary },
154
+ { type: button, action: REVEAL_FIT, label: @config.revealLabel, variant: secondary },
155
+ { type: button, action: TOGGLE_RUN, label: (if @entity.running @config.pauseLabel @config.runLabel), variant: secondary },
156
+ { type: button, action: RESET, label: @config.resetLabel, variant: ghost }
157
+ ] },
158
+ { type: typography, variant: caption, content: (str/concat @config.guessSlopeCaption (math/round @entity.guessSlope 2)) },
159
+ { type: range-slider, min: @config.guessSlopeMin, max: @config.guessSlopeMax, step: @config.guessSlopeStep, value: @entity.guessSlope, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "slope" } },
160
+ { type: typography, variant: caption, content: (str/concat @config.guessInterceptCaption (math/round @entity.guessIntercept 1)) },
161
+ { type: range-slider, min: @config.guessInterceptMin, max: @config.guessInterceptMax, step: @config.guessInterceptStep, value: @entity.guessIntercept, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "intercept" } },
162
+ { type: math-canvas, width: @config.width, height: @config.height, xMin: @config.axisXMin, xMax: @config.axisXMax, yMin: @config.axisYMin, yMax: @config.axisYMax, showAxes: @config.showAxes, showGrid: @config.showGrid, gridStep: @config.gridStep, showTickLabels: @config.showTickLabels, curves: @entity.curves, points: @entity.points, vectors: [], interactive: @config.interactive, animate: @config.animate },
163
+ { type: typography, variant: body, content: (if @config.showR (str/concat "r = " (math/round @entity.r 3) " — R² = " (math/round @entity.rSquared 3) " — guess score " @entity.guessScore "%") "") },
164
+ { type: typography, variant: body, content: @entity.narration }
165
+ ] }
166
+ ]
167
+ })
168
+ (emit LEARN_SCATTER_FIT_UPDATED { slope: @entity.slope, intercept: @entity.intercept, r: @entity.r, rSquared: @entity.rSquared, pointCount: (array/len @entity.points), narration: @entity.narration })
169
+
170
+ ADD_POINT -> idle when (and @entity.active (not @entity.done))
171
+ (let (
172
+ (marginX (* (- @config.axisXMax @config.axisXMin) @config.pointMarginPct))
173
+ (usableX (- (- @config.axisXMax @config.axisXMin) (* @marginX 2)))
174
+ (marginY (* (- @config.axisYMax @config.axisYMin) @config.pointMarginPct))
175
+ (usableY (- (- @config.axisYMax @config.axisYMin) (* @marginY 2)))
176
+ (newPoint { id: (str/concat "p" (array/len @entity.points)), x: (math/round (+ @config.axisXMin (+ @marginX (* (math/random) @usableX)))), y: (math/round (+ @config.axisYMin (+ @marginY (* (math/random) @usableY)))), label: "", color: @config.newPointColor, radius: @config.newPointRadius })
177
+ (pts (array/append @entity.points @newPoint))
178
+ (n (array/len @pts))
179
+ (meanX (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p x)))) @n))
180
+ (meanY (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p y)))) @n))
181
+ (sxx (array/reduce @pts 0 (fn (acc p) (let ((dx (- (object/get @p x) @meanX))) (+ @acc (* @dx @dx))))))
182
+ (syy (array/reduce @pts 0 (fn (acc p) (let ((dy (- (object/get @p y) @meanY))) (+ @acc (* @dy @dy))))))
183
+ (sxy (array/reduce @pts 0 (fn (acc p) (+ @acc (* (- (object/get @p x) @meanX) (- (object/get @p y) @meanY))))))
184
+ (newSlope (if (> @sxx 0) (/ @sxy @sxx) 0))
185
+ (newIntercept (- @meanY (* @newSlope @meanX)))
186
+ (newR (if (and (> @sxx 0) (> @syy 0)) (/ @sxy (math/sqrt (* @sxx @syy))) 0))
187
+ (fitCurve { label: @config.fitLabel, color: @config.fitColor, samples: [
188
+ { x: @config.axisXMin, y: (math/clamp @newIntercept @config.axisYMin @config.axisYMax) },
189
+ { x: @config.axisXMax, y: (math/clamp (+ (* @newSlope @config.axisXMax) @newIntercept) @config.axisYMin @config.axisYMax) }
190
+ ]})
191
+ (guessCurve { label: @config.guessLabel, color: @config.guessColor, samples: [
192
+ { x: @config.axisXMin, y: (math/clamp @entity.guessIntercept @config.axisYMin @config.axisYMax) },
193
+ { x: @config.axisXMax, y: (math/clamp (+ (* @entity.guessSlope @config.axisXMax) @entity.guessIntercept) @config.axisYMin @config.axisYMax) }
194
+ ]})
195
+ (residualCurves (if @config.showResiduals
196
+ (array/map @pts (fn p { label: "", color: @config.residualColor, dash: @config.residualDash, samples: [ { x: (object/get @p x), y: (object/get @p y) }, { x: (object/get @p x), y: (+ (* @newSlope (object/get @p x)) @newIntercept) } ] }))
197
+ []))
198
+ (baseCurves (if (and @config.guessMode (not @entity.revealed))
199
+ (array/append [] @guessCurve)
200
+ (if @config.guessMode
201
+ (array/append (array/append [] @guessCurve) @fitCurve)
202
+ (array/append [] @fitCurve))))
203
+ (nowDone (>= @n @config.maxPoints))
204
+ )
205
+ (do
206
+ (set @entity.points @pts)
207
+ (set @entity.slope @newSlope)
208
+ (set @entity.intercept @newIntercept)
209
+ (set @entity.r @newR)
210
+ (set @entity.rSquared (* @newR @newR))
211
+ (set @entity.curves (array/concat @baseCurves @residualCurves))
212
+ (set @entity.done @nowDone)
213
+ (set @entity.narration (str/concat @config.narrationAddedTemplate " " (str/concat "" @n) " — r = " (str/concat "" (math/round @newR 3)) ", R² = " (str/concat "" (math/round (* @newR @newR) 3)) (if @nowDone @config.narrationMaxReachedSuffix "")))
214
+ )
215
+ )
216
+ (render-ui main {
217
+ type: box
218
+ padding: md
219
+ children: [
220
+ { type: stack, direction: vertical, gap: md, children: [
221
+ { type: typography, variant: h4, content: @entity.title },
222
+ { type: stack, direction: horizontal, gap: sm, children: [
223
+ { type: button, action: ADD_POINT, label: @config.addPointLabel, variant: primary },
224
+ { type: button, action: REVEAL_FIT, label: @config.revealLabel, variant: secondary },
225
+ { type: button, action: TOGGLE_RUN, label: (if @entity.running @config.pauseLabel @config.runLabel), variant: secondary },
226
+ { type: button, action: RESET, label: @config.resetLabel, variant: ghost }
227
+ ] },
228
+ { type: typography, variant: caption, content: (str/concat @config.guessSlopeCaption (math/round @entity.guessSlope 2)) },
229
+ { type: range-slider, min: @config.guessSlopeMin, max: @config.guessSlopeMax, step: @config.guessSlopeStep, value: @entity.guessSlope, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "slope" } },
230
+ { type: typography, variant: caption, content: (str/concat @config.guessInterceptCaption (math/round @entity.guessIntercept 1)) },
231
+ { type: range-slider, min: @config.guessInterceptMin, max: @config.guessInterceptMax, step: @config.guessInterceptStep, value: @entity.guessIntercept, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "intercept" } },
232
+ { type: math-canvas, width: @config.width, height: @config.height, xMin: @config.axisXMin, xMax: @config.axisXMax, yMin: @config.axisYMin, yMax: @config.axisYMax, showAxes: @config.showAxes, showGrid: @config.showGrid, gridStep: @config.gridStep, showTickLabels: @config.showTickLabels, curves: @entity.curves, points: @entity.points, vectors: [], interactive: @config.interactive, animate: @config.animate },
233
+ { type: typography, variant: body, content: (if @config.showR (str/concat "r = " (math/round @entity.r 3) " — R² = " (math/round @entity.rSquared 3) " — guess score " @entity.guessScore "%") "") },
234
+ { type: typography, variant: body, content: @entity.narration }
235
+ ] }
236
+ ]
237
+ })
238
+ (emit LEARN_SCATTER_FIT_UPDATED { slope: @entity.slope, intercept: @entity.intercept, r: @entity.r, rSquared: @entity.rSquared, pointCount: (array/len @entity.points), narration: @entity.narration })
239
+
240
+ DRAG_POINT -> idle when (== @entity.active true)
241
+ (let (
242
+ (pts (array/map @entity.points (fn p
243
+ (if (== (object/get @p id) ?id)
244
+ (object/merge @p { x: (math/clamp ?x @config.axisXMin @config.axisXMax), y: (math/clamp ?y @config.axisYMin @config.axisYMax) })
245
+ @p))))
246
+ (n (array/len @pts))
247
+ (meanX (if (> @n 0) (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p x)))) @n) 0))
248
+ (meanY (if (> @n 0) (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p y)))) @n) 0))
249
+ (sxx (array/reduce @pts 0 (fn (acc p) (let ((dx (- (object/get @p x) @meanX))) (+ @acc (* @dx @dx))))))
250
+ (syy (array/reduce @pts 0 (fn (acc p) (let ((dy (- (object/get @p y) @meanY))) (+ @acc (* @dy @dy))))))
251
+ (sxy (array/reduce @pts 0 (fn (acc p) (+ @acc (* (- (object/get @p x) @meanX) (- (object/get @p y) @meanY))))))
252
+ (newSlope (if (> @sxx 0) (/ @sxy @sxx) 0))
253
+ (newIntercept (- @meanY (* @newSlope @meanX)))
254
+ (newR (if (and (> @sxx 0) (> @syy 0)) (/ @sxy (math/sqrt (* @sxx @syy))) 0))
255
+ (fitCurve { label: @config.fitLabel, color: @config.fitColor, samples: [
256
+ { x: @config.axisXMin, y: (math/clamp @newIntercept @config.axisYMin @config.axisYMax) },
257
+ { x: @config.axisXMax, y: (math/clamp (+ (* @newSlope @config.axisXMax) @newIntercept) @config.axisYMin @config.axisYMax) }
258
+ ]})
259
+ (guessCurve { label: @config.guessLabel, color: @config.guessColor, samples: [
260
+ { x: @config.axisXMin, y: (math/clamp @entity.guessIntercept @config.axisYMin @config.axisYMax) },
261
+ { x: @config.axisXMax, y: (math/clamp (+ (* @entity.guessSlope @config.axisXMax) @entity.guessIntercept) @config.axisYMin @config.axisYMax) }
262
+ ]})
263
+ (residualCurves (if @config.showResiduals
264
+ (array/map @pts (fn p { label: "", color: @config.residualColor, dash: @config.residualDash, samples: [ { x: (object/get @p x), y: (object/get @p y) }, { x: (object/get @p x), y: (+ (* @newSlope (object/get @p x)) @newIntercept) } ] }))
265
+ []))
266
+ (baseCurves (if (and @config.guessMode (not @entity.revealed))
267
+ (array/append [] @guessCurve)
268
+ (if @config.guessMode
269
+ (array/append (array/append [] @guessCurve) @fitCurve)
270
+ (array/append [] @fitCurve))))
271
+ )
272
+ (do
273
+ (set @entity.points @pts)
274
+ (set @entity.slope @newSlope)
275
+ (set @entity.intercept @newIntercept)
276
+ (set @entity.r @newR)
277
+ (set @entity.rSquared (* @newR @newR))
278
+ (set @entity.curves (array/concat @baseCurves @residualCurves))
279
+ (set @entity.narration (str/concat @config.narrationMovedTemplate " — r = " (str/concat "" (math/round @newR 3)) ", R² = " (str/concat "" (math/round (* @newR @newR) 3))))
280
+ )
281
+ )
282
+ (render-ui main {
283
+ type: box
284
+ padding: md
285
+ children: [
286
+ { type: stack, direction: vertical, gap: md, children: [
287
+ { type: typography, variant: h4, content: @entity.title },
288
+ { type: stack, direction: horizontal, gap: sm, children: [
289
+ { type: button, action: ADD_POINT, label: @config.addPointLabel, variant: primary },
290
+ { type: button, action: REVEAL_FIT, label: @config.revealLabel, variant: secondary },
291
+ { type: button, action: TOGGLE_RUN, label: (if @entity.running @config.pauseLabel @config.runLabel), variant: secondary },
292
+ { type: button, action: RESET, label: @config.resetLabel, variant: ghost }
293
+ ] },
294
+ { type: typography, variant: caption, content: (str/concat @config.guessSlopeCaption (math/round @entity.guessSlope 2)) },
295
+ { type: range-slider, min: @config.guessSlopeMin, max: @config.guessSlopeMax, step: @config.guessSlopeStep, value: @entity.guessSlope, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "slope" } },
296
+ { type: typography, variant: caption, content: (str/concat @config.guessInterceptCaption (math/round @entity.guessIntercept 1)) },
297
+ { type: range-slider, min: @config.guessInterceptMin, max: @config.guessInterceptMax, step: @config.guessInterceptStep, value: @entity.guessIntercept, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "intercept" } },
298
+ { type: math-canvas, width: @config.width, height: @config.height, xMin: @config.axisXMin, xMax: @config.axisXMax, yMin: @config.axisYMin, yMax: @config.axisYMax, showAxes: @config.showAxes, showGrid: @config.showGrid, gridStep: @config.gridStep, showTickLabels: @config.showTickLabels, curves: @entity.curves, points: @entity.points, vectors: [], interactive: @config.interactive, animate: @config.animate },
299
+ { type: typography, variant: body, content: (if @config.showR (str/concat "r = " (math/round @entity.r 3) " — R² = " (math/round @entity.rSquared 3) " — guess score " @entity.guessScore "%") "") },
300
+ { type: typography, variant: body, content: @entity.narration }
301
+ ] }
302
+ ]
303
+ })
304
+ (emit LEARN_SCATTER_FIT_UPDATED { slope: @entity.slope, intercept: @entity.intercept, r: @entity.r, rSquared: @entity.rSquared, pointCount: (array/len @entity.points), narration: @entity.narration })
305
+
306
+ GUESS_LINE -> idle when (== @entity.active true)
307
+ (let (
308
+ (gSlope (if (== ?param "slope") ?value @entity.guessSlope))
309
+ (gIntercept (if (== ?param "intercept") ?value @entity.guessIntercept))
310
+ (pts @entity.points)
311
+ (n (array/len @pts))
312
+ (meanX (if (> @n 0) (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p x)))) @n) 0))
313
+ (meanY (if (> @n 0) (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p y)))) @n) 0))
314
+ (sxx (array/reduce @pts 0 (fn (acc p) (let ((dx (- (object/get @p x) @meanX))) (+ @acc (* @dx @dx))))))
315
+ (sxy (array/reduce @pts 0 (fn (acc p) (+ @acc (* (- (object/get @p x) @meanX) (- (object/get @p y) @meanY))))))
316
+ (newSlope (if (> @sxx 0) (/ @sxy @sxx) 0))
317
+ (newIntercept (- @meanY (* @newSlope @meanX)))
318
+ (ssFit (array/reduce @pts 0 (fn (acc p) (let ((e (- (object/get @p y) (+ (* @newSlope (object/get @p x)) @newIntercept)))) (+ @acc (* @e @e))))))
319
+ (ssGuess (array/reduce @pts 0 (fn (acc p) (let ((e (- (object/get @p y) (+ (* @gSlope (object/get @p x)) @gIntercept)))) (+ @acc (* @e @e))))))
320
+ (newScore (if (> @ssGuess 0) (math/round (math/clamp (* 100 (/ @ssFit @ssGuess)) 0 100)) 100))
321
+ (guessCurve { label: @config.guessLabel, color: @config.guessColor, samples: [
322
+ { x: @config.axisXMin, y: (math/clamp @gIntercept @config.axisYMin @config.axisYMax) },
323
+ { x: @config.axisXMax, y: (math/clamp (+ (* @gSlope @config.axisXMax) @gIntercept) @config.axisYMin @config.axisYMax) }
324
+ ]})
325
+ (fitCurve { label: @config.fitLabel, color: @config.fitColor, samples: [
326
+ { x: @config.axisXMin, y: (math/clamp @newIntercept @config.axisYMin @config.axisYMax) },
327
+ { x: @config.axisXMax, y: (math/clamp (+ (* @newSlope @config.axisXMax) @newIntercept) @config.axisYMin @config.axisYMax) }
328
+ ]})
329
+ )
330
+ (do
331
+ (set @entity.guessSlope @gSlope)
332
+ (set @entity.guessIntercept @gIntercept)
333
+ (set @entity.guessScore @newScore)
334
+ (set @entity.curves (if (and @config.guessMode (not @entity.revealed))
335
+ (array/append [] @guessCurve)
336
+ (if @config.guessMode
337
+ (array/append (array/append [] @guessCurve) @fitCurve)
338
+ (array/append [] @fitCurve))))
339
+ (set @entity.narration (str/concat @config.narrationGuessUpdatedTemplate " " (str/concat "" @newScore) "%"))
340
+ )
341
+ )
342
+ (render-ui main {
343
+ type: box
344
+ padding: md
345
+ children: [
346
+ { type: stack, direction: vertical, gap: md, children: [
347
+ { type: typography, variant: h4, content: @entity.title },
348
+ { type: stack, direction: horizontal, gap: sm, children: [
349
+ { type: button, action: ADD_POINT, label: @config.addPointLabel, variant: primary },
350
+ { type: button, action: REVEAL_FIT, label: @config.revealLabel, variant: secondary },
351
+ { type: button, action: TOGGLE_RUN, label: (if @entity.running @config.pauseLabel @config.runLabel), variant: secondary },
352
+ { type: button, action: RESET, label: @config.resetLabel, variant: ghost }
353
+ ] },
354
+ { type: typography, variant: caption, content: (str/concat @config.guessSlopeCaption (math/round @entity.guessSlope 2)) },
355
+ { type: range-slider, min: @config.guessSlopeMin, max: @config.guessSlopeMax, step: @config.guessSlopeStep, value: @entity.guessSlope, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "slope" } },
356
+ { type: typography, variant: caption, content: (str/concat @config.guessInterceptCaption (math/round @entity.guessIntercept 1)) },
357
+ { type: range-slider, min: @config.guessInterceptMin, max: @config.guessInterceptMax, step: @config.guessInterceptStep, value: @entity.guessIntercept, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "intercept" } },
358
+ { type: math-canvas, width: @config.width, height: @config.height, xMin: @config.axisXMin, xMax: @config.axisXMax, yMin: @config.axisYMin, yMax: @config.axisYMax, showAxes: @config.showAxes, showGrid: @config.showGrid, gridStep: @config.gridStep, showTickLabels: @config.showTickLabels, curves: @entity.curves, points: @entity.points, vectors: [], interactive: @config.interactive, animate: @config.animate },
359
+ { type: typography, variant: body, content: (if @config.showR (str/concat "r = " (math/round @entity.r 3) " — R² = " (math/round @entity.rSquared 3) " — guess score " @entity.guessScore "%") "") },
360
+ { type: typography, variant: body, content: @entity.narration }
361
+ ] }
362
+ ]
363
+ })
364
+ (emit LEARN_SCATTER_FIT_UPDATED { slope: @entity.slope, intercept: @entity.intercept, r: @entity.r, rSquared: @entity.rSquared, pointCount: (array/len @entity.points), narration: @entity.narration })
365
+
366
+ REVEAL_FIT -> idle when (== @entity.active true)
367
+ (set @entity.revealed true)
368
+ (let (
369
+ (fitCurve { label: @config.fitLabel, color: @config.fitColor, samples: [
370
+ { x: @config.axisXMin, y: (math/clamp @entity.intercept @config.axisYMin @config.axisYMax) },
371
+ { x: @config.axisXMax, y: (math/clamp (+ (* @entity.slope @config.axisXMax) @entity.intercept) @config.axisYMin @config.axisYMax) }
372
+ ]})
373
+ (guessCurve { label: @config.guessLabel, color: @config.guessColor, samples: [
374
+ { x: @config.axisXMin, y: (math/clamp @entity.guessIntercept @config.axisYMin @config.axisYMax) },
375
+ { x: @config.axisXMax, y: (math/clamp (+ (* @entity.guessSlope @config.axisXMax) @entity.guessIntercept) @config.axisYMin @config.axisYMax) }
376
+ ]})
377
+ )
378
+ (do
379
+ (set @entity.curves (if @config.guessMode
380
+ (array/append (array/append [] @guessCurve) @fitCurve)
381
+ (array/append [] @fitCurve)))
382
+ (set @entity.narration (str/concat @config.narrationFitRevealedTemplate " — r = " (str/concat "" (math/round @entity.r 3)) ", R² = " (str/concat "" (math/round @entity.rSquared 3)) " — guess score " (str/concat "" @entity.guessScore) "%"))
383
+ )
384
+ )
385
+ (render-ui main {
386
+ type: box
387
+ padding: md
388
+ children: [
389
+ { type: stack, direction: vertical, gap: md, children: [
390
+ { type: typography, variant: h4, content: @entity.title },
391
+ { type: stack, direction: horizontal, gap: sm, children: [
392
+ { type: button, action: ADD_POINT, label: @config.addPointLabel, variant: primary },
393
+ { type: button, action: REVEAL_FIT, label: @config.revealLabel, variant: secondary },
394
+ { type: button, action: TOGGLE_RUN, label: (if @entity.running @config.pauseLabel @config.runLabel), variant: secondary },
395
+ { type: button, action: RESET, label: @config.resetLabel, variant: ghost }
396
+ ] },
397
+ { type: typography, variant: caption, content: (str/concat @config.guessSlopeCaption (math/round @entity.guessSlope 2)) },
398
+ { type: range-slider, min: @config.guessSlopeMin, max: @config.guessSlopeMax, step: @config.guessSlopeStep, value: @entity.guessSlope, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "slope" } },
399
+ { type: typography, variant: caption, content: (str/concat @config.guessInterceptCaption (math/round @entity.guessIntercept 1)) },
400
+ { type: range-slider, min: @config.guessInterceptMin, max: @config.guessInterceptMax, step: @config.guessInterceptStep, value: @entity.guessIntercept, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "intercept" } },
401
+ { type: math-canvas, width: @config.width, height: @config.height, xMin: @config.axisXMin, xMax: @config.axisXMax, yMin: @config.axisYMin, yMax: @config.axisYMax, showAxes: @config.showAxes, showGrid: @config.showGrid, gridStep: @config.gridStep, showTickLabels: @config.showTickLabels, curves: @entity.curves, points: @entity.points, vectors: [], interactive: @config.interactive, animate: @config.animate },
402
+ { type: typography, variant: body, content: (if @config.showR (str/concat "r = " (math/round @entity.r 3) " — R² = " (math/round @entity.rSquared 3) " — guess score " @entity.guessScore "%") "") },
403
+ { type: typography, variant: body, content: @entity.narration }
404
+ ] }
405
+ ]
406
+ })
407
+ (emit LEARN_SCATTER_FIT_UPDATED { slope: @entity.slope, intercept: @entity.intercept, r: @entity.r, rSquared: @entity.rSquared, pointCount: (array/len @entity.points), narration: @entity.narration })
408
+
409
+ TOGGLE_RUN -> idle when (== @entity.active true)
410
+ (set @entity.running (not @entity.running))
411
+ (set @entity.narration (if @entity.running @config.narrationAutoAddingTemplate @config.narrationPausedTemplate))
412
+ (render-ui main {
413
+ type: box
414
+ padding: md
415
+ children: [
416
+ { type: stack, direction: vertical, gap: md, children: [
417
+ { type: typography, variant: h4, content: @entity.title },
418
+ { type: stack, direction: horizontal, gap: sm, children: [
419
+ { type: button, action: ADD_POINT, label: @config.addPointLabel, variant: primary },
420
+ { type: button, action: REVEAL_FIT, label: @config.revealLabel, variant: secondary },
421
+ { type: button, action: TOGGLE_RUN, label: (if @entity.running @config.pauseLabel @config.runLabel), variant: secondary },
422
+ { type: button, action: RESET, label: @config.resetLabel, variant: ghost }
423
+ ] },
424
+ { type: typography, variant: caption, content: (str/concat @config.guessSlopeCaption (math/round @entity.guessSlope 2)) },
425
+ { type: range-slider, min: @config.guessSlopeMin, max: @config.guessSlopeMax, step: @config.guessSlopeStep, value: @entity.guessSlope, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "slope" } },
426
+ { type: typography, variant: caption, content: (str/concat @config.guessInterceptCaption (math/round @entity.guessIntercept 1)) },
427
+ { type: range-slider, min: @config.guessInterceptMin, max: @config.guessInterceptMax, step: @config.guessInterceptStep, value: @entity.guessIntercept, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "intercept" } },
428
+ { type: math-canvas, width: @config.width, height: @config.height, xMin: @config.axisXMin, xMax: @config.axisXMax, yMin: @config.axisYMin, yMax: @config.axisYMax, showAxes: @config.showAxes, showGrid: @config.showGrid, gridStep: @config.gridStep, showTickLabels: @config.showTickLabels, curves: @entity.curves, points: @entity.points, vectors: [], interactive: @config.interactive, animate: @config.animate },
429
+ { type: typography, variant: body, content: (if @config.showR (str/concat "r = " (math/round @entity.r 3) " — R² = " (math/round @entity.rSquared 3) " — guess score " @entity.guessScore "%") "") },
430
+ { type: typography, variant: body, content: @entity.narration }
431
+ ] }
432
+ ]
433
+ })
434
+ (emit LEARN_SCATTER_FIT_UPDATED { slope: @entity.slope, intercept: @entity.intercept, r: @entity.r, rSquared: @entity.rSquared, pointCount: (array/len @entity.points), narration: @entity.narration })
435
+
436
+ RESET -> idle when (== @entity.active true)
437
+ (set @entity.points @config.initialPoints)
438
+ (set @entity.guessSlope 0)
439
+ (set @entity.guessIntercept (/ (+ @config.axisYMin @config.axisYMax) 2))
440
+ (set @entity.revealed false)
441
+ (set @entity.running @config.autoRun)
442
+ (let (
443
+ (pts @config.initialPoints)
444
+ (n (array/len @pts))
445
+ (meanX (if (> @n 0) (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p x)))) @n) 0))
446
+ (meanY (if (> @n 0) (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p y)))) @n) 0))
447
+ (sxx (array/reduce @pts 0 (fn (acc p) (let ((dx (- (object/get @p x) @meanX))) (+ @acc (* @dx @dx))))))
448
+ (syy (array/reduce @pts 0 (fn (acc p) (let ((dy (- (object/get @p y) @meanY))) (+ @acc (* @dy @dy))))))
449
+ (sxy (array/reduce @pts 0 (fn (acc p) (+ @acc (* (- (object/get @p x) @meanX) (- (object/get @p y) @meanY))))))
450
+ (newSlope (if (> @sxx 0) (/ @sxy @sxx) 0))
451
+ (newIntercept (- @meanY (* @newSlope @meanX)))
452
+ (newR (if (and (> @sxx 0) (> @syy 0)) (/ @sxy (math/sqrt (* @sxx @syy))) 0))
453
+ (fitCurve { label: @config.fitLabel, color: @config.fitColor, samples: [
454
+ { x: @config.axisXMin, y: (math/clamp @newIntercept @config.axisYMin @config.axisYMax) },
455
+ { x: @config.axisXMax, y: (math/clamp (+ (* @newSlope @config.axisXMax) @newIntercept) @config.axisYMin @config.axisYMax) }
456
+ ]})
457
+ (residualCurves (if @config.showResiduals
458
+ (array/map @pts (fn p { label: "", color: @config.residualColor, dash: @config.residualDash, samples: [ { x: (object/get @p x), y: (object/get @p y) }, { x: (object/get @p x), y: (+ (* @newSlope (object/get @p x)) @newIntercept) } ] }))
459
+ []))
460
+ )
461
+ (do
462
+ (set @entity.slope @newSlope)
463
+ (set @entity.intercept @newIntercept)
464
+ (set @entity.r @newR)
465
+ (set @entity.rSquared (* @newR @newR))
466
+ (set @entity.guessScore 0)
467
+ (set @entity.curves (array/concat (array/append [] @fitCurve) @residualCurves))
468
+ (set @entity.done (>= @n @config.maxPoints))
469
+ (set @entity.narration (str/concat (str/concat "" @n) " " @config.narrationLoadedTemplate " — r = " (str/concat "" (math/round @newR 3)) ", R² = " (str/concat "" (math/round (* @newR @newR) 3))))
470
+ )
471
+ )
472
+ (render-ui main {
473
+ type: box
474
+ padding: md
475
+ children: [
476
+ { type: stack, direction: vertical, gap: md, children: [
477
+ { type: typography, variant: h4, content: @entity.title },
478
+ { type: stack, direction: horizontal, gap: sm, children: [
479
+ { type: button, action: ADD_POINT, label: @config.addPointLabel, variant: primary },
480
+ { type: button, action: REVEAL_FIT, label: @config.revealLabel, variant: secondary },
481
+ { type: button, action: TOGGLE_RUN, label: (if @entity.running @config.pauseLabel @config.runLabel), variant: secondary },
482
+ { type: button, action: RESET, label: @config.resetLabel, variant: ghost }
483
+ ] },
484
+ { type: typography, variant: caption, content: (str/concat @config.guessSlopeCaption (math/round @entity.guessSlope 2)) },
485
+ { type: range-slider, min: @config.guessSlopeMin, max: @config.guessSlopeMax, step: @config.guessSlopeStep, value: @entity.guessSlope, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "slope" } },
486
+ { type: typography, variant: caption, content: (str/concat @config.guessInterceptCaption (math/round @entity.guessIntercept 1)) },
487
+ { type: range-slider, min: @config.guessInterceptMin, max: @config.guessInterceptMax, step: @config.guessInterceptStep, value: @entity.guessIntercept, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "intercept" } },
488
+ { type: math-canvas, width: @config.width, height: @config.height, xMin: @config.axisXMin, xMax: @config.axisXMax, yMin: @config.axisYMin, yMax: @config.axisYMax, showAxes: @config.showAxes, showGrid: @config.showGrid, gridStep: @config.gridStep, showTickLabels: @config.showTickLabels, curves: @entity.curves, points: @entity.points, vectors: [], interactive: @config.interactive, animate: @config.animate },
489
+ { type: typography, variant: body, content: (if @config.showR (str/concat "r = " (math/round @entity.r 3) " — R² = " (math/round @entity.rSquared 3) " — guess score " @entity.guessScore "%") "") },
490
+ { type: typography, variant: body, content: @entity.narration }
491
+ ] }
492
+ ]
493
+ })
494
+ (emit LEARN_SCATTER_FIT_UPDATED { slope: @entity.slope, intercept: @entity.intercept, r: @entity.r, rSquared: @entity.rSquared, pointCount: (array/len @entity.points), narration: @entity.narration })
495
+ }
496
+
497
+ ticks {
498
+ scatterFitTick every 600ms
499
+ when (and @entity.active (and @entity.running (not @entity.done)))
500
+ (let (
501
+ (marginX (* (- @config.axisXMax @config.axisXMin) @config.pointMarginPct))
502
+ (usableX (- (- @config.axisXMax @config.axisXMin) (* @marginX 2)))
503
+ (marginY (* (- @config.axisYMax @config.axisYMin) @config.pointMarginPct))
504
+ (usableY (- (- @config.axisYMax @config.axisYMin) (* @marginY 2)))
505
+ (newPoint { id: (str/concat "p" (array/len @entity.points)), x: (math/round (+ @config.axisXMin (+ @marginX (* (math/random) @usableX)))), y: (math/round (+ @config.axisYMin (+ @marginY (* (math/random) @usableY)))), label: "", color: @config.newPointColor, radius: @config.newPointRadius })
506
+ (pts (array/append @entity.points @newPoint))
507
+ (n (array/len @pts))
508
+ (meanX (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p x)))) @n))
509
+ (meanY (/ (array/reduce @pts 0 (fn (acc p) (+ @acc (object/get @p y)))) @n))
510
+ (sxx (array/reduce @pts 0 (fn (acc p) (let ((dx (- (object/get @p x) @meanX))) (+ @acc (* @dx @dx))))))
511
+ (syy (array/reduce @pts 0 (fn (acc p) (let ((dy (- (object/get @p y) @meanY))) (+ @acc (* @dy @dy))))))
512
+ (sxy (array/reduce @pts 0 (fn (acc p) (+ @acc (* (- (object/get @p x) @meanX) (- (object/get @p y) @meanY))))))
513
+ (newSlope (if (> @sxx 0) (/ @sxy @sxx) 0))
514
+ (newIntercept (- @meanY (* @newSlope @meanX)))
515
+ (newR (if (and (> @sxx 0) (> @syy 0)) (/ @sxy (math/sqrt (* @sxx @syy))) 0))
516
+ (fitCurve { label: @config.fitLabel, color: @config.fitColor, samples: [
517
+ { x: @config.axisXMin, y: (math/clamp @newIntercept @config.axisYMin @config.axisYMax) },
518
+ { x: @config.axisXMax, y: (math/clamp (+ (* @newSlope @config.axisXMax) @newIntercept) @config.axisYMin @config.axisYMax) }
519
+ ]})
520
+ (guessCurve { label: @config.guessLabel, color: @config.guessColor, samples: [
521
+ { x: @config.axisXMin, y: (math/clamp @entity.guessIntercept @config.axisYMin @config.axisYMax) },
522
+ { x: @config.axisXMax, y: (math/clamp (+ (* @entity.guessSlope @config.axisXMax) @entity.guessIntercept) @config.axisYMin @config.axisYMax) }
523
+ ]})
524
+ (residualCurves (if @config.showResiduals
525
+ (array/map @pts (fn p { label: "", color: @config.residualColor, dash: @config.residualDash, samples: [ { x: (object/get @p x), y: (object/get @p y) }, { x: (object/get @p x), y: (+ (* @newSlope (object/get @p x)) @newIntercept) } ] }))
526
+ []))
527
+ (baseCurves (if (and @config.guessMode (not @entity.revealed))
528
+ (array/append [] @guessCurve)
529
+ (if @config.guessMode
530
+ (array/append (array/append [] @guessCurve) @fitCurve)
531
+ (array/append [] @fitCurve))))
532
+ (nowDone (>= @n @config.maxPoints))
533
+ )
534
+ (do
535
+ (set @entity.points @pts)
536
+ (set @entity.slope @newSlope)
537
+ (set @entity.intercept @newIntercept)
538
+ (set @entity.r @newR)
539
+ (set @entity.rSquared (* @newR @newR))
540
+ (set @entity.curves (array/concat @baseCurves @residualCurves))
541
+ (set @entity.done @nowDone)
542
+ (set @entity.narration (str/concat @config.narrationAddedTemplate " " (str/concat "" @n) " — r = " (str/concat "" (math/round @newR 3)) ", R² = " (str/concat "" (math/round (* @newR @newR) 3)) (if @nowDone @config.narrationMaxReachedSuffix "")))
543
+ )
544
+ )
545
+ (render-ui main {
546
+ type: box
547
+ padding: md
548
+ children: [
549
+ { type: stack, direction: vertical, gap: md, children: [
550
+ { type: typography, variant: h4, content: @entity.title },
551
+ { type: stack, direction: horizontal, gap: sm, children: [
552
+ { type: button, action: ADD_POINT, label: @config.addPointLabel, variant: primary },
553
+ { type: button, action: REVEAL_FIT, label: @config.revealLabel, variant: secondary },
554
+ { type: button, action: TOGGLE_RUN, label: (if @entity.running @config.pauseLabel @config.runLabel), variant: secondary },
555
+ { type: button, action: RESET, label: @config.resetLabel, variant: ghost }
556
+ ] },
557
+ { type: typography, variant: caption, content: (str/concat @config.guessSlopeCaption (math/round @entity.guessSlope 2)) },
558
+ { type: range-slider, min: @config.guessSlopeMin, max: @config.guessSlopeMax, step: @config.guessSlopeStep, value: @entity.guessSlope, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "slope" } },
559
+ { type: typography, variant: caption, content: (str/concat @config.guessInterceptCaption (math/round @entity.guessIntercept 1)) },
560
+ { type: range-slider, min: @config.guessInterceptMin, max: @config.guessInterceptMax, step: @config.guessInterceptStep, value: @entity.guessIntercept, showTooltip: true, action: "GUESS_LINE", actionPayload: { param: "intercept" } },
561
+ { type: math-canvas, width: @config.width, height: @config.height, xMin: @config.axisXMin, xMax: @config.axisXMax, yMin: @config.axisYMin, yMax: @config.axisYMax, showAxes: @config.showAxes, showGrid: @config.showGrid, gridStep: @config.gridStep, showTickLabels: @config.showTickLabels, curves: @entity.curves, points: @entity.points, vectors: [], interactive: @config.interactive, animate: @config.animate },
562
+ { type: typography, variant: body, content: (if @config.showR (str/concat "r = " (math/round @entity.r 3) " — R² = " (math/round @entity.rSquared 3) " — guess score " @entity.guessScore "%") "") },
563
+ { type: typography, variant: body, content: @entity.narration }
564
+ ] }
565
+ ]
566
+ })
567
+ (emit LEARN_SCATTER_FIT_UPDATED { slope: @entity.slope, intercept: @entity.intercept, r: @entity.r, rSquared: @entity.rSquared, pointCount: (array/len @entity.points), narration: @entity.narration })
568
+ }
569
+
570
+ listens {
571
+ MODE_SELECTED {
572
+ mode : string
573
+ }
574
+ @description "Lab router broadcast naming the active mode; this instrument renders when the mode equals matchMode, otherwise renders null."
575
+ @tier "domain"
576
+ DRAG_POINT {
577
+ id : string
578
+ x : number
579
+ y : number
580
+ }
581
+ @description "A scatter point was dragged — id names the point, x/y its new coordinates (clamped to the axis ranges); the fit recomputes live."
582
+ @tier "domain"
583
+ GUESS_LINE {
584
+ param : string
585
+ value : number
586
+ }
587
+ @description "Learner dragged a guess-the-line slider — param is slope or intercept; the guess is scored against the least-squares fit."
588
+ @tier "domain"
589
+ }
590
+
591
+ emits {
592
+ LEARN_SCATTER_FIT_UPDATED -> external {
593
+ slope : number
594
+ intercept : number
595
+ r : number
596
+ rSquared : number
597
+ pointCount : number
598
+ narration : string
599
+ }
600
+ @description "The least-squares fit and narration after each learner action, auto-add tick, or reset — a composed listener (readout, grader) can subscribe; a harmless no-op when the atom runs standalone."
601
+ @synonyms "fit update, line of best fit, slope, intercept, correlation, r squared"
602
+ @tier "domain"
603
+ }
604
+
605
+ config {
606
+ matchMode : string = ""
607
+ @label "Match Mode"
608
+ @description "When composed behind a lab router, the MODE_SELECTED mode value this instrument responds to. Empty string means standalone — render immediately on INIT."
609
+ @tier "presentation"
610
+ autoRun : boolean = false
611
+ @label "Auto Run"
612
+ @description "When true, INIT/MODE_SELECTED start the tick auto-adding points up to Max Points; toggle live with the Run/Pause button."
613
+ @tier "domain"
614
+ title : string = "Scatter Fit"
615
+ @label "Title"
616
+ @description "Title shown above the canvas."
617
+ @tier "presentation"
618
+ initialPoints : [LearnScatterFitPoint] = [
619
+ { id: "p0", x: 15, y: 25, label: "", color: "#334155", radius: 5 },
620
+ { id: "p1", x: 32, y: 40, label: "", color: "#334155", radius: 5 },
621
+ { id: "p2", x: 48, y: 44, label: "", color: "#334155", radius: 5 },
622
+ { id: "p3", x: 63, y: 62, label: "", color: "#334155", radius: 5 },
623
+ { id: "p4", x: 80, y: 70, label: "", color: "#334155", radius: 5 }
624
+ ]
625
+ @label "Initial Points"
626
+ @description "Scatter points the canvas starts with, in axis data coordinates; Reset reseeds from this roster."
627
+ @synonyms "seed points, dataset, scatter data, sample"
628
+ @tier "domain"
629
+ guessMode : boolean = true
630
+ @label "Guess Mode"
631
+ @description "When true the learner's guessed line is shown instead of the fit until Reveal Fit, and the guess is scored against least squares."
632
+ @synonyms "guess the line, prediction, estimate the fit"
633
+ @tier "domain"
634
+ showResiduals : boolean = true
635
+ @label "Show Residuals"
636
+ @description "Draw a dashed segment from each point to the fit line — the errors least squares minimizes."
637
+ @synonyms "residuals, errors, deviations"
638
+ @tier "domain"
639
+ showR : boolean = true
640
+ @label "Show r and R²"
641
+ @description "Show the correlation coefficient r and coefficient of determination R² under the canvas."
642
+ @synonyms "correlation coefficient, r squared, goodness of fit"
643
+ @tier "domain"
644
+ maxPoints : number = 20
645
+ @label "Max Points"
646
+ @description "Cap on accumulated scatter points; Add Point and the auto-run tick stop once reached, until Reset."
647
+ @tier "domain"
648
+ width : number = 600
649
+ @label "Width"
650
+ @description "Canvas width in pixels."
651
+ @tier "presentation"
652
+ height : number = 400
653
+ @label "Height"
654
+ @description "Canvas height in pixels."
655
+ @tier "presentation"
656
+ axisXMin : number = 0
657
+ @label "X Axis Min"
658
+ @description "Lower bound of the canvas x axis, in data coordinates."
659
+ @tier "domain"
660
+ axisXMax : number = 100
661
+ @label "X Axis Max"
662
+ @description "Upper bound of the canvas x axis, in data coordinates; also the x endpoint the fit/guess lines are drawn to."
663
+ @tier "domain"
664
+ axisYMin : number = 0
665
+ @label "Y Axis Min"
666
+ @description "Lower bound of the canvas y axis; also the floor curve samples clamp to and the low end of the guess-intercept slider default range."
667
+ @tier "domain"
668
+ axisYMax : number = 100
669
+ @label "Y Axis Max"
670
+ @description "Upper bound of the canvas y axis; also the ceiling curve samples clamp to and the high end of the guess-intercept slider default range."
671
+ @tier "domain"
672
+ gridStep : number = 20
673
+ @label "Grid Step"
674
+ @description "Spacing between canvas gridlines, in data coordinates."
675
+ @tier "presentation"
676
+ showAxes : boolean = true
677
+ @label "Show Axes"
678
+ @tier "presentation"
679
+ showGrid : boolean = true
680
+ @label "Show Grid"
681
+ @tier "presentation"
682
+ showTickLabels : boolean = true
683
+ @label "Show Tick Labels"
684
+ @tier "presentation"
685
+ interactive : boolean = true
686
+ @label "Interactive"
687
+ @description "Enable pointer interaction on the canvas — required for dragging points."
688
+ @tier "presentation"
689
+ animate : boolean = false
690
+ @label "Animate"
691
+ @description "Enable continuous redraw loop."
692
+ @tier "presentation"
693
+ fitLabel : string = "fit"
694
+ @label "Fit Curve Label"
695
+ @description "Legend label for the computed least-squares fit line."
696
+ @tier "presentation"
697
+ fitColor : string = "#2563eb"
698
+ @label "Fit Curve Color"
699
+ @tier "presentation"
700
+ guessLabel : string = "your guess"
701
+ @label "Guess Curve Label"
702
+ @description "Legend label for the learner's proposed line, shown while guessMode is on and the fit isn't yet revealed."
703
+ @tier "presentation"
704
+ guessColor : string = "#dc2626"
705
+ @label "Guess Curve Color"
706
+ @tier "presentation"
707
+ residualColor : string = "#f59e0b"
708
+ @label "Residual Color"
709
+ @tier "presentation"
710
+ residualDash : "dashed" | "dotted" = "dashed"
711
+ @label "Residual Dash Style"
712
+ @tier "presentation"
713
+ newPointColor : string = "#334155"
714
+ @label "New Point Color"
715
+ @description "Fill color assigned to points created by Add Point or the auto-add tick."
716
+ @tier "presentation"
717
+ newPointRadius : number = 5
718
+ @label "New Point Radius"
719
+ @tier "presentation"
720
+ pointMarginPct : number = 0.05
721
+ @label "Point Margin Percent"
722
+ @description "Fraction of each axis span kept clear at both ends when Add Point/the tick place a new random point, so new points never land flush against the axis edge."
723
+ @tier "domain"
724
+ guessSlopeMin : number = -3
725
+ @label "Guess Slope Min"
726
+ @description "Lower bound of the guess-slope slider."
727
+ @tier "domain"
728
+ guessSlopeMax : number = 3
729
+ @label "Guess Slope Max"
730
+ @description "Upper bound of the guess-slope slider."
731
+ @tier "domain"
732
+ guessSlopeStep : number = 0.05
733
+ @label "Guess Slope Step"
734
+ @tier "domain"
735
+ guessInterceptMin : number = 0
736
+ @label "Guess Intercept Min"
737
+ @description "Lower bound of the guess-intercept slider; typically set to axisYMin."
738
+ @tier "domain"
739
+ guessInterceptMax : number = 100
740
+ @label "Guess Intercept Max"
741
+ @description "Upper bound of the guess-intercept slider; typically set to axisYMax."
742
+ @tier "domain"
743
+ guessInterceptStep : number = 1
744
+ @label "Guess Intercept Step"
745
+ @tier "domain"
746
+ addPointLabel : string = "Add Point"
747
+ @label "Add Point Button Label"
748
+ @tier "presentation"
749
+ revealLabel : string = "Reveal Fit"
750
+ @label "Reveal Fit Button Label"
751
+ @tier "presentation"
752
+ runLabel : string = "Run"
753
+ @label "Run Button Label"
754
+ @tier "presentation"
755
+ pauseLabel : string = "Pause"
756
+ @label "Pause Button Label"
757
+ @tier "presentation"
758
+ resetLabel : string = "Reset"
759
+ @label "Reset Button Label"
760
+ @tier "presentation"
761
+ guessSlopeCaption : string = "Guess slope: "
762
+ @label "Guess Slope Caption"
763
+ @description "Text prefixed to the current guess-slope value above its slider."
764
+ @tier "presentation"
765
+ guessInterceptCaption : string = "Guess intercept: "
766
+ @label "Guess Intercept Caption"
767
+ @description "Text prefixed to the current guess-intercept value above its slider."
768
+ @tier "presentation"
769
+ narrationLoadedTemplate : string = "point(s) loaded"
770
+ @label "Loaded Narration Template"
771
+ @description "Narration phrase after INIT/MODE_SELECTED/Reset load the seed points, assembled as \"<n> <template> — r = .., R² = ..\"."
772
+ @tier "domain"
773
+ narrationAddedTemplate : string = "point added"
774
+ @label "Added Narration Template"
775
+ @description "Narration phrase after a point is added (button or tick), assembled as \"<template> <n> — r = .., R² = ..\"."
776
+ @tier "domain"
777
+ narrationMovedTemplate : string = "point moved"
778
+ @label "Moved Narration Template"
779
+ @description "Narration phrase after a point is dragged, assembled as \"<template> — r = .., R² = ..\"."
780
+ @tier "domain"
781
+ narrationGuessUpdatedTemplate : string = "guess updated — score"
782
+ @label "Guess Updated Narration Template"
783
+ @description "Narration phrase after a guess slider moves, assembled as \"<template> <score>%\"."
784
+ @tier "domain"
785
+ narrationFitRevealedTemplate : string = "fit revealed"
786
+ @label "Fit Revealed Narration Template"
787
+ @description "Narration phrase after Reveal Fit, assembled as \"<template> — r = .., R² = .. — guess score <score>%\"."
788
+ @tier "domain"
789
+ narrationMaxReachedSuffix : string = " — max points reached"
790
+ @label "Max Reached Narration Suffix"
791
+ @description "Suffix appended to the added-point narration once maxPoints is hit."
792
+ @tier "domain"
793
+ narrationAutoAddingTemplate : string = "Auto-adding — tick adds a new point."
794
+ @label "Auto-Adding Narration"
795
+ @description "Narration shown after Run is pressed."
796
+ @tier "domain"
797
+ narrationPausedTemplate : string = "Paused — add manually with Add Point."
798
+ @label "Paused Narration"
799
+ @description "Narration shown after Pause is pressed."
800
+ @tier "domain"
801
+ }
802
+ }
803
+
804
+ page "/learn-scatter-fit" as LearnScatterFitPage -> LearnScatterFitSim
805
+ }