maidr 1.0.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.
- package/.Rbuildignore +1 -0
- package/.eslintignore +3 -0
- package/.eslintrc.json +6 -0
- package/.github/workflows/build.yml +20 -0
- package/.prettierignore +3 -0
- package/.prettierrc.json +7 -0
- package/.vscode/extensions.json +25 -0
- package/.vscode/settings.json +30 -0
- package/.vscode/tasks.json +57 -0
- package/CHANGELOG.md +7 -0
- package/CITATION.cff +21 -0
- package/CONTRIBUTING.md +87 -0
- package/LICENSE.md +595 -0
- package/README.md +341 -0
- package/dist/maidr.js +8851 -0
- package/dist/maidr.min.js +1 -0
- package/dist/styles.css +244 -0
- package/dist/styles.min.css +1 -0
- package/docs/Audio.html +1398 -0
- package/docs/Constants.html +256 -0
- package/docs/Description.html +582 -0
- package/docs/Helper.html +364 -0
- package/docs/LogError.html +905 -0
- package/docs/Menu.html +665 -0
- package/docs/Position.html +174 -0
- package/docs/Resources.html +338 -0
- package/docs/Review.html +333 -0
- package/docs/Tracker.html +965 -0
- package/docs/audio.js.html +635 -0
- package/docs/constants.js.html +1242 -0
- package/docs/display.js.html +1184 -0
- package/docs/fonts/OpenSans-Bold-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Bold-webfont.svg +1830 -0
- package/docs/fonts/OpenSans-Bold-webfont.woff +0 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.svg +1830 -0
- package/docs/fonts/OpenSans-BoldItalic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Italic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Italic-webfont.svg +1830 -0
- package/docs/fonts/OpenSans-Italic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Light-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Light-webfont.svg +1831 -0
- package/docs/fonts/OpenSans-Light-webfont.woff +0 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.svg +1835 -0
- package/docs/fonts/OpenSans-LightItalic-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Regular-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Regular-webfont.svg +1831 -0
- package/docs/fonts/OpenSans-Regular-webfont.woff +0 -0
- package/docs/fonts/OpenSans-Semibold-webfont.eot +0 -0
- package/docs/fonts/OpenSans-Semibold-webfont.svg +1830 -0
- package/docs/fonts/OpenSans-Semibold-webfont.ttf +0 -0
- package/docs/fonts/OpenSans-Semibold-webfont.woff +0 -0
- package/docs/fonts/OpenSans-SemiboldItalic-webfont.eot +0 -0
- package/docs/fonts/OpenSans-SemiboldItalic-webfont.svg +1830 -0
- package/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf +0 -0
- package/docs/fonts/OpenSans-SemiboldItalic-webfont.woff +0 -0
- package/docs/index.html +66 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/styles/jsdoc-default.css +692 -0
- package/docs/styles/prettify-jsdoc.css +111 -0
- package/docs/styles/prettify-tomorrow.css +132 -0
- package/examples/dev_charts/barplot.html +1056 -0
- package/examples/dev_charts/boxplot.html +1856 -0
- package/examples/dev_charts/boxplot_flipped.svg +727 -0
- package/examples/dev_charts/heatmap.html +1217 -0
- package/examples/dev_charts/scatterplot/displ.js +18 -0
- package/examples/dev_charts/scatterplot/histogram_for_residual.svg +595 -0
- package/examples/dev_charts/scatterplot/hwy.js +15 -0
- package/examples/dev_charts/scatterplot/layers/point_layer.json +938 -0
- package/examples/dev_charts/scatterplot/layers/smooth_layer.json +322 -0
- package/examples/dev_charts/scatterplot/point_layer.js +938 -0
- package/examples/dev_charts/scatterplot/prediction_array.js +31 -0
- package/examples/dev_charts/scatterplot/prediction_array.json +31 -0
- package/examples/dev_charts/scatterplot/residual_array.js +29 -0
- package/examples/dev_charts/scatterplot/residual_array.json +29 -0
- package/examples/dev_charts/scatterplot/scatterplot.svg +1428 -0
- package/examples/dev_charts/scatterplot/scatterplot_data.html +2838 -0
- package/examples/dev_charts/scatterplot/scatterplot_no_jitter_point_only.svg +1393 -0
- package/examples/dev_charts/scatterplot/scatterplot_no_jitter_with_bestfit.svg +1424 -0
- package/examples/dev_charts/scatterplot/scatterplot_no_jitter_with_loess_curve.svg +1402 -0
- package/examples/dev_charts/scatterplot/smooth_layer.js +322 -0
- package/examples/dev_charts/scatterplot.html +4560 -0
- package/examples/dodged_bar/dodged_bar.png +0 -0
- package/examples/dodged_bar/dodged_bar.svg +198 -0
- package/examples/dodged_bar/schema.json +41 -0
- package/examples/histogram/histogram_tutorial.svg +482 -0
- package/examples/histogram/histogram_tutorial_raw_data.json +362 -0
- package/examples/histogram/histogram_user_study.svg +578 -0
- package/examples/histogram/histogram_user_study_raw_data.json +362 -0
- package/examples/lineplot/lineplot_sample.svg +126 -0
- package/examples/lineplot/lineplot_sample_raw_data.json +1 -0
- package/examples/lineplot/point+lineplot_sample.svg +700 -0
- package/examples/other/audio_oscillator_boxplot.js +95 -0
- package/examples/other/barplot_labels.svg +314 -0
- package/examples/other/barplot_user_study.svg +313 -0
- package/examples/other/boxplot.html +927 -0
- package/examples/other/boxplot_data_frame.html +568 -0
- package/examples/other/boxplot_label.svg +751 -0
- package/examples/other/braille-display_boxplot.js +79 -0
- package/examples/other/control_boxplot.js +55 -0
- package/examples/other/draft.js +56 -0
- package/examples/other/getData.html +400 -0
- package/examples/other/getData.js +41 -0
- package/examples/other/ggplot_to_svg.R +371 -0
- package/examples/other/heatmap.svg +582 -0
- package/examples/other/heatmap_label.svg +608 -0
- package/examples/other/multiple_barplot.html +2250 -0
- package/examples/other/new_scatterplot_user_study_point_layer.json +122 -0
- package/examples/other/py_binder_output.html +1167 -0
- package/examples/other/scatterplot_label.svg +1429 -0
- package/examples/other/seaborn_plot.py +9 -0
- package/examples/other/svglite_bar.svg +136 -0
- package/examples/other/tutorial_boxplot.svg +727 -0
- package/examples/other/tutorial_boxplot_data.json +72 -0
- package/examples/other/user_study_boxplot.svg +676 -0
- package/examples/stacked_bar/schema.json +41 -0
- package/examples/stacked_bar/stack_bar.png +0 -0
- package/examples/stacked_bar/stacked_bar.svg +180 -0
- package/examples/stacked_normalized_bar/stacked_normalized_bar.png +0 -0
- package/examples/stacked_normalized_bar/stacked_normalized_bar.svg +189 -0
- package/examples/static/barplot.svg +263 -0
- package/examples/static/barplot_diamonds_gridSVG.svg +254 -0
- package/examples/static/boxplot.svg +424 -0
- package/examples/static/heatmap.svg +373 -0
- package/examples/static/heatmap_penguins_table.html +486 -0
- package/examples/static/scatterplot.svg +530 -0
- package/examples/svglite/task_heatmap.html +802 -0
- package/examples/svglite/task_heatmap.svg +111 -0
- package/examples/svglite/tutorial_bar.svg +136 -0
- package/examples/svglite/tutorial_bar_plot.html +504 -0
- package/examples/svglite/tutorial_boxplot.html +1850 -0
- package/examples/svglite/tutorial_boxplot.svg +727 -0
- package/examples/svglite/tutorial_scatterplot.html +3135 -0
- package/examples/svglite/tutorial_scatterplot.svg +311 -0
- package/gulpfile.js +49 -0
- package/index.html +40 -0
- package/jsconfig.json +10 -0
- package/jsdoc.json +19 -0
- package/package.json +47 -0
- package/src/css/styles.css +241 -0
- package/src/js/__tests__/audio.test.js +49 -0
- package/src/js/__tests__/constants.test.js +622 -0
- package/src/js/audio.js +575 -0
- package/src/js/barplot.js +254 -0
- package/src/js/boxplot.js +682 -0
- package/src/js/constants.js +1182 -0
- package/src/js/controls.js +3182 -0
- package/src/js/display.js +1124 -0
- package/src/js/heatmap.js +411 -0
- package/src/js/histogram.js +134 -0
- package/src/js/init.js +427 -0
- package/src/js/lineplot.js +219 -0
- package/src/js/scatterplot.js +619 -0
- package/src/js/segmented.js +268 -0
- package/user_study_pilot/binder_test.html +526 -0
- package/user_study_pilot/data/barplot_user_study.svg +492 -0
- package/user_study_pilot/data/barplot_user_study_raw_data.json +22 -0
- package/user_study_pilot/data/boxplot_tutorial.json +72 -0
- package/user_study_pilot/data/boxplot_tutorial_horizontal.svg +727 -0
- package/user_study_pilot/data/boxplot_user_study.json +52 -0
- package/user_study_pilot/data/boxplot_user_study_vertical.svg +675 -0
- package/user_study_pilot/data/boxplot_user_study_vertical_horizontal.svg +676 -0
- package/user_study_pilot/data/heatmap_user_study.svg +719 -0
- package/user_study_pilot/data/heatmap_user_study_raw_data.json +127 -0
- package/user_study_pilot/data/new_barplot_user_study.svg +269 -0
- package/user_study_pilot/data/new_heatmap_user_study.svg +367 -0
- package/user_study_pilot/data/new_scatterplot_user_study.svg +603 -0
- package/user_study_pilot/data/new_scatterplot_user_study_point_layer.json +122 -0
- package/user_study_pilot/data/scatterplot_user_study (1).svg +321 -0
- package/user_study_pilot/data/scatterplot_user_study.svg +603 -0
- package/user_study_pilot/data/scatterplot_user_study_point_layer.json +122 -0
- package/user_study_pilot/data/scatterplot_user_study_smooth_layer.json +322 -0
- package/user_study_pilot/intro.html +215 -0
- package/user_study_pilot/jaws_settings/Chrome.JDF +10 -0
- package/user_study_pilot/jaws_settings/Firefox.JDF +10 -0
- package/user_study_pilot/jaws_settings/backup_utf8/Chrome.JDF +10 -0
- package/user_study_pilot/jaws_settings/backup_utf8/Firefox.JDF +10 -0
- package/user_study_pilot/jaws_settings/backup_utf8/msedge.JDF +10 -0
- package/user_study_pilot/jaws_settings/msedge.JDF +10 -0
- package/user_study_pilot/nvda_settings/chrome.dic +10 -0
- package/user_study_pilot/nvda_settings/default.dic +10 -0
- package/user_study_pilot/nvda_settings/firefox.dic +10 -0
- package/user_study_pilot/nvda_settings/msedge.dic +10 -0
- package/user_study_pilot/scatterplot.html +4560 -0
- package/user_study_pilot/seaborn_test.html +1059 -0
- package/user_study_pilot/svglite_test.html +534 -0
- package/user_study_pilot/task1_bar_plot.html +1111 -0
- package/user_study_pilot/task2_heatmap.html +1661 -0
- package/user_study_pilot/task3_boxplot_horizontal.html +1690 -0
- package/user_study_pilot/task3_boxplot_vertical.html +1689 -0
- package/user_study_pilot/task4_scatterplot.html +2091 -0
- package/user_study_pilot/tutorial1_bar_plot.html +1159 -0
- package/user_study_pilot/tutorial2_heatmap.html +1276 -0
- package/user_study_pilot/tutorial3_boxplot_horizontal.html +1861 -0
- package/user_study_pilot/tutorial3_boxplot_vertical.html +1807 -0
- package/user_study_pilot/tutorial4_scatterplot.html +5893 -0
- package/user_study_pilot/tutorial5_histogram.html +1553 -0
- package/user_study_pilot/tutorial6_lineplot.html +1011 -0
- package/user_study_pilot/tutorial7_stacked.html +763 -0
- package/user_study_pilot/tutorial8_stacked_normalized.html +796 -0
- package/user_study_pilot/tutorial9_dodged_bar.html +831 -0
- package/user_study_pilot/voiceover_settings/user_study_VoiceOver Archive.voprefs +573 -0
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
class Display {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.infoDiv = constants.infoDiv;
|
|
4
|
+
|
|
5
|
+
this.x = {};
|
|
6
|
+
this.x.id = 'x';
|
|
7
|
+
this.x.textBase = 'x-value: ';
|
|
8
|
+
|
|
9
|
+
this.y = {};
|
|
10
|
+
this.y.id = 'y';
|
|
11
|
+
this.y.textBase = 'y-value: ';
|
|
12
|
+
|
|
13
|
+
this.boxplotGridPlaceholders = [
|
|
14
|
+
resources.GetString('lower_outlier'),
|
|
15
|
+
resources.GetString('min'),
|
|
16
|
+
resources.GetString('25'),
|
|
17
|
+
resources.GetString('50'),
|
|
18
|
+
resources.GetString('75'),
|
|
19
|
+
resources.GetString('max'),
|
|
20
|
+
resources.GetString('upper_outlier'),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
toggleTextMode() {
|
|
25
|
+
if (constants.textMode == 'off') {
|
|
26
|
+
constants.textMode = 'terse';
|
|
27
|
+
} else if (constants.textMode == 'terse') {
|
|
28
|
+
constants.textMode = 'verbose';
|
|
29
|
+
} else if (constants.textMode == 'verbose') {
|
|
30
|
+
constants.textMode = 'off';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.announceText(
|
|
34
|
+
'<span aria-hidden="true">Text mode:</span> ' + constants.textMode
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
toggleBrailleMode(onoff) {
|
|
39
|
+
if (constants.chartType == 'point') {
|
|
40
|
+
this.announceText('Braille is not supported in point layer.');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (typeof onoff === 'undefined') {
|
|
44
|
+
if (typeof constants.brailleMode === 'undefined') {
|
|
45
|
+
constants.brailleMode = 'off';
|
|
46
|
+
onoff = constants.brailleMode == 'on';
|
|
47
|
+
} else {
|
|
48
|
+
// switch on/off
|
|
49
|
+
if (constants.brailleMode == 'on') {
|
|
50
|
+
onoff = 'off';
|
|
51
|
+
} else {
|
|
52
|
+
onoff = 'on';
|
|
53
|
+
}
|
|
54
|
+
constants.brailleMode = onoff;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (onoff == 'on') {
|
|
58
|
+
if (constants.chartType == 'box') {
|
|
59
|
+
// braille mode is on before any plot is selected
|
|
60
|
+
if (
|
|
61
|
+
constants.plotOrientation != 'vert' &&
|
|
62
|
+
position.x == -1 &&
|
|
63
|
+
position.y == plot.plotData.length
|
|
64
|
+
) {
|
|
65
|
+
position.x += 1;
|
|
66
|
+
position.y -= 1;
|
|
67
|
+
} else if (
|
|
68
|
+
constants.plotOrientation == 'vert' &&
|
|
69
|
+
position.x == 0 &&
|
|
70
|
+
position.y == plot.plotData[0].length - 1
|
|
71
|
+
) {
|
|
72
|
+
// do nothing; don't think there's any problem
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
constants.brailleMode = 'on';
|
|
77
|
+
document
|
|
78
|
+
.getElementById(constants.braille_container_id)
|
|
79
|
+
.classList.remove('hidden');
|
|
80
|
+
constants.brailleInput.focus();
|
|
81
|
+
constants.brailleInput.setSelectionRange(position.x, position.x);
|
|
82
|
+
|
|
83
|
+
this.SetBraille();
|
|
84
|
+
|
|
85
|
+
if (constants.chartType == 'heat') {
|
|
86
|
+
let pos = position.y * (plot.num_cols + 1) + position.x;
|
|
87
|
+
constants.brailleInput.setSelectionRange(pos, pos);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// braille mode is on before navigation of chart
|
|
91
|
+
// very important to make sure braille works properly
|
|
92
|
+
if (position.x == -1 && position.y == -1) {
|
|
93
|
+
constants.brailleInput.setSelectionRange(0, 0);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
constants.brailleMode = 'off';
|
|
97
|
+
document
|
|
98
|
+
.getElementById(constants.braille_container_id)
|
|
99
|
+
.classList.add('hidden');
|
|
100
|
+
|
|
101
|
+
if (constants.review_container) {
|
|
102
|
+
if (!constants.review_container.classList.contains('hidden')) {
|
|
103
|
+
constants.review.focus();
|
|
104
|
+
} else {
|
|
105
|
+
constants.chart.focus();
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
constants.chart.focus();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.announceText('Braille ' + constants.brailleMode);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
toggleSonificationMode() {
|
|
116
|
+
if (
|
|
117
|
+
constants.chartType == 'point' ||
|
|
118
|
+
constants.chartType == 'stacked_bar' ||
|
|
119
|
+
constants.chartType == 'stacked_normalized_bar' ||
|
|
120
|
+
constants.chartType == 'dodged_bar'
|
|
121
|
+
) {
|
|
122
|
+
if (constants.sonifMode == 'off') {
|
|
123
|
+
constants.sonifMode = 'on';
|
|
124
|
+
this.announceText(resources.GetString('son_sep'));
|
|
125
|
+
} else if (constants.sonifMode == 'on') {
|
|
126
|
+
constants.sonifMode = 'same';
|
|
127
|
+
this.announceText(resources.GetString('son_same'));
|
|
128
|
+
} else if (constants.sonifMode == 'same') {
|
|
129
|
+
constants.sonifMode = 'off';
|
|
130
|
+
this.announceText(resources.GetString('son_off'));
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
if (constants.sonifMode == 'off') {
|
|
134
|
+
constants.sonifMode = 'on';
|
|
135
|
+
this.announceText(resources.GetString('son_on'));
|
|
136
|
+
} else {
|
|
137
|
+
constants.sonifMode = 'off';
|
|
138
|
+
this.announceText(resources.GetString('son_off'));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
changeChartLayer(updown = 'down') {
|
|
144
|
+
// get possible chart types, where we are, and move between them
|
|
145
|
+
let chartTypes = maidr.type;
|
|
146
|
+
if (Array.isArray(chartTypes)) {
|
|
147
|
+
let currentIndex = chartTypes.indexOf(constants.chartType);
|
|
148
|
+
if (updown == 'down') {
|
|
149
|
+
if (currentIndex == 0) {
|
|
150
|
+
//constants.chartType = chartTypes[chartTypes.length - 1];
|
|
151
|
+
} else {
|
|
152
|
+
constants.chartType = chartTypes[currentIndex - 1];
|
|
153
|
+
this.announceText('Switched to ' + constants.chartType); // todo: connect this to a resource file so it can be localized
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
if (currentIndex == chartTypes.length - 1) {
|
|
157
|
+
//constants.chartType = chartTypes[0];
|
|
158
|
+
} else {
|
|
159
|
+
constants.chartType = chartTypes[currentIndex + 1];
|
|
160
|
+
this.announceText('Switched to ' + constants.chartType); // todo: connect this to a resource file so it can be localized
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// update position relative to where we were on the previous layer
|
|
166
|
+
// newX = oldX * newLen / oldLen
|
|
167
|
+
if (constants.chartType == 'point') {
|
|
168
|
+
position.x = Math.round(
|
|
169
|
+
((plot.x.length - 1) * positionL1.x) / (plot.curvePoints.length - 1)
|
|
170
|
+
);
|
|
171
|
+
} else if (constants.chartType == 'smooth') {
|
|
172
|
+
// reverse math of the above
|
|
173
|
+
positionL1.x = Math.round(
|
|
174
|
+
((plot.curvePoints.length - 1) * position.x) / (plot.x.length - 1)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
announceText(txt) {
|
|
180
|
+
constants.announceContainer.innerHTML = txt;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
UpdateBraillePos() {
|
|
184
|
+
if (
|
|
185
|
+
constants.chartType == 'bar' ||
|
|
186
|
+
constants.chartType == 'hist' ||
|
|
187
|
+
constants.chartType == 'line'
|
|
188
|
+
) {
|
|
189
|
+
constants.brailleInput.setSelectionRange(position.x, position.x);
|
|
190
|
+
} else if (
|
|
191
|
+
constants.chartType == 'stacked_bar' ||
|
|
192
|
+
constants.chartType == 'stacked_normalized_bar' ||
|
|
193
|
+
constants.chartType == 'dodged_bar'
|
|
194
|
+
) {
|
|
195
|
+
// if we're not on the top y position
|
|
196
|
+
let pos = null;
|
|
197
|
+
if (position.y < plot.plotData[0].length - 1) {
|
|
198
|
+
pos = position.x;
|
|
199
|
+
} else {
|
|
200
|
+
pos = position.x * (plot.fill.length + 1) + position.y;
|
|
201
|
+
}
|
|
202
|
+
constants.brailleInput.setSelectionRange(pos, pos);
|
|
203
|
+
} else if (constants.chartType == 'heat') {
|
|
204
|
+
let pos = position.y * (plot.num_cols + 1) + position.x;
|
|
205
|
+
constants.brailleInput.setSelectionRange(pos, pos);
|
|
206
|
+
} else if (constants.chartType == 'box') {
|
|
207
|
+
// on box we extend characters a lot and have blanks, so we go to our type
|
|
208
|
+
let sectionPos =
|
|
209
|
+
constants.plotOrientation == 'vert' ? position.y : position.x;
|
|
210
|
+
let targetLabel = this.boxplotGridPlaceholders[sectionPos];
|
|
211
|
+
let haveTargetLabel = false;
|
|
212
|
+
let adjustedPos = 0;
|
|
213
|
+
if (constants.brailleData) {
|
|
214
|
+
for (let i = 0; i < constants.brailleData.length; i++) {
|
|
215
|
+
if (constants.brailleData[i].type != 'blank') {
|
|
216
|
+
if (
|
|
217
|
+
resources.GetString(constants.brailleData[i].type) == targetLabel
|
|
218
|
+
) {
|
|
219
|
+
haveTargetLabel = true;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
adjustedPos += constants.brailleData[i].numChars;
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
throw 'Braille data not set up, cannot move cursor in braille, sorry.';
|
|
227
|
+
}
|
|
228
|
+
// but sometimes we don't have our targetLabel, go to the start
|
|
229
|
+
// future todo: look for nearby label and go to the nearby side of that
|
|
230
|
+
if (!haveTargetLabel) {
|
|
231
|
+
adjustedPos = 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
constants.brailleInput.setSelectionRange(adjustedPos, adjustedPos);
|
|
235
|
+
} else if (
|
|
236
|
+
singleMaidr.type == 'point' ||
|
|
237
|
+
singleMaidr.type.includes('point')
|
|
238
|
+
) {
|
|
239
|
+
constants.brailleInput.setSelectionRange(positionL1.x, positionL1.x);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
displayValues() {
|
|
244
|
+
// we build an html text string to output to both visual users and aria live based on what chart we're on, our position, and the mode
|
|
245
|
+
// note: we do this all as one string rather than changing individual element IDs so that aria-live receives a single update
|
|
246
|
+
|
|
247
|
+
let output = '';
|
|
248
|
+
let verboseText = '';
|
|
249
|
+
let reviewText = '';
|
|
250
|
+
if (constants.chartType == 'bar') {
|
|
251
|
+
// {legend x} is {colname x}, {legend y} is {value y}
|
|
252
|
+
if (plot.plotLegend.x.length > 0 && plot.columnLabels[position.x]) {
|
|
253
|
+
verboseText =
|
|
254
|
+
plot.plotLegend.x + ' is ' + plot.columnLabels[position.x] + ', ';
|
|
255
|
+
}
|
|
256
|
+
if (plot.plotData[position.x]) {
|
|
257
|
+
verboseText += plot.plotLegend.y + ' is ' + plot.plotData[position.x];
|
|
258
|
+
}
|
|
259
|
+
if (constants.textMode == 'off') {
|
|
260
|
+
// do nothing :D
|
|
261
|
+
} else if (constants.textMode == 'terse') {
|
|
262
|
+
// {colname} {value}
|
|
263
|
+
output +=
|
|
264
|
+
'<p>' +
|
|
265
|
+
plot.columnLabels[position.x] +
|
|
266
|
+
' ' +
|
|
267
|
+
plot.plotData[position.x] +
|
|
268
|
+
'</p>\n';
|
|
269
|
+
} else if (constants.textMode == 'verbose') {
|
|
270
|
+
output += '<p>' + verboseText + '</p>\n';
|
|
271
|
+
}
|
|
272
|
+
} else if (constants.chartType == 'heat') {
|
|
273
|
+
// col name and value
|
|
274
|
+
if (constants.navigation == 1) {
|
|
275
|
+
verboseText +=
|
|
276
|
+
plot.x_group_label +
|
|
277
|
+
' ' +
|
|
278
|
+
plot.x_labels[position.x] +
|
|
279
|
+
', ' +
|
|
280
|
+
plot.y_group_label +
|
|
281
|
+
' ' +
|
|
282
|
+
plot.y_labels[position.y] +
|
|
283
|
+
', ' +
|
|
284
|
+
plot.fill +
|
|
285
|
+
' is ';
|
|
286
|
+
// if (constants.hasRect) {
|
|
287
|
+
verboseText += plot.plotData[2][position.y][position.x];
|
|
288
|
+
// }
|
|
289
|
+
} else {
|
|
290
|
+
verboseText +=
|
|
291
|
+
plot.y_group_label +
|
|
292
|
+
' ' +
|
|
293
|
+
plot.y_labels[position.y] +
|
|
294
|
+
', ' +
|
|
295
|
+
plot.x_group_label +
|
|
296
|
+
' ' +
|
|
297
|
+
plot.x_labels[position.x] +
|
|
298
|
+
', ' +
|
|
299
|
+
plot.fill +
|
|
300
|
+
' is ';
|
|
301
|
+
// if (constants.hasRect) {
|
|
302
|
+
verboseText += plot.plotData[2][position.y][position.x];
|
|
303
|
+
// }
|
|
304
|
+
}
|
|
305
|
+
// terse and verbose alternate between columns and rows
|
|
306
|
+
if (constants.textMode == 'off') {
|
|
307
|
+
// do nothing :D
|
|
308
|
+
} else if (constants.textMode == 'terse') {
|
|
309
|
+
// value only
|
|
310
|
+
if (constants.navigation == 1) {
|
|
311
|
+
// column navigation
|
|
312
|
+
output +=
|
|
313
|
+
'<p>' +
|
|
314
|
+
plot.x_labels[position.x] +
|
|
315
|
+
', ' +
|
|
316
|
+
plot.plotData[2][position.y][position.x] +
|
|
317
|
+
'</p>\n';
|
|
318
|
+
} else {
|
|
319
|
+
// row navigation
|
|
320
|
+
output +=
|
|
321
|
+
'<p>' +
|
|
322
|
+
plot.y_labels[position.y] +
|
|
323
|
+
', ' +
|
|
324
|
+
plot.plotData[2][position.y][position.x] +
|
|
325
|
+
'</p>\n';
|
|
326
|
+
}
|
|
327
|
+
} else if (constants.textMode == 'verbose') {
|
|
328
|
+
output += '<p>' + verboseText + '</p>\n';
|
|
329
|
+
}
|
|
330
|
+
} else if (constants.chartType == 'box') {
|
|
331
|
+
// setup
|
|
332
|
+
let val = 0;
|
|
333
|
+
let numPoints = 1;
|
|
334
|
+
let isOutlier = false;
|
|
335
|
+
let plotPos =
|
|
336
|
+
constants.plotOrientation == 'vert' ? position.x : position.y;
|
|
337
|
+
let sectionKey = plot.GetSectionKey(
|
|
338
|
+
constants.plotOrientation == 'vert' ? position.y : position.x
|
|
339
|
+
);
|
|
340
|
+
let textTerse = '';
|
|
341
|
+
let textVerbose = '';
|
|
342
|
+
|
|
343
|
+
if (sectionKey == 'lower_outlier' || sectionKey == 'upper_outlier') {
|
|
344
|
+
isOutlier = true;
|
|
345
|
+
}
|
|
346
|
+
if (plot.plotData[plotPos][sectionKey] == null) {
|
|
347
|
+
val = '';
|
|
348
|
+
if (isOutlier) numPoints = 0;
|
|
349
|
+
} else if (isOutlier) {
|
|
350
|
+
val = plot.plotData[plotPos][sectionKey].join(', ');
|
|
351
|
+
numPoints = plot.plotData[plotPos][sectionKey].length;
|
|
352
|
+
} else {
|
|
353
|
+
val = plot.plotData[plotPos][sectionKey];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// set output
|
|
357
|
+
|
|
358
|
+
// group label for verbose
|
|
359
|
+
if (constants.navigation) {
|
|
360
|
+
if (plot.x_group_label) textVerbose += plot.x_group_label;
|
|
361
|
+
} else if (!constants.navigation) {
|
|
362
|
+
if (plot.y_group_label) textVerbose += plot.y_group_label;
|
|
363
|
+
}
|
|
364
|
+
// and axes label
|
|
365
|
+
if (constants.navigation) {
|
|
366
|
+
if (plot.x_labels[plotPos]) {
|
|
367
|
+
textVerbose += ' is ';
|
|
368
|
+
textTerse += plot.x_labels[plotPos] + ', ';
|
|
369
|
+
textVerbose += plot.x_labels[plotPos] + ', ';
|
|
370
|
+
} else {
|
|
371
|
+
textVerbose += ', ';
|
|
372
|
+
}
|
|
373
|
+
} else if (!constants.navigation) {
|
|
374
|
+
if (plot.y_labels[plotPos]) {
|
|
375
|
+
textVerbose += ' is ';
|
|
376
|
+
textTerse += plot.y_labels[plotPos] + ', ';
|
|
377
|
+
textVerbose += plot.y_labels[plotPos] + ', ';
|
|
378
|
+
} else {
|
|
379
|
+
textVerbose += ', ';
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// outliers
|
|
383
|
+
if (isOutlier) {
|
|
384
|
+
textTerse += numPoints + ' ';
|
|
385
|
+
textVerbose += numPoints + ' ';
|
|
386
|
+
}
|
|
387
|
+
// label
|
|
388
|
+
textVerbose += resources.GetString(sectionKey);
|
|
389
|
+
if (numPoints == 1) textVerbose += ' is ';
|
|
390
|
+
else {
|
|
391
|
+
textVerbose += 's ';
|
|
392
|
+
if (numPoints > 1) textVerbose += ' are ';
|
|
393
|
+
}
|
|
394
|
+
if (
|
|
395
|
+
isOutlier ||
|
|
396
|
+
(constants.navigation && constants.plotOrientation == 'horz') ||
|
|
397
|
+
(!constants.navigation && constants.plotOrientation == 'vert')
|
|
398
|
+
) {
|
|
399
|
+
textTerse += resources.GetString(sectionKey);
|
|
400
|
+
|
|
401
|
+
// grammar
|
|
402
|
+
if (numPoints != 1) {
|
|
403
|
+
textTerse += 's';
|
|
404
|
+
}
|
|
405
|
+
textTerse += ' ';
|
|
406
|
+
}
|
|
407
|
+
// val
|
|
408
|
+
if (plot.plotData[plotPos][sectionKey] != null && !isOutlier) {
|
|
409
|
+
textTerse += 'empty';
|
|
410
|
+
textVerbose += 'empty';
|
|
411
|
+
} else {
|
|
412
|
+
textTerse += val;
|
|
413
|
+
textVerbose += val;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
verboseText = textVerbose; // yeah it's an extra var, who cares
|
|
417
|
+
if (constants.textMode == 'verbose')
|
|
418
|
+
output = '<p>' + textVerbose + '</p>\n';
|
|
419
|
+
else if (constants.textMode == 'terse')
|
|
420
|
+
output = '<p>' + textTerse + '</p>\n';
|
|
421
|
+
} else if (
|
|
422
|
+
singleMaidr.type == 'point' ||
|
|
423
|
+
singleMaidr.type.includes('point')
|
|
424
|
+
) {
|
|
425
|
+
if (constants.chartType == 'point') {
|
|
426
|
+
// point layer
|
|
427
|
+
verboseText +=
|
|
428
|
+
plot.x_group_label +
|
|
429
|
+
' ' +
|
|
430
|
+
plot.x[position.x] +
|
|
431
|
+
', ' +
|
|
432
|
+
plot.y_group_label +
|
|
433
|
+
' [' +
|
|
434
|
+
plot.y[position.x].join(', ') +
|
|
435
|
+
']';
|
|
436
|
+
|
|
437
|
+
if (constants.textMode == 'off') {
|
|
438
|
+
// do nothing
|
|
439
|
+
} else if (constants.textMode == 'terse') {
|
|
440
|
+
output +=
|
|
441
|
+
'<p>' +
|
|
442
|
+
plot.x[position.x] +
|
|
443
|
+
', ' +
|
|
444
|
+
'[' +
|
|
445
|
+
plot.y[position.x].join(', ') +
|
|
446
|
+
']' +
|
|
447
|
+
'</p>\n';
|
|
448
|
+
} else if (constants.textMode == 'verbose') {
|
|
449
|
+
// set from verboseText
|
|
450
|
+
}
|
|
451
|
+
} else if (constants.chartType == 'smooth') {
|
|
452
|
+
// best fit smooth layer
|
|
453
|
+
verboseText +=
|
|
454
|
+
plot.x_group_label +
|
|
455
|
+
' ' +
|
|
456
|
+
plot.curveX[positionL1.x] +
|
|
457
|
+
', ' +
|
|
458
|
+
plot.y_group_label +
|
|
459
|
+
' ' +
|
|
460
|
+
plot.curvePoints[positionL1.x]; // verbose mode: x and y values
|
|
461
|
+
|
|
462
|
+
if (constants.textMode == 'off') {
|
|
463
|
+
// do nothing
|
|
464
|
+
} else if (constants.textMode == 'terse') {
|
|
465
|
+
// terse mode: gradient trend
|
|
466
|
+
// output += '<p>' + plot.gradient[positionL1.x] + '<p>\n';
|
|
467
|
+
|
|
468
|
+
// display absolute gradient of the graph
|
|
469
|
+
output += '<p>' + plot.curvePoints[positionL1.x] + '<p>\n';
|
|
470
|
+
} else if (constants.textMode == 'verbose') {
|
|
471
|
+
// set from verboseText
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (constants.textMode == 'verbose')
|
|
475
|
+
output = '<p>' + verboseText + '</p>\n';
|
|
476
|
+
} else if (constants.chartType == 'hist') {
|
|
477
|
+
if (constants.textMode == 'terse') {
|
|
478
|
+
// terse: {x}, {y}
|
|
479
|
+
output =
|
|
480
|
+
'<p>' +
|
|
481
|
+
plot.plotData[position.x].x +
|
|
482
|
+
', ' +
|
|
483
|
+
plot.plotData[position.x].y +
|
|
484
|
+
'</p>\n';
|
|
485
|
+
} else if (constants.textMode == 'verbose') {
|
|
486
|
+
// verbose: {xlabel} is xmin through xmax, {ylabel} is y
|
|
487
|
+
output = '<p>';
|
|
488
|
+
if (plot.legendX) {
|
|
489
|
+
output = plot.legendX + ' is ';
|
|
490
|
+
}
|
|
491
|
+
output += plot.plotData[position.x].xmin;
|
|
492
|
+
output += ' through ' + plot.plotData[position.x].xmax + ', ';
|
|
493
|
+
if (plot.legendY) {
|
|
494
|
+
output += plot.legendY + ' is ';
|
|
495
|
+
}
|
|
496
|
+
output += plot.plotData[position.x].y;
|
|
497
|
+
}
|
|
498
|
+
} else if (constants.chartType == 'line') {
|
|
499
|
+
// line layer
|
|
500
|
+
verboseText +=
|
|
501
|
+
plot.x_group_label +
|
|
502
|
+
' is ' +
|
|
503
|
+
plot.pointValuesX[position.x] +
|
|
504
|
+
', ' +
|
|
505
|
+
plot.y_group_label +
|
|
506
|
+
' is ' +
|
|
507
|
+
plot.pointValuesY[position.x];
|
|
508
|
+
|
|
509
|
+
if (constants.textMode == 'off') {
|
|
510
|
+
// do nothing
|
|
511
|
+
} else if (constants.textMode == 'terse') {
|
|
512
|
+
output +=
|
|
513
|
+
'<p>' +
|
|
514
|
+
plot.pointValuesX[position.x] +
|
|
515
|
+
', ' +
|
|
516
|
+
plot.pointValuesY[position.x] +
|
|
517
|
+
'</p>\n';
|
|
518
|
+
} else if (constants.textMode == 'verbose') {
|
|
519
|
+
// set from verboseText
|
|
520
|
+
output += '<p>' + verboseText + '</p>\n';
|
|
521
|
+
}
|
|
522
|
+
} else if (
|
|
523
|
+
constants.chartType == 'stacked_bar' ||
|
|
524
|
+
constants.chartType == 'stacked_normalized_bar' ||
|
|
525
|
+
constants.chartType == 'dodged_bar'
|
|
526
|
+
) {
|
|
527
|
+
// {legend x} is {colname x}, {legend y} is {colname y}, value is {plotData[x][y]}
|
|
528
|
+
verboseText += plot.plotLegend.x + ' is ' + plot.level[position.x] + ', ';
|
|
529
|
+
verboseText += plot.plotLegend.y + ' is ' + plot.fill[position.y] + ', ';
|
|
530
|
+
verboseText += 'value is ' + plot.plotData[position.x][position.y];
|
|
531
|
+
|
|
532
|
+
if (constants.textMode == 'off') {
|
|
533
|
+
// do nothing
|
|
534
|
+
} else if (constants.textMode == 'terse') {
|
|
535
|
+
// navigation == 1 ? {colname x} : {colname y} is {plotData[x][y]}
|
|
536
|
+
if (constants.navigation == 1) {
|
|
537
|
+
output +=
|
|
538
|
+
'<p>' +
|
|
539
|
+
plot.level[position.x] +
|
|
540
|
+
' is ' +
|
|
541
|
+
plot.plotData[position.x][position.y] +
|
|
542
|
+
'</p>\n';
|
|
543
|
+
} else {
|
|
544
|
+
output +=
|
|
545
|
+
'<p>' +
|
|
546
|
+
plot.fill[position.y] +
|
|
547
|
+
' is ' +
|
|
548
|
+
plot.plotData[position.x][position.y] +
|
|
549
|
+
'</p>\n';
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
output += '<p>' + verboseText + '</p>\n';
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (constants.infoDiv) constants.infoDiv.innerHTML = output;
|
|
557
|
+
if (constants.review) {
|
|
558
|
+
if (output.length > 0) {
|
|
559
|
+
constants.review.value = output.replace(/<[^>]*>?/gm, '');
|
|
560
|
+
} else {
|
|
561
|
+
constants.review.value = verboseText;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
displayInfo(textType, textValue) {
|
|
567
|
+
if (textType) {
|
|
568
|
+
if (textValue) {
|
|
569
|
+
if (constants.textMode == 'terse') {
|
|
570
|
+
constants.infoDiv.innerHTML = '<p>' + textValue + '<p>';
|
|
571
|
+
} else if (constants.textMode == 'verbose') {
|
|
572
|
+
let capsTextType =
|
|
573
|
+
textType.charAt(0).toUpperCase() + textType.slice(1);
|
|
574
|
+
constants.infoDiv.innerHTML =
|
|
575
|
+
'<p>' + capsTextType + ' is ' + textValue + '<p>';
|
|
576
|
+
}
|
|
577
|
+
} else {
|
|
578
|
+
let aOrAn = ['a', 'e', 'i', 'o', 'u'].includes(textType.charAt(0))
|
|
579
|
+
? 'an'
|
|
580
|
+
: 'a';
|
|
581
|
+
|
|
582
|
+
constants.infoDiv.innerHTML =
|
|
583
|
+
'<p>Plot does not have ' + aOrAn + ' ' + textType + '<p>';
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
SetBraille() {
|
|
589
|
+
let brailleArray = [];
|
|
590
|
+
|
|
591
|
+
if (constants.chartType == 'heat') {
|
|
592
|
+
let range = (constants.maxY - constants.minY) / 3;
|
|
593
|
+
let low = constants.minY + range;
|
|
594
|
+
let medium = low + range;
|
|
595
|
+
let high = medium + range;
|
|
596
|
+
for (let i = 0; i < plot.y_coord.length; i++) {
|
|
597
|
+
for (let j = 0; j < plot.x_coord.length; j++) {
|
|
598
|
+
if (plot.values[i][j] == 0) {
|
|
599
|
+
brailleArray.push('⠀');
|
|
600
|
+
} else if (plot.values[i][j] <= low) {
|
|
601
|
+
brailleArray.push('⠤');
|
|
602
|
+
} else if (plot.values[i][j] <= medium) {
|
|
603
|
+
brailleArray.push('⠒');
|
|
604
|
+
} else {
|
|
605
|
+
brailleArray.push('⠉');
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
brailleArray.push('⠳');
|
|
609
|
+
}
|
|
610
|
+
} else if (
|
|
611
|
+
constants.chartType == 'stacked_bar' ||
|
|
612
|
+
constants.chartType == 'stacked_normalized_bar' ||
|
|
613
|
+
constants.chartType == 'dodged_bar'
|
|
614
|
+
) {
|
|
615
|
+
// if we're not on the top y position, display just this level, using local min max
|
|
616
|
+
if (position.y < plot.plotData[0].length - 1) {
|
|
617
|
+
let localMin = null;
|
|
618
|
+
let localMax = null;
|
|
619
|
+
for (let i = 0; i < plot.plotData.length; i++) {
|
|
620
|
+
if (i == 0) {
|
|
621
|
+
localMin = plot.plotData[i][position.y];
|
|
622
|
+
localMax = plot.plotData[i][position.y];
|
|
623
|
+
} else {
|
|
624
|
+
if (plot.plotData[i][position.y] < localMin) {
|
|
625
|
+
localMin = plot.plotData[i][position.y];
|
|
626
|
+
}
|
|
627
|
+
if (plot.plotData[i][position.y] > localMax) {
|
|
628
|
+
localMax = plot.plotData[i][position.y];
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
let range = (localMax - localMin) / 4;
|
|
633
|
+
let low = localMin + range;
|
|
634
|
+
let medium = low + range;
|
|
635
|
+
let medium_high = medium + range;
|
|
636
|
+
for (let i = 0; i < plot.plotData.length; i++) {
|
|
637
|
+
if (plot.plotData[i][position.y] == 0) {
|
|
638
|
+
brailleArray.push('⠀');
|
|
639
|
+
} else if (plot.plotData[i][position.y] <= low) {
|
|
640
|
+
brailleArray.push('⣀');
|
|
641
|
+
} else if (plot.plotData[i][position.y] <= medium) {
|
|
642
|
+
brailleArray.push('⠤');
|
|
643
|
+
} else if (plot.plotData[i][position.y] <= medium_high) {
|
|
644
|
+
brailleArray.push('⠒');
|
|
645
|
+
} else {
|
|
646
|
+
brailleArray.push('⠉');
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
// all mode, do braille similar to heatmap, with all data and seperator
|
|
651
|
+
for (let i = 0; i < plot.plotData.length; i++) {
|
|
652
|
+
let range = (constants.maxY - constants.minY) / 4;
|
|
653
|
+
let low = constants.minY + range;
|
|
654
|
+
let medium = low + range;
|
|
655
|
+
let medium_high = medium + range;
|
|
656
|
+
for (let j = 0; j < plot.plotData[i].length; j++) {
|
|
657
|
+
if (plot.plotData[i][j] == 0) {
|
|
658
|
+
brailleArray.push('⠀');
|
|
659
|
+
} else if (plot.plotData[i][j] <= low) {
|
|
660
|
+
brailleArray.push('⣀');
|
|
661
|
+
} else if (plot.plotData[i][j] <= medium) {
|
|
662
|
+
brailleArray.push('⠤');
|
|
663
|
+
} else if (plot.plotData[i][j] <= medium_high) {
|
|
664
|
+
brailleArray.push('⠒');
|
|
665
|
+
} else {
|
|
666
|
+
brailleArray.push('⠉');
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
brailleArray.push('⠳');
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
} else if (constants.chartType == 'bar') {
|
|
673
|
+
let range = (constants.maxY - constants.minY) / 4;
|
|
674
|
+
let low = constants.minY + range;
|
|
675
|
+
let medium = low + range;
|
|
676
|
+
let medium_high = medium + range;
|
|
677
|
+
for (let i = 0; i < plot.plotData.length; i++) {
|
|
678
|
+
if (plot.plotData[i] <= low) {
|
|
679
|
+
brailleArray.push('⣀');
|
|
680
|
+
} else if (plot.plotData[i] <= medium) {
|
|
681
|
+
brailleArray.push('⠤');
|
|
682
|
+
} else if (plot.plotData[i] <= medium_high) {
|
|
683
|
+
brailleArray.push('⠒');
|
|
684
|
+
} else {
|
|
685
|
+
brailleArray.push('⠉');
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
} else if (constants.chartType == 'smooth') {
|
|
689
|
+
let range = (plot.curveMaxY - plot.curveMinY) / 4;
|
|
690
|
+
let low = plot.curveMinY + range;
|
|
691
|
+
let medium = low + range;
|
|
692
|
+
let medium_high = medium + range;
|
|
693
|
+
let high = medium_high + range;
|
|
694
|
+
for (let i = 0; i < plot.curvePoints.length; i++) {
|
|
695
|
+
if (plot.curvePoints[i] <= low) {
|
|
696
|
+
brailleArray.push('⣀');
|
|
697
|
+
} else if (plot.curvePoints[i] <= medium) {
|
|
698
|
+
brailleArray.push('⠤');
|
|
699
|
+
} else if (plot.curvePoints[i] <= medium_high) {
|
|
700
|
+
brailleArray.push('⠒');
|
|
701
|
+
} else if (plot.curvePoints[i] <= high) {
|
|
702
|
+
brailleArray.push('⠉');
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
} else if (constants.chartType == 'hist') {
|
|
706
|
+
let range = (constants.maxY - constants.minY) / 4;
|
|
707
|
+
let low = constants.minY + range;
|
|
708
|
+
let medium = low + range;
|
|
709
|
+
let medium_high = medium + range;
|
|
710
|
+
for (let i = 0; i < plot.plotData.length; i++) {
|
|
711
|
+
if (plot.plotData[i].y <= low) {
|
|
712
|
+
brailleArray.push('⣀');
|
|
713
|
+
} else if (plot.plotData[i].y <= medium) {
|
|
714
|
+
brailleArray.push('⠤');
|
|
715
|
+
} else if (plot.plotData[i].y <= medium_high) {
|
|
716
|
+
brailleArray.push('⠒');
|
|
717
|
+
} else {
|
|
718
|
+
brailleArray.push('⠉');
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} else if (constants.chartType == 'box' && position.y > -1) {
|
|
722
|
+
// Idea here is to use different braille characters to physically represent the box
|
|
723
|
+
// if sections are longer or shorter we'll add more characters
|
|
724
|
+
// example: outlier, small space, long min, med 25/50/75, short max: ⠂ ⠒⠒⠒⠒⠒⠒⠿⠸⠿⠒
|
|
725
|
+
//
|
|
726
|
+
// So, we get weighted lengths of each section (or gaps between outliers, etc),
|
|
727
|
+
// and then create the appropriate number of characters
|
|
728
|
+
// Full explanation on readme
|
|
729
|
+
//
|
|
730
|
+
// This is messy and long (250 lines). If anyone wants to improve, be my guest
|
|
731
|
+
|
|
732
|
+
// Some init stuff
|
|
733
|
+
let plotPos;
|
|
734
|
+
let globalMin;
|
|
735
|
+
let globalMax;
|
|
736
|
+
let numSections = plot.sections.length;
|
|
737
|
+
if (constants.plotOrientation == 'vert') {
|
|
738
|
+
plotPos = position.x;
|
|
739
|
+
globalMin = constants.minY;
|
|
740
|
+
globalMax = constants.maxY;
|
|
741
|
+
} else {
|
|
742
|
+
plotPos = position.y;
|
|
743
|
+
globalMin = constants.minX;
|
|
744
|
+
globalMax = constants.maxX;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// We convert main plot data to array of values and types, including min and max, and seperating outliers and removing nulls
|
|
748
|
+
let valData = [];
|
|
749
|
+
valData.push({ type: 'global_min', value: globalMin });
|
|
750
|
+
for (let i = 0; i < numSections; i++) {
|
|
751
|
+
let sectionKey = plot.sections[i];
|
|
752
|
+
let point = plot.plotData[plotPos][sectionKey];
|
|
753
|
+
let charData = {};
|
|
754
|
+
|
|
755
|
+
if (point != null) {
|
|
756
|
+
if (sectionKey == 'lower_outlier' || sectionKey == 'upper_outlier') {
|
|
757
|
+
for (let j = 0; j < point.length; j++) {
|
|
758
|
+
charData = {
|
|
759
|
+
type: sectionKey,
|
|
760
|
+
value: point[j],
|
|
761
|
+
};
|
|
762
|
+
valData.push(charData);
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
charData = {
|
|
766
|
+
type: sectionKey,
|
|
767
|
+
value: point,
|
|
768
|
+
};
|
|
769
|
+
valData.push(charData);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
valData.push({ type: 'global_max', value: globalMax });
|
|
774
|
+
|
|
775
|
+
// Then we convert to lengths and types
|
|
776
|
+
// We assign lengths based on the difference between each point, and assign blanks if this comes before or after an outlier
|
|
777
|
+
let lenData = [];
|
|
778
|
+
let isBeforeMid = true;
|
|
779
|
+
for (let i = 0; i < valData.length; i++) {
|
|
780
|
+
let diff;
|
|
781
|
+
// we compare inwardly, and midpoint is len 0
|
|
782
|
+
if (isBeforeMid) {
|
|
783
|
+
diff = Math.abs(valData[i + 1].value - valData[i].value);
|
|
784
|
+
} else {
|
|
785
|
+
diff = Math.abs(valData[i].value - valData[i - 1].value);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (
|
|
789
|
+
valData[i].type == 'global_min' ||
|
|
790
|
+
valData[i].type == 'global_max'
|
|
791
|
+
) {
|
|
792
|
+
lenData.push({ type: 'blank', length: diff });
|
|
793
|
+
} else if (valData[i].type == 'lower_outlier') {
|
|
794
|
+
// add diff as space, as well as a 0 len outlier point
|
|
795
|
+
// add blank last, as the earlier point is covered by global_min
|
|
796
|
+
lenData.push({ type: valData[i].type, length: 0 });
|
|
797
|
+
lenData.push({ type: 'blank', length: diff });
|
|
798
|
+
} else if (valData[i].type == 'upper_outlier') {
|
|
799
|
+
// add diff as space, as well as a 0 len outlier point, but reverse order from lower_outlier obvs
|
|
800
|
+
lenData.push({ type: 'blank', length: diff });
|
|
801
|
+
lenData.push({ type: valData[i].type, length: 0 });
|
|
802
|
+
} else if (valData[i].type == 'q2') {
|
|
803
|
+
// change calc method after midpoint, as we want spacing to go outward from center (and so center has no length)
|
|
804
|
+
isBeforeMid = false;
|
|
805
|
+
lenData.push({ type: valData[i].type, length: 0 });
|
|
806
|
+
} else {
|
|
807
|
+
// normal points
|
|
808
|
+
lenData.push({ type: valData[i].type, length: diff });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// We create a set of braille characters based on the lengths
|
|
813
|
+
|
|
814
|
+
// Method:
|
|
815
|
+
// We normalize the lengths of each characters needed length
|
|
816
|
+
// by the total number of characters we have availble
|
|
817
|
+
// (including offset from characters requiring 1 character).
|
|
818
|
+
// Then apply the appropriate number of characters to each
|
|
819
|
+
|
|
820
|
+
// A few exceptions:
|
|
821
|
+
// exception: each must have min 1 character (not blanks or length 0)
|
|
822
|
+
// exception: for 25/75 and min/max, if they aren't exactly equal, assign different num characters
|
|
823
|
+
// exception: center is always 456 123
|
|
824
|
+
|
|
825
|
+
// Step 1, sorta init.
|
|
826
|
+
// We prepopulate each non null section with a single character, and log for character offset
|
|
827
|
+
let locMin = -1;
|
|
828
|
+
let locQ1 = -1;
|
|
829
|
+
let locQ3 = -1;
|
|
830
|
+
let locMax = -1;
|
|
831
|
+
let numAllocatedChars = 0; // counter for number of characters we've already assigned
|
|
832
|
+
for (let i = 0; i < lenData.length; i++) {
|
|
833
|
+
if (
|
|
834
|
+
lenData[i].type != 'blank' &&
|
|
835
|
+
(lenData[i].length > 0 ||
|
|
836
|
+
lenData[i].type == 'lower_outlier' ||
|
|
837
|
+
lenData[i].type == 'upper_outlier')
|
|
838
|
+
) {
|
|
839
|
+
lenData[i].numChars = 1;
|
|
840
|
+
numAllocatedChars++;
|
|
841
|
+
} else {
|
|
842
|
+
lenData[i].numChars = 0;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// store 25/75 min/max locations so we can check them later more easily
|
|
846
|
+
if (lenData[i].type == 'min' && lenData[i].length > 0) locMin = i;
|
|
847
|
+
if (lenData[i].type == 'max' && lenData[i].length > 0) locMax = i;
|
|
848
|
+
if (lenData[i].type == 'q1') locQ1 = i;
|
|
849
|
+
if (lenData[i].type == 'q3') locQ3 = i;
|
|
850
|
+
|
|
851
|
+
// 50 gets 2 characters by default
|
|
852
|
+
if (lenData[i].type == 'q2') {
|
|
853
|
+
lenData[i].numChars = 2;
|
|
854
|
+
numAllocatedChars++; // we just ++ here as we already ++'d above
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// make sure rules are set for pairs (q1 / q3, min / max)
|
|
859
|
+
// if they're equal length, we don't need to do anything as they already each have 1 character
|
|
860
|
+
// if they're not equal length, we need to add 1 character to the longer one
|
|
861
|
+
if (locMin > -1 && locMax > -1) {
|
|
862
|
+
// we do it this way as we don't always have both min and max
|
|
863
|
+
|
|
864
|
+
if (lenData[locMin].length != lenData[locMax].length) {
|
|
865
|
+
if (lenData[locMin].length > lenData[locMax].length) {
|
|
866
|
+
lenData[locMin].numChars++;
|
|
867
|
+
numAllocatedChars++;
|
|
868
|
+
} else {
|
|
869
|
+
lenData[locMax].numChars++;
|
|
870
|
+
numAllocatedChars++;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// same for q1/q3
|
|
875
|
+
if (lenData[locQ1].length != lenData[locQ3].length) {
|
|
876
|
+
if (lenData[locQ1].length > lenData[locQ3].length) {
|
|
877
|
+
lenData[locQ1].numChars++;
|
|
878
|
+
numAllocatedChars++;
|
|
879
|
+
} else {
|
|
880
|
+
lenData[locQ3].numChars++;
|
|
881
|
+
numAllocatedChars++;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Step 2: normalize and allocate remaining characters and add to our main braille array
|
|
886
|
+
let charsAvailable = constants.brailleDisplayLength - numAllocatedChars;
|
|
887
|
+
let allocateCharacters = this.AllocateCharacters(lenData, charsAvailable);
|
|
888
|
+
// apply allocation
|
|
889
|
+
let brailleData = lenData;
|
|
890
|
+
for (let i = 0; i < allocateCharacters.length; i++) {
|
|
891
|
+
if (allocateCharacters[i]) {
|
|
892
|
+
brailleData[i].numChars += allocateCharacters[i];
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
constants.brailleData = brailleData;
|
|
897
|
+
if (constants.debugLevel > 5) {
|
|
898
|
+
console.log('plotData[i]', plot.plotData[plotPos]);
|
|
899
|
+
console.log('valData', valData);
|
|
900
|
+
console.log('lenData', lenData);
|
|
901
|
+
console.log('brailleData', brailleData);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// convert to braille characters
|
|
905
|
+
for (let i = 0; i < brailleData.length; i++) {
|
|
906
|
+
for (let j = 0; j < brailleData[i].numChars; j++) {
|
|
907
|
+
let brailleChar = '⠀'; // blank
|
|
908
|
+
if (brailleData[i].type == 'min' || brailleData[i].type == 'max') {
|
|
909
|
+
brailleChar = '⠒';
|
|
910
|
+
} else if (
|
|
911
|
+
brailleData[i].type == 'q1' ||
|
|
912
|
+
brailleData[i].type == 'q3'
|
|
913
|
+
) {
|
|
914
|
+
brailleChar = '⠿';
|
|
915
|
+
} else if (brailleData[i].type == 'q2') {
|
|
916
|
+
if (j == 0) {
|
|
917
|
+
brailleChar = '⠸';
|
|
918
|
+
} else {
|
|
919
|
+
brailleChar = '⠇';
|
|
920
|
+
}
|
|
921
|
+
} else if (
|
|
922
|
+
brailleData[i].type == 'lower_outlier' ||
|
|
923
|
+
brailleData[i].type == 'upper_outlier'
|
|
924
|
+
) {
|
|
925
|
+
brailleChar = '⠂';
|
|
926
|
+
}
|
|
927
|
+
brailleArray.push(brailleChar);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
} else if (constants.chartType == 'line') {
|
|
931
|
+
// TODO
|
|
932
|
+
// ⠑
|
|
933
|
+
let range = (constants.maxY - constants.minY) / 4;
|
|
934
|
+
let low = constants.minY + range;
|
|
935
|
+
let medium = low + range;
|
|
936
|
+
let medium_high = medium + range;
|
|
937
|
+
let high = medium_high + range;
|
|
938
|
+
|
|
939
|
+
for (let i = 0; i < plot.pointValuesY.length; i++) {
|
|
940
|
+
if (
|
|
941
|
+
plot.pointValuesY[i] <= low &&
|
|
942
|
+
i - 1 >= 0 &&
|
|
943
|
+
plot.pointValuesY[i - 1] > low
|
|
944
|
+
) {
|
|
945
|
+
// move from higher ranges to low
|
|
946
|
+
if (plot.pointValuesY[i - 1] <= medium) {
|
|
947
|
+
// move away from medium range
|
|
948
|
+
brailleArray.push('⢄');
|
|
949
|
+
} else if (plot.pointValuesY[i - 1] <= medium_high) {
|
|
950
|
+
// move away from medium high range
|
|
951
|
+
brailleArray.push('⢆');
|
|
952
|
+
} else if (plot.pointValuesY[i - 1] > medium_high) {
|
|
953
|
+
// move away from high range
|
|
954
|
+
brailleArray.push('⢇');
|
|
955
|
+
}
|
|
956
|
+
} else if (plot.pointValuesY[i] <= low) {
|
|
957
|
+
// in the low range
|
|
958
|
+
brailleArray.push('⣀');
|
|
959
|
+
} else if (i - 1 >= 0 && plot.pointValuesY[i - 1] <= low) {
|
|
960
|
+
// move from low to higher ranges
|
|
961
|
+
if (plot.pointValuesY[i] <= medium) {
|
|
962
|
+
// move to medium range
|
|
963
|
+
brailleArray.push('⡠');
|
|
964
|
+
} else if (plot.pointValuesY[i] <= medium_high) {
|
|
965
|
+
// move to medium high range
|
|
966
|
+
brailleArray.push('⡰');
|
|
967
|
+
} else if (plot.pointValuesY[i] > medium_high) {
|
|
968
|
+
// move to high range
|
|
969
|
+
brailleArray.push('⡸');
|
|
970
|
+
}
|
|
971
|
+
} else if (
|
|
972
|
+
plot.pointValuesY[i] <= medium &&
|
|
973
|
+
i - 1 >= 0 &&
|
|
974
|
+
plot.pointValuesY[i - 1] > medium
|
|
975
|
+
) {
|
|
976
|
+
if (plot.pointValuesY[i - 1] <= medium_high) {
|
|
977
|
+
// move away from medium high range to medium
|
|
978
|
+
brailleArray.push('⠢');
|
|
979
|
+
} else if (plot.pointValuesY[i - 1] > medium_high) {
|
|
980
|
+
// move away from high range
|
|
981
|
+
brailleArray.push('⠣');
|
|
982
|
+
}
|
|
983
|
+
} else if (plot.pointValuesY[i] <= medium) {
|
|
984
|
+
brailleArray.push('⠤');
|
|
985
|
+
} else if (i - 1 >= 0 && plot.pointValuesY[i - 1] <= medium) {
|
|
986
|
+
// move from medium to higher ranges
|
|
987
|
+
if (plot.pointValuesY[i] <= medium_high) {
|
|
988
|
+
// move to medium high range
|
|
989
|
+
brailleArray.push('⠔');
|
|
990
|
+
} else if (plot.pointValuesY[i] > medium_high) {
|
|
991
|
+
// move to high range
|
|
992
|
+
brailleArray.push('⠜');
|
|
993
|
+
}
|
|
994
|
+
} else if (
|
|
995
|
+
plot.pointValuesY[i] <= medium_high &&
|
|
996
|
+
i - 1 >= 0 &&
|
|
997
|
+
plot.pointValuesY[i - 1] > medium_high
|
|
998
|
+
) {
|
|
999
|
+
// move away from high range to medium high
|
|
1000
|
+
brailleArray.push('⠑');
|
|
1001
|
+
} else if (plot.pointValuesY[i] <= medium_high) {
|
|
1002
|
+
brailleArray.push('⠒');
|
|
1003
|
+
} else if (i - 1 >= 0 && plot.pointValuesY[i - 1] <= medium_high) {
|
|
1004
|
+
// move from medium high to high range
|
|
1005
|
+
brailleArray.push('⠊');
|
|
1006
|
+
} else if (plot.pointValuesY[i] <= high) {
|
|
1007
|
+
brailleArray.push('⠉');
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
constants.brailleInput.value = brailleArray.join('');
|
|
1013
|
+
|
|
1014
|
+
constants.brailleInput.value = brailleArray.join('');
|
|
1015
|
+
if (constants.debugLevel > 5) {
|
|
1016
|
+
console.log('braille:', constants.brailleInput.value);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
this.UpdateBraillePos();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
CharLenImpact(charData) {
|
|
1023
|
+
return charData.length / charData.numChars;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* This function allocates a total number of characters among an array of lengths,
|
|
1028
|
+
* proportionally to each length.
|
|
1029
|
+
*
|
|
1030
|
+
* @param {Array} arr - The array of objects containing lengths, type, and current numChars. Each length should be a positive number.
|
|
1031
|
+
* @param {number} charsToAllocate - The total number of characters to be allocated.
|
|
1032
|
+
*
|
|
1033
|
+
* The function first calculates the sum of all lengths in the array. Then, it
|
|
1034
|
+
* iterates over the array and calculates an initial allocation for each length,
|
|
1035
|
+
* rounded to the nearest integer, based on its proportion of the total length.
|
|
1036
|
+
*
|
|
1037
|
+
* If the sum of these initial allocations is not equal to the total number of
|
|
1038
|
+
* characters due to rounding errors, the function makes adjustments to the allocations.
|
|
1039
|
+
*
|
|
1040
|
+
* The adjustments are made in a loop that continues until the difference between
|
|
1041
|
+
* the total number of characters and the sum of the allocations is zero, or until
|
|
1042
|
+
* the loop has run a maximum number of iterations equal to the length of the array.
|
|
1043
|
+
*
|
|
1044
|
+
* In each iteration of the loop, the function calculates a rounding adjustment for
|
|
1045
|
+
* each length, again based on its proportion of the total length, and adds this
|
|
1046
|
+
* adjustment to the length's allocation.
|
|
1047
|
+
*
|
|
1048
|
+
* If there's still a difference after the maximum number of iterations, the function
|
|
1049
|
+
* falls back to a simpler method of distributing the difference: it sorts the lengths
|
|
1050
|
+
* by their allocations and adds or subtracts 1 from each length in this order until
|
|
1051
|
+
* the difference is zero.
|
|
1052
|
+
*
|
|
1053
|
+
* The function returns an array of the final allocations.
|
|
1054
|
+
*
|
|
1055
|
+
* @returns {Array} The array of allocations.
|
|
1056
|
+
*/
|
|
1057
|
+
AllocateCharacters(arr, charsToAllocate) {
|
|
1058
|
+
// init
|
|
1059
|
+
let allocation = [];
|
|
1060
|
+
let sumLen = 0;
|
|
1061
|
+
for (let i = 0; i < arr.length; i++) {
|
|
1062
|
+
sumLen += arr[i].length;
|
|
1063
|
+
}
|
|
1064
|
+
let notAllowed = ['lower_outlier', 'upper_outlier', '50']; // these types only have the 1 char they were assigned above
|
|
1065
|
+
|
|
1066
|
+
// main allocation
|
|
1067
|
+
for (let i = 0; i < arr.length; i++) {
|
|
1068
|
+
if (!notAllowed.includes(arr[i].type)) {
|
|
1069
|
+
allocation[i] = Math.round((arr[i].length / sumLen) * charsToAllocate);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// main allocation is not perfect, so we need to adjust
|
|
1074
|
+
let allocatedSum = allocation.reduce((a, b) => a + b, 0);
|
|
1075
|
+
let difference = charsToAllocate - allocatedSum;
|
|
1076
|
+
|
|
1077
|
+
// If there's a rounding error, add/subtract characters proportionally
|
|
1078
|
+
let maxIterations = arr.length; // inf loop handler :D
|
|
1079
|
+
while (difference !== 0 && maxIterations > 0) {
|
|
1080
|
+
// (same method as above)
|
|
1081
|
+
for (let i = 0; i < arr.length; i++) {
|
|
1082
|
+
if (!notAllowed.includes(arr[i].type)) {
|
|
1083
|
+
allocation[i] += Math.round((arr[i].length / sumLen) * difference);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
allocatedSum = allocation.reduce((a, b) => a + b, 0);
|
|
1087
|
+
difference = charsToAllocate - allocatedSum;
|
|
1088
|
+
|
|
1089
|
+
maxIterations--;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// if there's still a rounding error after max iterations, fuck it, just distribute it evenly
|
|
1093
|
+
if (difference !== 0) {
|
|
1094
|
+
// create an array of indices sorted low to high based on current allocations
|
|
1095
|
+
let indices = [];
|
|
1096
|
+
for (let i = 0; i < arr.length; i++) {
|
|
1097
|
+
indices.push(i);
|
|
1098
|
+
}
|
|
1099
|
+
indices.sort((a, b) => allocation[a] - allocation[b]);
|
|
1100
|
+
|
|
1101
|
+
// if we need to add or remove characters, do so from the beginning
|
|
1102
|
+
let plusminus = -1; // add or remove?
|
|
1103
|
+
if (difference > 0) {
|
|
1104
|
+
plusminus = 1;
|
|
1105
|
+
}
|
|
1106
|
+
let i = 0;
|
|
1107
|
+
let maxIterations = indices.length * 3; // run it for a while just in case
|
|
1108
|
+
while (difference > 0 && maxIterations > 0) {
|
|
1109
|
+
allocation[indices[i]] += plusminus;
|
|
1110
|
+
difference += -plusminus;
|
|
1111
|
+
|
|
1112
|
+
i += 1;
|
|
1113
|
+
// loop back to start if we end
|
|
1114
|
+
if (i >= indices.length) {
|
|
1115
|
+
i = 0;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
maxIterations += -1;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
return allocation;
|
|
1123
|
+
}
|
|
1124
|
+
}
|