maidr 2.9.1 → 2.10.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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  # maidr: Multimodal Access and Interactive Data Representation
10
10
 
11
- maidr (pronounced as 'mader') is a system for non-visual access and control of statistical plots. It aims to provide an inclusive experience for users with visual impairments by offering multiple modes of interaction: braille, text, and sonification (BTS). This comprehensive approach enhances the accessibility of data visualization and encourages a multi-modal exploration on visualization. Check out the current build: [maidr Demo](https://xability.github.io/maidr/user_study_pilot/intro.html). You may also clone or download the GitHub repo, navigate to the ./user_study_pilot folder, and open any of the html files in your browser.
11
+ maidr (pronounced as 'mader') is a system for non-visual access and control of statistical plots. It aims to provide an inclusive experience for users with visual impairments by offering multiple modes of interaction: braille, text, and sonification (BTS). This comprehensive approach enhances the accessibility of data visualization and encourages a multi-modal exploration on visualization. Check out the current build: [maidr Demo](https://xability.github.io/maidr/galleries/index.html). You may also clone or download the GitHub repo, navigate to the ./user_study_pilot folder, and open any of the html files in your browser.
12
12
 
13
13
  ## Table of Contents
14
14
 
package/dist/maidr.js CHANGED
@@ -76,6 +76,26 @@ class Constants {
76
76
  globalMinMax = true;
77
77
  ariaMode = 'assertive'; // assertive (default) / polite
78
78
 
79
+ userSettingsKeys = [
80
+ 'vol',
81
+ 'autoPlayRate',
82
+ 'brailleDisplayLength',
83
+ 'colorSelected',
84
+ 'MIN_FREQUENCY',
85
+ 'MAX_FREQUENCY',
86
+ 'keypressInterval',
87
+ 'ariaMode',
88
+ 'openAIAuthKey',
89
+ 'geminiAuthKey',
90
+ 'skillLevel',
91
+ 'skillLevelOther',
92
+ 'LLMModel',
93
+ 'LLMPreferences',
94
+ 'LLMOpenAiMulti',
95
+ 'LLMGeminiMulti',
96
+ 'autoInitLLM',
97
+ ];
98
+
79
99
  // LLM settings
80
100
  openAIAuthKey = null; // OpenAI authentication key, set in menu
81
101
  geminiAuthKey = null; // Gemini authentication key, set in menu
@@ -389,7 +409,9 @@ class Menu {
389
409
  <tr>
390
410
  <td>Open GenAI Chat</td>
391
411
  <td>${
392
- constants.control
412
+ constants.isMac
413
+ ? constants.alt
414
+ : constants.control
393
415
  } + Shift + ?</td>
394
416
  </tr>
395
417
  <tr>
@@ -454,12 +476,12 @@ class Menu {
454
476
  <option value="basic">Basic</option>
455
477
  <option value="intermediate">Intermediate</option>
456
478
  <option value="expert">Expert</option>
457
- <option value="other">other</option>
479
+ <option value="other">Other: describe in your own words</option>
458
480
  </select>
459
481
  <label for="skill_level">Level of skill in statistical charts</label>
460
482
  </p>
461
483
  <p id="skill_level_other_container" class="hidden"><input type="text" placeholder="Very basic" id="skill_level_other"> <label for="skill_level_other">Describe your level of skill in statistical charts</label></p>
462
- <p><label for="LLM_preferences">LLM Preferences</label></p>
484
+ <p><label for="LLM_preferences">Custom instructions for the chat response</label></p>
463
485
  <p><textarea id="LLM_preferences" rows="4" cols="50" placeholder="I'm a stats undergrad and work with Python. I prefer a casual tone, and favor information accuracy over creative description; just the facts please!"></textarea></p>
464
486
  </div>
465
487
  </div>
@@ -605,13 +627,22 @@ class Menu {
605
627
 
606
628
  // trigger notification that LLM will be reset
607
629
  // this is done on change of LLM model, multi settings, or skill level
608
- constants.events.push([
609
- document.getElementById('LLM_model'),
610
- 'change',
611
- function (e) {
612
- menu.NotifyOfLLMReset();
613
- },
614
- ]);
630
+ let LLMResetIds = [
631
+ 'LLM_model',
632
+ 'openai_multi',
633
+ 'gemini_multi',
634
+ 'skill_level',
635
+ 'LLM_preferences',
636
+ ];
637
+ for (let i = 0; i < LLMResetIds.length; i++) {
638
+ constants.events.push([
639
+ document.getElementById(LLMResetIds[i]),
640
+ 'change',
641
+ function (e) {
642
+ menu.NotifyOfLLMReset();
643
+ },
644
+ ]);
645
+ }
615
646
  }
616
647
 
617
648
  /**
@@ -855,6 +886,13 @@ class Menu {
855
886
  ) {
856
887
  shouldReset = true;
857
888
  }
889
+ if (
890
+ !shouldReset &&
891
+ constants.LLMPreferences !=
892
+ document.getElementById('LLM_preferences').value
893
+ ) {
894
+ shouldReset = true;
895
+ }
858
896
  if (
859
897
  !shouldReset &&
860
898
  constants.LLMModel != document.getElementById('LLM_model').value
@@ -882,24 +920,21 @@ class Menu {
882
920
  */
883
921
  SaveDataToLocalStorage() {
884
922
  let data = {};
885
- data.vol = constants.vol;
886
- data.autoPlayRate = constants.autoPlayRate;
887
- data.brailleDisplayLength = constants.brailleDisplayLength;
888
- data.colorSelected = constants.colorSelected;
889
- data.MIN_FREQUENCY = constants.MIN_FREQUENCY;
890
- data.MAX_FREQUENCY = constants.MAX_FREQUENCY;
891
- data.keypressInterval = constants.keypressInterval;
892
- data.ariaMode = constants.ariaMode;
893
- data.openAIAuthKey = constants.openAIAuthKey;
894
- data.geminiAuthKey = constants.geminiAuthKey;
895
- data.skillLevel = constants.skillLevel;
896
- data.skillLevelOther = constants.skillLevelOther;
897
- data.LLMModel = constants.LLMModel;
898
- data.LLMPreferences = constants.LLMPreferences;
899
- data.LLMOpenAiMulti = constants.LLMOpenAiMulti;
900
- data.LLMGeminiMulti = constants.LLMGeminiMulti;
901
- data.autoInitLLM = constants.autoInitLLM;
923
+ for (let i = 0; i < constants.userSettingsKeys.length; i++) {
924
+ data[constants.userSettingsKeys[i]] =
925
+ constants[constants.userSettingsKeys[i]];
926
+ }
902
927
  localStorage.setItem('settings_data', JSON.stringify(data));
928
+
929
+ // also save to tracking if we're doing that
930
+ if (constants.isTracking) {
931
+ // but not auth keys
932
+ data.openAIAuthKey = 'hidden';
933
+ data.geminiAuthKey = 'hidden';
934
+ // and need a timestamp
935
+ data.timestamp = new Date().toISOString();
936
+ tracker.SetData('settings', data);
937
+ }
903
938
  }
904
939
  /**
905
940
  * Loads data from local storage and updates the constants object with the retrieved values, to be loaded into the menu
@@ -907,23 +942,10 @@ class Menu {
907
942
  LoadDataFromLocalStorage() {
908
943
  let data = JSON.parse(localStorage.getItem('settings_data'));
909
944
  if (data) {
910
- constants.vol = data.vol;
911
- constants.autoPlayRate = data.autoPlayRate;
912
- constants.brailleDisplayLength = data.brailleDisplayLength;
913
- constants.colorSelected = data.colorSelected;
914
- constants.MIN_FREQUENCY = data.MIN_FREQUENCY;
915
- constants.MAX_FREQUENCY = data.MAX_FREQUENCY;
916
- constants.keypressInterval = data.keypressInterval;
917
- constants.ariaMode = data.ariaMode;
918
- constants.openAIAuthKey = data.openAIAuthKey;
919
- constants.geminiAuthKey = data.geminiAuthKey;
920
- constants.skillLevel = data.skillLevel;
921
- constants.skillLevelOther = data.skillLevelOther;
922
- constants.LLMModel = data.LLMModel ? data.LLMModel : constants.LLMModel;
923
- constants.LLMPreferences = data.LLMPreferences;
924
- constants.LLMOpenAiMulti = data.LLMOpenAiMulti;
925
- constants.LLMGeminiMulti = data.LLMGeminiMulti;
926
- constants.autoInitLLM = data.autoInitLLM;
945
+ for (let i = 0; i < constants.userSettingsKeys.length; i++) {
946
+ constants[constants.userSettingsKeys[i]] =
947
+ data[constants.userSettingsKeys[i]];
948
+ }
927
949
  }
928
950
  this.PopulateData();
929
951
  this.UpdateHtml();
@@ -944,7 +966,16 @@ class ChatLLM {
944
966
  this.CreateComponent();
945
967
  this.SetEvents();
946
968
  if (constants.autoInitLLM) {
947
- this.InitChatMessage();
969
+ // only run if we have API keys set
970
+ if (
971
+ (constants.LLMModel == 'openai' && constants.openAIAuthKey) ||
972
+ (constants.LLMModel == 'gemini' && constants.geminiAuthKey) ||
973
+ (constants.LLMModel == 'multi' &&
974
+ constants.openAIAuthKey &&
975
+ constants.geminiAuthKey)
976
+ ) {
977
+ this.InitChatMessage();
978
+ }
948
979
  }
949
980
  }
950
981
 
@@ -975,11 +1006,10 @@ class ChatLLM {
975
1006
  <p><button type="button">What is the title?</button></p>
976
1007
  <p><button type="button">What are the high and low values?</button></p>
977
1008
  <p><button type="button">What is the general shape of the chart?</button></p>
978
- <p><button type="button" id="more_suggestions">More</button></p>
979
1009
  </div>
980
- <div id="more_suggestions_container" class="hidden LLM_suggestions">
1010
+ <div id="more_suggestions_container" class="LLM_suggestions">
981
1011
  <p><button type="button">Please provide the title of this visualization, then provide a description for someone who is blind or low vision. Include general overview of axes and the data at a high-level.</button></p>
982
- <p><button type="button">For the visualization I shared, please provide the following (where applicable): mean, standard deviation, extrema, correlations, relational comparisons like greater than OR lesser than.</button></p>
1012
+ <p><button type="button">For the visualization I shared, please provide the following (where applicable): mean, standard deviation, extreme, correlations, relational comparisons like greater than OR lesser than.</button></p>
983
1013
  <p><button type="button">Based on the visualization shared, address the following: Do you observe any unforeseen trends? If yes, what? Please convey any complex multi-faceted patterns present. Can you identify any noteworthy exceptions that aren't readily apparent through non-visual methods of analysis?</button></p>
984
1014
  <p><button type="button">Provide context to help explain the data depicted in this visualization based on domain-specific insight.</button></p>
985
1015
  </div>
@@ -1029,12 +1059,7 @@ class ChatLLM {
1029
1059
  document,
1030
1060
  'keyup',
1031
1061
  function (e) {
1032
- if (
1033
- ((e.ctrlKey || e.metaKey) &&
1034
- e.shiftKey &&
1035
- (e.key == '?' || e.key == '¿')) ||
1036
- (e.metaKey && e.altKey && (e.key == '?' || e.key == '¿'))
1037
- ) {
1062
+ if ((e.key == '?' && (e.ctrlKey || e.metaKey)) || e.key == '¿') {
1038
1063
  chatLLM.Toggle();
1039
1064
  }
1040
1065
  },
@@ -1063,21 +1088,6 @@ class ChatLLM {
1063
1088
  ]);
1064
1089
 
1065
1090
  // ChatLLM suggestion events
1066
- // the more button
1067
- constants.events.push([
1068
- document.getElementById('more_suggestions'),
1069
- 'click',
1070
- function (e) {
1071
- document
1072
- .getElementById('more_suggestions_container')
1073
- .classList.toggle('hidden');
1074
- // focus on button right after the more button
1075
- document
1076
- .querySelector('#more_suggestions_container > p > button')
1077
- .focus();
1078
- document.getElementById('more_suggestions').remove();
1079
- },
1080
- ]);
1081
1091
  // actual suggestions:
1082
1092
  let suggestions = document.querySelectorAll(
1083
1093
  '#chatLLM .LLM_suggestions button:not(#more_suggestions)'
@@ -1188,7 +1198,7 @@ class ChatLLM {
1188
1198
  markdown = markdown.replace(/\n{3,}/g, '\n\n');
1189
1199
 
1190
1200
  try {
1191
- navigator.clipboard.writeText(markdown);
1201
+ navigator.clipboard.writeText(markdown); // note: this fails if you're on the inspector. That's fine as it'll never happen to real users
1192
1202
  } catch (err) {
1193
1203
  console.error('Failed to copy: ', err);
1194
1204
  }
@@ -1358,6 +1368,7 @@ class ChatLLM {
1358
1368
 
1359
1369
  if (data.error) {
1360
1370
  chatLLM.DisplayChatMessage(LLMName, 'Error processing request.', true);
1371
+ chatLLM.WaitingSound(false);
1361
1372
  } else {
1362
1373
  chatLLM.DisplayChatMessage(LLMName, text);
1363
1374
  }
@@ -1368,10 +1379,12 @@ class ChatLLM {
1368
1379
  } else {
1369
1380
  if (!data.error) {
1370
1381
  data.error = 'Error processing request.';
1382
+ chatLLM.WaitingSound(false);
1371
1383
  }
1372
1384
  }
1373
1385
  if (data.error) {
1374
1386
  chatLLM.DisplayChatMessage(LLMName, 'Error processing request.', true);
1387
+ chatLLM.WaitingSound(false);
1375
1388
  } else {
1376
1389
  // todo: display actual response
1377
1390
  }
@@ -1472,7 +1485,7 @@ class ChatLLM {
1472
1485
  .catch((error) => {
1473
1486
  chatLLM.WaitingSound(false);
1474
1487
  console.error('Error:', error);
1475
- chatLLM.DisplayChatMessage(LLMName, 'Error processing request.', true);
1488
+ chatLLM.DisplayChatMessage('OpenAI', 'Error processing request.', true);
1476
1489
  // also todo: handle errors somehow
1477
1490
  });
1478
1491
  }
@@ -1562,6 +1575,8 @@ class ChatLLM {
1562
1575
  // Process the response
1563
1576
  chatLLM.ProcessLLMResponse(result.response, 'gemini');
1564
1577
  } catch (error) {
1578
+ chatLLM.WaitingSound(false);
1579
+ chatLLM.DisplayChatMessage('Gemini', 'Error processing request.', true);
1565
1580
  console.error('Error in GeminiPrompt:', error);
1566
1581
  throw error; // Rethrow the error for further handling if necessary
1567
1582
  }
@@ -1623,11 +1638,6 @@ class ChatLLM {
1623
1638
  ResetLLM() {
1624
1639
  // clear the main chat history
1625
1640
  document.getElementById('chatLLM_chat_history').innerHTML = '';
1626
- // unhide the more button
1627
- document
1628
- .getElementById('more_suggestions_container')
1629
- .classList.add('hidden');
1630
- document.getElementById('more_suggestions').classList.remove('hidden');
1631
1641
 
1632
1642
  // reset the data
1633
1643
  this.requestJson = null;
@@ -2081,7 +2091,7 @@ class Tracker {
2081
2091
  DataSetup() {
2082
2092
  let prevData = this.GetTrackerData();
2083
2093
  if (prevData) {
2084
- // good to go already, do nothing
2094
+ // good to go already, do nothing, but make sure we have our containers
2085
2095
  } else {
2086
2096
  let data = {};
2087
2097
  data.userAgent = Object.assign(navigator.userAgent);
@@ -2089,8 +2099,10 @@ class Tracker {
2089
2099
  data.language = Object.assign(navigator.language);
2090
2100
  data.platform = Object.assign(navigator.platform);
2091
2101
  data.events = [];
2102
+ data.settings = [];
2092
2103
 
2093
2104
  this.SaveTrackerData(data);
2105
+ this.SaveSettings();
2094
2106
  }
2095
2107
  }
2096
2108
 
@@ -2137,6 +2149,17 @@ class Tracker {
2137
2149
  this.DataSetup();
2138
2150
  }
2139
2151
 
2152
+ SaveSettings() {
2153
+ // fetch all settings, push to data.settings
2154
+ let settings = JSON.parse(localStorage.getItem('settings_data'));
2155
+ if (settings) {
2156
+ // don't store their auth keys
2157
+ settings.openAIAuthKey = 'hidden';
2158
+ settings.geminiAuthKey = 'hidden';
2159
+ this.SetData('settings', settings);
2160
+ }
2161
+ }
2162
+
2140
2163
  /**
2141
2164
  * Logs an event with various properties to the tracker data.
2142
2165
  * @param {Event} e - The event to log.
@@ -2280,6 +2303,7 @@ class Tracker {
2280
2303
  constants.plotOrientation == 'vert' ? position.x : position.y;
2281
2304
  let sectionPos =
2282
2305
  constants.plotOrientation == 'vert' ? position.y : position.x;
2306
+ let sectionLabel = plot.sections[sectionPos];
2283
2307
 
2284
2308
  if (!this.isUndefinedOrNull(plot.x_group_label)) {
2285
2309
  x_label = plot.x_group_label;
@@ -2289,42 +2313,26 @@ class Tracker {
2289
2313
  }
2290
2314
  if (constants.plotOrientation == 'vert') {
2291
2315
  if (plotPos > -1 && sectionPos > -1) {
2292
- if (
2293
- !this.isUndefinedOrNull(plot.plotData[plotPos][sectionPos].label)
2294
- ) {
2295
- y_tickmark = plot.plotData[plotPos][sectionPos].label;
2316
+ if (!this.isUndefinedOrNull(sectionLabel)) {
2317
+ y_tickmark = sectionLabel;
2296
2318
  }
2297
2319
  if (!this.isUndefinedOrNull(plot.x_labels[position.x])) {
2298
2320
  x_tickmark = plot.x_labels[position.x];
2299
2321
  }
2300
- if (
2301
- !this.isUndefinedOrNull(plot.plotData[plotPos][sectionPos].values)
2302
- ) {
2303
- value = plot.plotData[plotPos][sectionPos].values;
2304
- } else if (
2305
- !this.isUndefinedOrNull(plot.plotData[plotPos][sectionPos].y)
2306
- ) {
2307
- value = plot.plotData[plotPos][sectionPos].y;
2322
+ if (!this.isUndefinedOrNull(plot.plotData[plotPos][sectionLabel])) {
2323
+ value = plot.plotData[plotPos][sectionLabel];
2308
2324
  }
2309
2325
  }
2310
2326
  } else {
2311
2327
  if (plotPos > -1 && sectionPos > -1) {
2312
- if (
2313
- !this.isUndefinedOrNull(plot.plotData[plotPos][sectionPos].label)
2314
- ) {
2315
- x_tickmark = plot.plotData[plotPos][sectionPos].label;
2328
+ if (!this.isUndefinedOrNull(sectionLabel)) {
2329
+ x_tickmark = sectionLabel;
2316
2330
  }
2317
2331
  if (!this.isUndefinedOrNull(plot.y_labels[position.y])) {
2318
2332
  y_tickmark = plot.y_labels[position.y];
2319
2333
  }
2320
- if (
2321
- !this.isUndefinedOrNull(plot.plotData[plotPos][sectionPos].values)
2322
- ) {
2323
- value = plot.plotData[plotPos][sectionPos].values;
2324
- } else if (
2325
- !this.isUndefinedOrNull(plot.plotData[plotPos][sectionPos].x)
2326
- ) {
2327
- value = plot.plotData[plotPos][sectionPos].x;
2334
+ if (!this.isUndefinedOrNull(plot.plotData[plotPos][sectionLabel])) {
2335
+ value = plot.plotData[plotPos][sectionLabel];
2328
2336
  }
2329
2337
  }
2330
2338
  }
@@ -2361,10 +2369,14 @@ class Tracker {
2361
2369
 
2362
2370
  SetData(key, value) {
2363
2371
  let data = this.GetTrackerData();
2364
- if (key == 'events') {
2365
- data[key].push(value);
2366
- } else {
2372
+ let arrayKeys = ['events', 'ChatHistory', 'settings'];
2373
+ if (!arrayKeys.includes(key)) {
2367
2374
  data[key] = value;
2375
+ } else {
2376
+ if (!data[key]) {
2377
+ data[key] = [];
2378
+ }
2379
+ data[key].push(value);
2368
2380
  }
2369
2381
  this.SaveTrackerData(data);
2370
2382
  }