-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathPlotController.j
More file actions
279 lines (236 loc) · 10.3 KB
/
PlotController.j
File metadata and controls
279 lines (236 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
/*
This file is part of FrACT10, a vision test battery.
© 2025 Michael Bach, bach@uni-freiburg.de, <https://michaelbach.de>
PlotController.j
*/
@import "AppController.j"
@import "MDB2plot.j"
/**
For plotting test results history
Created by Bach on 2025-05-29
*/
const buttonsWidth = 130, buttonsHeight = 23, buttonsOkWidth = 48;
const buttonsY = (kFractHeight - buttonsHeight - 18);
@implementation PlotView: CPView {
CPString firstTime;
id testHistory;
float testHistoryResultValue;
BOOL isAcuity, isContrast;
}
- (id) initWithFrame: (CGRect) theFrame { //CPLog("PlotView>initWithFrame");
self = [super initWithFrame: theFrame];
if (gTestingPlottingAcuity1Contrast2 == 1) { //this only needed for testing
testHistoryResultValue = 0.149;
testHistory = [
{value: 1, isCorrect: true},
{value: 0.7, isCorrect: true},
{value: 0.4, isCorrect: true},
{value: 0.1, isCorrect: true},
{value: -0.033, isCorrect: false},
{value: 0.139, isCorrect: false},
{value: 0.284, isCorrect: true},
{value: 0.203, isCorrect: true},
{value: 0.136, isCorrect: true},
{value: 0.078, isCorrect: false},
{value: 0.160, isCorrect: true},
{value: 0.615, isCorrect: true},
{value: 0.101, isCorrect: true},
{value: 0.060, isCorrect: false},
{value: 0.121, isCorrect: true},
{value: 0.085, isCorrect: false},
{value: 0.138, isCorrect: true},
{value: 0.609, isCorrect: true}
]
}
if (gTestingPlottingAcuity1Contrast2 == 2) { //this only needed for testing
testHistoryResultValue = 1.832;
testHistory = [
{value: 0.584, isCorrect: true},
{value: 1.796, isCorrect: false},
{value: 1.192, isCorrect: true},
{value: 1.498, isCorrect: true},
{value: 1.709, isCorrect: true},
{value: 1.881, isCorrect: false},
{value: 1.724, isCorrect: true},
{value: 1.832, isCorrect: false},
{value: 1.724, isCorrect: true},
{value: 1.802, isCorrect: false},
{value: 1.719, isCorrect: true},
{value: 1.181, isCorrect: true},
{value: 1.797, isCorrect: false},
{value: 1.732, isCorrect: true},
{value: 1.781, isCorrect: true},
{value: 1.827, isCorrect: true},
{value: 1.870, isCorrect: false},
{value: 1.221, isCorrect: true}
]
}
return self;
}
- (void) drawRect: (CGRect) dirtyRect { //CPLog("PlotView>drawRect");
if (firstTime !== "notFirstTime") { //ignore the very first call during launching
firstTime = "notFirstTime"; return;
}
[MDB2plot p2init];
if (gTestingPlottingAcuity1Contrast2 === 0) {
testHistory = [TrialHistoryController trialHistoryRecord];
testHistoryResultValue = gTestDetails[td_resultValue];
isAcuity = gTestDetails[td_testName].startsWith("Acuity");
isContrast = gTestDetails[td_testName].startsWith("Contrast");
} else {
isAcuity = gTestingPlottingAcuity1Contrast2 == 1;
isContrast = gTestingPlottingAcuity1Contrast2 == 2;
}
const nTrials = testHistory.length;
let yMin = 3, yMax = -1.05, yHorAxis = yMin; //note inverted axis
if (isContrast) {
yMin = 0.0, yMax = 2.5, yHorAxis = yMin; //note normal axis
}
const yTick = (yMax - yMin) / 50;
const xMin = -1, xMax = nTrials + 1;
const xTick = (xMax - xMin) / 80;
[MDB2plot p2wndwX0: xMin-0.01 y0: yMin x1: xMax y1: yMax];
//title
[MDB2plot p2setFontSize: 24];
[MDB2plot p2setTextAlignHorizontal: "center" vertical: "hanging"];
const sHeader = "Presented " + (isAcuity ? "acuity" : "logCS") + " grades along the run";
[MDB2plot p2showText: sHeader atX: (xMax - xMin - 2) / 2 y: yMax];
if ([Settings doThreshCorrection] && isAcuity) {
[MDB2plot p2setFontSize: 14];
[MDB2plot p2showText: "[All with DIN/ISO threshold correction]" atX: (xMax - xMin - 2) / 2 y: yMax +0.21];
}
[MDB2plot p2setTextAlignDefault];
//axes
//abscissa
[MDB2plot p2setFontSize: 18];
[MDB2plot p2hlineX0: xMin y: yHorAxis x1: nTrials];
[MDB2plot p2setTextAlignHorizontal: "end" vertical: "bottom"];
[MDB2plot p2showText: "Trials→" atX: xMax - 1 y: yHorAxis + 4 * yTick];
[MDB2plot p2setTextAlignHorizontal: "center"];
for (let trial = 1; trial <= nTrials; trial++) {
[MDB2plot p2vlineX: trial-0.5 y0: yHorAxis + yTick y1: yHorAxis];
if ([Misc isOdd: trial]) {
[MDB2plot p2showText: trial atX: trial-0.5 y: yHorAxis + yTick ];
}
}
[MDB2plot p2setTextAlignDefault];
//ordinate
[MDB2plot p2vlineX: xMin y0: yMin y1: yMax];
const sUnit = isAcuity ? "↓LogMAR" : "↑logCS";
[MDB2plot p2showText: sUnit atXpx: [MDB2plot p2tx: xMin+0.5] ypx: 40];
for (let y = -1; y < 3; y++) {
[MDB2plot p2hlineX0: xMin y: y x1: xMin + xTick];
[MDB2plot p2showText: y atX: xMin + xTick +0.1 y: y];
}
for (let y = -1; y < 3; y+=0.1) {
[MDB2plot p2hlineX0: xMin y: y x1: xMin + xTick/2];
}
//test points
[MDB2plot p2setLineWidthInPx: 4];
for (let trial = 0; trial < nTrials; trial++) {
let y = testHistory[trial].value;
if ([Settings doThreshCorrection] && isAcuity) y -= Math.log10(kThresholdCorrectionFactor4Ascending);
if (testHistory[trial].isCorrect) {
[MDB2plot p2setFillColor: [CPColor colorWithRed: 0 green: 0.6 blue: 0 alpha: 1]];
[MDB2plot p2fillCircleAtX: trial+0.5 y: y radiusInPx: 10];
} else {
[MDB2plot p2setStrokeColor: [CPColor redColor]];
[MDB2plot p2strokeXAtX: trial+0.5 y: y sizeInPx: 25];
}
}
//line for final value
[MDB2plot p2setStrokeColor: [CPColor blueColor]];
[MDB2plot p2setLineWidthInPx: 2];
[MDB2plot p2hlineX0: xMin+1.5 y: testHistoryResultValue x1: xMax];
[MDB2plot p2setFillColor: [CPColor blueColor]];
[MDB2plot p2setTextAlignVertical: "top"];
let s = [Misc stringFromNumber: testHistoryResultValue decimals: 2];
[MDB2plot p2showText: s+"↑" atXpx: [MDB2plot p2tx: xMin+1.5] ypx: [MDB2plot p2ty: testHistoryResultValue]+8];
}
@end
let SharedPlotController = nil;
@implementation PlotController: CPWindowController {
CPPanel plotPanel;
PlotView plotView;
}
//not really necessary, because `PlotController` is instantiated in the XIB
+ (PlotController) sharedController {
if (!SharedPlotController) {
SharedPlotController = [[PlotController alloc] init];
}
return SharedPlotController;
}
- (id) init { //console.info("PlotController>init")
self = [super init];
if (self) {
[self createPlotPanel];
}
return self;
}
- (void) createPlotPanel {
plotPanel = [[CPPanel alloc] initWithContentRect: CGRectMake(0, 0, kFractWidth, kFractHeight) styleMask: CPTitledWindowMask];
[plotPanel setTitle: "FrACT₁₀ – Plot"];
const contentView = [plotPanel contentView];
plotView = [[PlotView alloc] initWithFrame: CGRectMake(kGuiMarginHorizontal, 16, 760, 530)];
[contentView addSubview: plotView];
const btnPdf = [CPButton buttonWithTitle: "Plot → PDF"];
[btnPdf setFrame: CGRectMake(kGuiMarginHorizontal, buttonsY, 84, buttonsHeight)];
[btnPdf setTarget: self]; [btnPdf setAction: @selector(buttonPlotToPDF_action:)];
[btnPdf setKeyEquivalent: "p"];
[contentView addSubview: btnPdf];
const btnOk = [CPButton buttonWithTitle: "OK"];
[btnOk setFrame: CGRectMake(734, buttonsY, buttonsOkWidth, buttonsHeight)];
[btnOk setTarget: self]; [btnOk setAction: @selector(buttonPlotClose_action:)];
[btnOk setKeyEquivalent: "\r"];
[contentView addSubview: btnOk];
}
- (IBAction) buttonPlotOpen_action: (id) sender { //CPLog("PlotView>buttonPlotOpen_action");
if (!plotPanel) [self createPlotPanel];
[plotPanel setMovable: NO]; [Misc centerWindowOrPanel: plotPanel];
[plotPanel makeKeyAndOrderFront: self];
}
- (IBAction) buttonPlotClose_action: (id) sender { //CPLog("PlotView>buttonPlotClose_action");
[plotPanel close];
}
- (IBAction) buttonPlotToPDF_action: (id) sender { //CPLog("PlotView>buttonPlotToPDF_action");
const canvas = [MDB2plot getCGC].DOMElement;
const imgData = canvas.toDataURL("image/png", 1.0);
// Default is 'pt' units and 'a4' size
const doc = new window.jspdf.jsPDF({
title: 'FrACT10 RESULT PLOT',
author: 'bach@uni-freiburg.de',
keywords: 'visual acuity',
creator: "FrACT10_" + gVersionStringOfFract + "·" + gVersionDateOfFrACT,
orientation: 'portrait', unit: 'pt', format: 'a4'
});
doc.setFontSize(10); doc.setFont("Courier", "bold");
doc.text("FrACT10 RESULT PLOT", 15, 10);
let tableBody = [ //let's build a table
['Date', gTestDetails[td_dateOfRunStart]],
['Time', [Misc date2HH_MM: gTestDetails[td_dateTimeOfRunStart]]]
];
if (gTestDetails[td_ID] !== gPatIDdefault) {
tableBody.push(["ID", gTestDetails[td_ID]]);
}
if (gTestDetails[td_eyeCondition] !== gEyeIndex2string[0]) { //optional
tableBody.push(["Eye", gTestDetails[td_eyeCondition]]);
}
tableBody.push(["Test", gTestDetails[td_testName]]);
const styles = {
fontSize: 10, font: "Courier", fontStyle: 'normal', halign: 'left',
cellPadding: {top: 1, right: 1, bottom: 1, left: 1},
};
const columnStyles = {0: {cellWidth: 40}, 1: {cellWidth: 'auto'},}
doc.autoTable({body: tableBody, theme: 'grid', styles: styles, columnStyles: columnStyles});
// Scale image to fit (keeping aspect ratio)
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const canvasWidth = canvas.width, canvasHeight = canvas.height;
const ratio = 0.8 * Math.min(pageWidth / canvasWidth, pageHeight / canvasHeight);
const imgWidth = canvasWidth * ratio, imgHeight = canvasHeight * ratio;
const x = (pageWidth - imgWidth) / 2, y = (pageHeight - imgHeight) / 2; //Center it
doc.addImage(imgData, "PNG", x, y, imgWidth, imgHeight);
const filename = "FrACT_"+ gTestDetails[td_dateOfRunStart] + "_" + [Misc date2HHdashMM: gTestDetails[td_dateTimeOfRunStart]] + "_AllTrialsPlot.pdf";
doc.save(filename);
}
@end