maidr 2.4.0 → 2.5.1

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
@@ -13,11 +13,15 @@ maidr (pronounced as 'mader') is a system for non-visual access and control of s
13
13
  ## Table of Contents
14
14
 
15
15
  1. [Usage](#usage)
16
- 2. [Controls](#controls)
17
- 3. [Braille Generation](#braille-generation)
18
- 4. [License](#license)
19
- 5. [Contact](#contact)
20
- 6. [Acknowledgments](#acknowledgments)
16
+ 1. [Controls](#controls)
17
+ 1. [Braille Generation](#braille-generation)
18
+ 1. [API](#api)
19
+ 1. [Binders](#binders)
20
+ 1. [Papers](#papers)
21
+ 1. [License](#license)
22
+ 1. [Contact](#contact)
23
+ 1. [Acknowledgments](#acknowledgments)
24
+
21
25
 
22
26
  ## Usage
23
27
 
@@ -47,7 +51,7 @@ To use maidr, follow these steps:
47
51
  </html>
48
52
  ```
49
53
 
50
- 3. Add your data: Include your data as a json schema directly in the HTML file. There should be a single `maidr` object with the following properties, or an array of objects if multiple charts exist on the page. Your json schema may look like so: (values for demonstration purposes)
54
+ 3. Add your data: Include your data as a json schema directly in the HTML file. There should be a single `maidr` object with the following properties, or an array of objects if multiple plots exist on the page. Your json schema may look like so: (values for demonstration purposes)
51
55
 
52
56
  ```javascript
53
57
  // a single plot
@@ -74,7 +78,7 @@ To use maidr, follow these steps:
74
78
  data: ...
75
79
  }
76
80
 
77
- // or, multiple charts
81
+ // or, multiple plots
78
82
  let maidr = [
79
83
  {
80
84
  type: 'box',
@@ -220,7 +224,7 @@ For more information and examples, refer to the example HTML files provided in t
220
224
 
221
225
  ## Controls
222
226
 
223
- To interact with the charts using maidr, follow these steps:
227
+ To interact with the plots using maidr, follow these steps:
224
228
 
225
229
  1. Press the **Tab** key to focus on the SVG element.
226
230
  2. Use the **arrow keys** to move around the plot.
@@ -355,6 +359,53 @@ In the braille representation of segmented bar plots, braille depends on where y
355
359
 
356
360
  In the Braille representation of a lineplot, braille is nearly identical to the above barplot: data values are encoded as Braille characters based on their relative magnitude within the plot. Low values are denoted by Braille characters that have dots only along the bottom, while high values are indicated by characters that have dots higher up.
357
361
 
362
+ ## API
363
+
364
+ maidr is available via a restful API. Learn more about the usage at [maidr-api](https://github.com/xability/maidr-api) repo.
365
+
366
+ ## Binders
367
+
368
+ We currently provide the following binders, all of which can be found at each separate repo:
369
+
370
+ - [x] Python binder for matplotlib and seaborn: [py_maidr](https://github.com/xability/py_maidr).
371
+
372
+ - [ ] R binder for ggplot2: [r_maidr](https://github.com/xability/r_maidr).
373
+
374
+ ## Papers
375
+
376
+ To learn more about the theoretical background and user study results, we recommend you read and cite the following papers.
377
+
378
+ 1. [MAIDR: Making Statistical Visualizations Accessible with Multimodal Data Representation](https://arxiv.org/abs/2403.00717):
379
+
380
+ ```tex
381
+ @inproceedings{seoMAIDR2024,
382
+ title = {{{MAIDR}}: Making Statistical Visualizations Accessible with Multimodal Data Representation},
383
+ booktitle = {Proceedings of the {{SIGCHI Conference}} on {{Human Factors}} in {{Computing Systems}}},
384
+ author = {Seo, JooYoung and Xia, Yilin and Lee, Bongshin and McCurry, Sean and Yam, Yu Jun},
385
+ year = {2024},
386
+ doi = {10.1145/3613904.3642730}
387
+ }
388
+ ```
389
+
390
+ 1. [Born Accessible Data Science and Visualization Courses: Challenges of Developing Curriculum to be Taught by Blind Instructors to Blind Students](https://arxiv.org/abs/2403.02568v1):
391
+
392
+ ```tex
393
+ @misc{seoBornAccessibleData2024,
394
+ title = {Born {{Accessible Data Science}} and {{Visualization Courses}}: {{Challenges}} of {{Developing Curriculum}} to Be {{Taught}} by {{Blind Instructors}} to {{Blind Students}}},
395
+ shorttitle = {Born {{Accessible Data Science}} and {{Visualization Courses}}},
396
+ author = {Seo, JooYoung and O'Modhrain, Sile and Xia, Yilin and Kamath, Sanchita and Lee, Bongshin and Coughlan, James M.},
397
+ year = {2024},
398
+ month = mar,
399
+ number = {arXiv:2403.02568},
400
+ eprint = {2403.02568},
401
+ primaryclass = {cs},
402
+ publisher = {{arXiv}},
403
+ urldate = {2024-03-08},
404
+ archiveprefix = {arxiv},
405
+ keywords = {Computer Science - Human-Computer Interaction}
406
+ }
407
+ ```
408
+
358
409
  ## License
359
410
 
360
411
  This project is licensed under the GPL 3 License.
package/dist/maidr.js CHANGED
@@ -86,6 +86,7 @@ class Constants {
86
86
  'You are a helpful assistant describing the chart to a blind person. ';
87
87
  skillLevel = 'basic'; // basic / intermediate / expert
88
88
  skillLevelOther = ''; // custom skill level
89
+ autoInitLLM = true; // auto initialize LLM on page load
89
90
 
90
91
  // user controls (not exposed to menu, with shortcuts usually)
91
92
  showDisplay = 1; // true / false
@@ -429,6 +430,9 @@ class Menu {
429
430
  <span id="gemini_multi_container" class="hidden"><input type="checkbox" id="gemini_multi" name="gemini_multi" aria-label="Use Gemini in Multi modal mode"></span>
430
431
  <input type="password" id="gemini_auth_key"><button aria-label="Delete Gemini key" title="Delete Gemini key" id="delete_gemini_key" class="invis_button">&times;</button><label for="gemini_auth_key">Gemini Authentication Key</label>
431
432
  </p>
433
+ <p><input type="checkbox" ${
434
+ constants.autoInitLLM ? 'checked' : ''
435
+ } id="init_llm_on_load" name="init_llm_on_load"><label for="init_llm_on_load">Start first LLM chat chart load</label></p>
432
436
  <p>
433
437
  <select id="skill_level">
434
438
  <option value="basic">Basic</option>
@@ -740,7 +744,7 @@ class Menu {
740
744
  * Saves the data from the HTML elements into the constants object.
741
745
  */
742
746
  SaveData() {
743
- this.HandleLLMChanges();
747
+ let shouldReset = this.ShouldLLMReset();
744
748
 
745
749
  constants.vol = document.getElementById('vol').value;
746
750
  constants.autoPlayRate = document.getElementById('autoplay_rate').value;
@@ -760,9 +764,9 @@ class Menu {
760
764
  document.getElementById('skill_level_other').value;
761
765
  constants.LLMModel = document.getElementById('LLM_model').value;
762
766
  constants.LLMPreferences = document.getElementById('LLM_preferences').value;
763
-
764
767
  constants.LLMOpenAiMulti = document.getElementById('openai_multi').checked;
765
768
  constants.LLMGeminiMulti = document.getElementById('gemini_multi').checked;
769
+ constants.autoInitLLM = document.getElementById('init_llm_on_load').checked;
766
770
 
767
771
  // aria
768
772
  if (document.getElementById('aria_mode_assertive').checked) {
@@ -773,6 +777,12 @@ class Menu {
773
777
 
774
778
  this.SaveDataToLocalStorage();
775
779
  this.UpdateHtml();
780
+
781
+ if (shouldReset) {
782
+ if (chatLLM) {
783
+ chatLLM.ResetLLM();
784
+ }
785
+ }
776
786
  }
777
787
 
778
788
  /**
@@ -789,6 +799,8 @@ class Menu {
789
799
  document
790
800
  .getElementById(constants.announcement_container_id)
791
801
  .setAttribute('aria-live', constants.ariaMode);
802
+
803
+ document.getElementById('init_llm_on_load').checked = constants.autoInitLLM;
792
804
  }
793
805
 
794
806
  /**
@@ -797,6 +809,10 @@ class Menu {
797
809
  NotifyOfLLMReset() {
798
810
  let html =
799
811
  '<p id="LLM_reset_notification">Note: Changes in LLM settings will reset any existing conversation.</p>';
812
+
813
+ if (document.getElementById('LLM_reset_notification')) {
814
+ document.getElementById('LLM_reset_notification').remove();
815
+ }
800
816
  document
801
817
  .getElementById('save_and_close_menu')
802
818
  .insertAdjacentHTML('beforebegin', html);
@@ -813,7 +829,7 @@ class Menu {
813
829
  * Handles changes to the LLM model and multi-modal settings.
814
830
  * We reset if we change the LLM model, multi settings, or skill level.
815
831
  */
816
- HandleLLMChanges() {
832
+ ShouldLLMReset() {
817
833
  let shouldReset = false;
818
834
  if (
819
835
  !shouldReset &&
@@ -837,11 +853,7 @@ class Menu {
837
853
  shouldReset = true;
838
854
  }
839
855
 
840
- if (shouldReset) {
841
- if (chatLLM) {
842
- chatLLM.ResetChatHistory();
843
- }
844
- }
856
+ return shouldReset;
845
857
  }
846
858
 
847
859
  /**
@@ -868,6 +880,7 @@ class Menu {
868
880
  data.LLMPreferences = constants.LLMPreferences;
869
881
  data.LLMOpenAiMulti = constants.LLMOpenAiMulti;
870
882
  data.LLMGeminiMulti = constants.LLMGeminiMulti;
883
+ data.autoInitLLM = constants.autoInitLLM;
871
884
  localStorage.setItem('settings_data', JSON.stringify(data));
872
885
  }
873
886
  /**
@@ -892,6 +905,7 @@ class Menu {
892
905
  constants.LLMPreferences = data.LLMPreferences;
893
906
  constants.LLMOpenAiMulti = data.LLMOpenAiMulti;
894
907
  constants.LLMGeminiMulti = data.LLMGeminiMulti;
908
+ constants.autoInitLLM = data.autoInitLLM;
895
909
  }
896
910
  this.PopulateData();
897
911
  this.UpdateHtml();
@@ -907,8 +921,12 @@ class ChatLLM {
907
921
  constructor() {
908
922
  this.firstTime = true;
909
923
  this.firstMulti = true;
924
+ this.shown = false;
910
925
  this.CreateComponent();
911
926
  this.SetEvents();
927
+ if (constants.autoInitLLM) {
928
+ this.InitChatMessage();
929
+ }
912
930
  }
913
931
 
914
932
  /**
@@ -1073,10 +1091,18 @@ class ChatLLM {
1073
1091
  document.getElementById('reset_chatLLM'),
1074
1092
  'click',
1075
1093
  function (e) {
1076
- chatLLM.ResetChatHistory();
1094
+ chatLLM.ResetLLM();
1077
1095
  },
1078
1096
  ]);
1079
1097
 
1098
+ // bookmark:
1099
+ //
1100
+ // have LLM run on init (#425)
1101
+ // quiet first, but if they open window, it does the beep and aria live alert
1102
+ // make toggle in settings to yes / no auto initiate LLM (or wait for window to open)
1103
+ // as part of this, fix reset so it loads the LLM again without refreshing the window,
1104
+ // left undone from the other request
1105
+
1080
1106
  // copy to clipboard
1081
1107
  constants.events.push([
1082
1108
  document.getElementById('chatLLM_copy_all'),
@@ -1123,7 +1149,7 @@ class ChatLLM {
1123
1149
  async Submit(text, firsttime = false) {
1124
1150
  // start waiting sound
1125
1151
  if (constants.playLLMWaitingSound) {
1126
- chatLLM.WaitingSound(true);
1152
+ this.WaitingSound(true);
1127
1153
  }
1128
1154
 
1129
1155
  let img = null;
@@ -1169,7 +1195,7 @@ class ChatLLM {
1169
1195
  let delay = 1000;
1170
1196
  let freq = 440; // a440 babee
1171
1197
  constants.waitingInterval = setInterval(function () {
1172
- if (audio) {
1198
+ if (audio && chatLLM.shown) {
1173
1199
  audio.playOscillator(freq, 0.2, 0);
1174
1200
  }
1175
1201
  }, delay);
@@ -1181,6 +1207,15 @@ class ChatLLM {
1181
1207
  }
1182
1208
  }
1183
1209
 
1210
+ InitChatMessage() {
1211
+ // get name from resource
1212
+ let LLMName = resources.GetString(constants.LLMModel);
1213
+ this.firstTime = false;
1214
+ this.DisplayChatMessage(LLMName, resources.GetString('processing'), true);
1215
+ let defaultPrompt = this.GetDefaultPrompt();
1216
+ this.Submit(defaultPrompt, true);
1217
+ }
1218
+
1184
1219
  /**
1185
1220
  * Processes the response from the LLM and displays it to the user.
1186
1221
  * @function
@@ -1392,9 +1427,9 @@ class ChatLLM {
1392
1427
  };
1393
1428
 
1394
1429
  // Generate the content
1395
- //console.log('LLM request: ', prompt, image);
1430
+ console.log('LLM request: ', prompt, image);
1396
1431
  const result = await model.generateContent([prompt, image]);
1397
- //console.log(result.response.text());
1432
+ console.log(result.response.text());
1398
1433
 
1399
1434
  // Process the response
1400
1435
  chatLLM.ProcessLLMResponse(result.response, 'gemini');
@@ -1457,7 +1492,7 @@ class ChatLLM {
1457
1492
  /**
1458
1493
  * Resets the chat history window
1459
1494
  */
1460
- ResetChatHistory() {
1495
+ ResetLLM() {
1461
1496
  // clear the main chat history
1462
1497
  document.getElementById('chatLLM_chat_history').innerHTML = '';
1463
1498
  // unhide the more button
@@ -1469,6 +1504,11 @@ class ChatLLM {
1469
1504
  // reset the data
1470
1505
  this.requestJson = null;
1471
1506
  this.firstTime = true;
1507
+
1508
+ // and start over, if enabled
1509
+ if (constants.autoInitLLM) {
1510
+ chatLLM.InitChatMessage();
1511
+ }
1472
1512
  }
1473
1513
 
1474
1514
  /**
@@ -1502,6 +1542,7 @@ class ChatLLM {
1502
1542
  onoff = false;
1503
1543
  }
1504
1544
  }
1545
+ chatLLM.shown = onoff;
1505
1546
  if (onoff) {
1506
1547
  // open
1507
1548
  this.whereWasMyFocus = document.activeElement;
@@ -1511,20 +1552,6 @@ class ChatLLM {
1511
1552
  .getElementById('chatLLM_modal_backdrop')
1512
1553
  .classList.remove('hidden');
1513
1554
  document.querySelector('#chatLLM .close').focus();
1514
-
1515
- // first time, send default query
1516
- if (this.firstTime) {
1517
- // get name from resource
1518
- let LLMName = resources.GetString(constants.LLMModel);
1519
- this.firstTime = false;
1520
- this.DisplayChatMessage(
1521
- LLMName,
1522
- resources.GetString('processing'),
1523
- true
1524
- );
1525
- let defaultPrompt = this.GetDefaultPrompt();
1526
- this.Submit(defaultPrompt, true);
1527
- }
1528
1555
  } else {
1529
1556
  // close
1530
1557
  document.getElementById('chatLLM').classList.add('hidden');
@@ -6631,19 +6658,21 @@ class LinePlot {
6631
6658
 
6632
6659
  if (elements) {
6633
6660
  this.plotLine = elements[elements.length - 1];
6661
+ } else {
6662
+ constants.hasRect = 0;
6663
+ }
6634
6664
 
6635
- let pointCoords = this.GetPointCoords();
6636
- let pointValues = this.GetPoints();
6665
+ let pointCoords = this.GetPointCoords();
6666
+ let pointValues = this.GetPoints();
6637
6667
 
6638
- this.chartLineX = pointCoords[0]; // x coordinates of curve
6639
- this.chartLineY = pointCoords[1]; // y coordinates of curve
6668
+ this.chartLineX = pointCoords[0]; // x coordinates of curve
6669
+ this.chartLineY = pointCoords[1]; // y coordinates of curve
6640
6670
 
6641
- this.pointValuesX = pointValues[0]; // actual values of x
6642
- this.pointValuesY = pointValues[1]; // actual values of y
6671
+ this.pointValuesX = pointValues[0]; // actual values of x
6672
+ this.pointValuesY = pointValues[1]; // actual values of y
6643
6673
 
6644
- this.curveMinY = Math.min(...this.pointValuesY);
6645
- this.curveMaxY = Math.max(...this.pointValuesY);
6646
- }
6674
+ this.curveMinY = Math.min(...this.pointValuesY);
6675
+ this.curveMaxY = Math.max(...this.pointValuesY);
6647
6676
  }
6648
6677
 
6649
6678
  /**
@@ -6678,27 +6707,47 @@ class LinePlot {
6678
6707
  */
6679
6708
  GetPointCoords() {
6680
6709
  let svgLineCoords = [[], []];
6681
- // lineplot SVG containing path element instead of polyline
6682
- if (this.plotLine instanceof SVGPathElement) {
6683
- // Assuming the path data is in the format "M x y L x y L x y L x y"
6684
- const pathD = this.plotLine.getAttribute('d');
6685
- const regex = /[ML]\s*(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)/g;
6686
6710
 
6687
- let match;
6688
- while ((match = regex.exec(pathD)) !== null) {
6689
- svgLineCoords[0].push(match[1]); // x coordinate
6690
- svgLineCoords[1].push(match[3]); // y coordinate
6711
+ if (this.plotLine) {
6712
+ // lineplot SVG containing path element instead of polyline
6713
+ if (this.plotLine instanceof SVGPathElement) {
6714
+ // Assuming the path data is in the format "M x y L x y L x y L x y"
6715
+ const pathD = this.plotLine.getAttribute('d');
6716
+ const regex = /[ML]\s*(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)/g;
6717
+
6718
+ let match;
6719
+ while ((match = regex.exec(pathD)) !== null) {
6720
+ svgLineCoords[0].push(match[1]); // x coordinate
6721
+ svgLineCoords[1].push(match[3]); // y coordinate
6722
+ }
6723
+ } else {
6724
+ let points = this.plotLine.getAttribute('points').split(' ');
6725
+ for (let i = 0; i < points.length; i++) {
6726
+ if (points[i] !== '') {
6727
+ let point = points[i].split(',');
6728
+ svgLineCoords[0].push(point[0]);
6729
+ svgLineCoords[1].push(point[1]);
6730
+ }
6731
+ }
6691
6732
  }
6692
6733
  } else {
6693
- let points = this.plotLine.getAttribute('points').split(' ');
6694
- for (let i = 0; i < points.length; i++) {
6695
- if (points[i] !== '') {
6696
- let point = points[i].split(',');
6697
- svgLineCoords[0].push(point[0]);
6698
- svgLineCoords[1].push(point[1]);
6734
+ // fetch from data instead
6735
+ let x_points = [];
6736
+ let y_points = [];
6737
+
6738
+ let data;
6739
+ if ('data' in singleMaidr) {
6740
+ data = singleMaidr.data;
6741
+ }
6742
+ if (typeof data !== 'undefined') {
6743
+ for (let i = 0; i < data.length; i++) {
6744
+ x_points.push(data[i].x);
6745
+ y_points.push(data[i].y);
6699
6746
  }
6700
6747
  }
6748
+ return [x_points, y_points];
6701
6749
  }
6750
+
6702
6751
  return svgLineCoords;
6703
6752
  }
6704
6753
 
@@ -6720,8 +6769,6 @@ class LinePlot {
6720
6769
  y_points.push(data[i].y);
6721
6770
  }
6722
6771
  return [x_points, y_points];
6723
- } else {
6724
- return;
6725
6772
  }
6726
6773
  }
6727
6774
 
@@ -10220,7 +10267,7 @@ class Control {
10220
10267
  if (constants.showDisplay) {
10221
10268
  display.displayValues();
10222
10269
  }
10223
- if (constants.showRect) {
10270
+ if (constants.showRect && constants.hasRect) {
10224
10271
  point.UpdatePointDisplay();
10225
10272
  }
10226
10273
  if (constants.sonifMode != 'off') {