diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..ea674ceb --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,48 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "yarn lint", + "type": "shell", + "command": "yarn lint", + "group": "build", + "presentation": { + "reveal": "always" + }, + "problemMatcher": [] + }, + { + "label": "yarn build", + "type": "shell", + "command": "yarn build", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always" + }, + "problemMatcher": [] + }, + { + "label": "yarn test", + "type": "shell", + "command": "yarn test", + "group": "test", + "presentation": { + "reveal": "always" + }, + "problemMatcher": [] + }, + { + "label": "yarn docs", + "type": "shell", + "command": "yarn docs", + "group": "build", + "presentation": { + "reveal": "always" + }, + "problemMatcher": [] + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..7c500128 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [Unreleased] + +## [1.0.0] - 2023-11-01 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..b0f3a957 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,67 @@ +# Contributing to maidr + +Thank you for your interest in contributing to maidr project! We welcome contributions from everyone. + +## Getting Started + +To get started, please follow these steps: + +1. Fork the repository on GitHub. + +2. Clone your forked repository to your local machine. + +3. install `npm` if you don't have it already (version 9 or higher is recommended). + +4. Install `yarn` if you don't have it already. You can install it by running the following command in your terminal: + +```shell +npm install -g yarn +`````` + +5. In the forked project root, install the dependencies by running the following command in your terminal: + +```shell +yarn install +``` + +5. Make changes. + +6. Run `yarn lint` to lint the code. + +7. Run `yarn build` to build the project. + +8. Run `yarn test` to run the tests. + +9. Run `yarn docs` to generate the documentation. + +10. Test the new features locally. + +11. Commit your changes and push them to your forked repository. + +12. Create a pull request to the main repository. + +## Guidelines + +Please follow these guidelines when contributing to the project: + +- Use JavaScript (es6) for all code. +- Write clear and concise commit messages. +- Follow the code style and formatting guidelines. +- Write tests for new features and bug fixes. +- Update the documentation as needed in `CHANGELOG.md`. + +## Code Style and Formatting + +We use ESLint to enforce a consistent code style and formatting. Please run `yarn lint` before committing your changes to ensure that your code meets the standards. + +## Tests + +We use Jest for unit testing. Please run `yarn test` before committing your changes to ensure that your changes do not break any existing tests. + +## Documentation + +We use jsdoc for documentation. Please update the relevant documentation in js files when making changes to the project. + +## Code of Conduct + +Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. \ No newline at end of file diff --git a/contribution.md b/contribution.md deleted file mode 100644 index e69de29b..00000000 diff --git a/dist/maidr.js b/dist/maidr.js index 7c3d3b3e..715193fc 100644 --- a/dist/maidr.js +++ b/dist/maidr.js @@ -4764,6 +4764,487 @@ class Histogram { } } +class LinePlot { + constructor() { + this.SetLineLayer(); + this.SetAxes(); + + let legendX = ''; + let legendY = ''; + if ('axes' in singleMaidr) { + // legend labels + if (singleMaidr.axes.x) { + if (singleMaidr.axes.x.label) { + if (legendX == '') { + legendX = singleMaidr.axes.x.label; + } + } + } + if (singleMaidr.axes.y) { + if (singleMaidr.axes.y.label) { + if (legendY == '') { + legendY = singleMaidr.axes.y.label; + } + } + } + } + + this.plotLegend = { + x: legendX, + y: legendY, + }; + + // title + this.title = ''; + if ('labels' in singleMaidr) { + if ('title' in singleMaidr.labels) { + this.title = singleMaidr.labels.title; + } + } + if (this.title == '') { + if ('title' in singleMaidr) { + this.title = singleMaidr.title; + } + } + + // subtitle + if ('labels' in singleMaidr) { + if ('subtitle' in singleMaidr.labels) { + this.subtitle = singleMaidr.labels.subtitle; + } + } + // caption + if ('labels' in singleMaidr) { + if ('caption' in singleMaidr.labels) { + this.caption = singleMaidr.labels.caption; + } + } + } + + SetLineLayer() { + let len = maidr.elements.length; + this.plotLine = maidr.elements[len - 1]; + if (typeof this.plotLine !== 'undefined') { + let pointCoords = this.GetPointCoords(); + let pointValues = this.GetPoints(); + + this.chartLineX = pointCoords[0]; // x coordinates of curve + this.chartLineY = pointCoords[1]; // y coordinates of curve + + this.pointValuesX = pointValues[0]; // actual values of x + this.pointValuesY = pointValues[1]; // actual values of y + + this.curveMinY = Math.min(...this.pointValuesY); + this.curveMaxY = Math.max(...this.pointValuesY); + constants.minX = 0; + constants.maxX = this.pointValuesX.length - 1; + constants.minY = this.curveMinY; + constants.maxY = this.curveMaxY; + + constants.autoPlayRate = Math.min( + Math.ceil(constants.AUTOPLAY_DURATION / (constants.maxX + 1)), + constants.MAX_SPEED + ); + constants.DEFAULT_SPEED = constants.autoPlayRate; + if (constants.autoPlayRate < constants.MIN_SPEED) { + constants.MIN_SPEED = constants.autoPlayRate; + } + + // this.gradient = this.GetGradient(); + } + } + + SetMinMax() { + constants.minX = 0; + constants.maxX = this.pointValuesX.length - 1; + constants.minY = this.curveMinY; + constants.maxY = this.curveMaxY; + constants.autoPlayRate = Math.ceil( + constants.AUTOPLAY_DURATION / (constants.maxX + 1) + ); + } + + GetPointCoords() { + let svgLineCoords = [[], []]; + let points = this.plotLine.getAttribute('points').split(' '); + for (let i = 0; i < points.length; i++) { + if (points[i] !== '') { + let point = points[i].split(','); + svgLineCoords[0].push(point[0]); + svgLineCoords[1].push(point[1]); + } + } + return svgLineCoords; + } + + GetPoints() { + let x_points = []; + let y_points = []; + + let data; + if ('data' in singleMaidr) { + data = singleMaidr.data; + } + if (typeof data !== 'undefined') { + for (let i = 0; i < data.length; i++) { + x_points.push(data[i].x); + y_points.push(data[i].y); + } + return [x_points, y_points]; + } else { + return; + } + } + + // GetGradient() { + // let gradients = []; + + // for (let i = 0; i < this.pointValuesY.length - 1; i++) { + // let abs_grad = Math.abs( + // (this.pointValuesY[i + 1] - this.pointValuesY[i]) / + // (this.pointValuesX[i + 1] - this.pointValuesX[i]) + // ).toFixed(3); + // gradients.push(abs_grad); + // } + + // gradients.push('end'); + + // return gradients; + // } + + SetAxes() { + this.x_group_label = ''; + this.y_group_label = ''; + this.title = ''; + if ('axes' in singleMaidr) { + if ('x' in singleMaidr.axes) { + if (this.x_group_label == '') { + this.x_group_label = singleMaidr.axes.x.label; + } + } + if ('y' in singleMaidr.axes) { + if (this.y_group_label == '') { + this.y_group_label = singleMaidr.axes.y.label; + } + } + } + if ('title' in singleMaidr) { + if (this.title == '') { + this.title = singleMaidr.title; + } + } + } + + PlayTones() { + audio.playTone(); + } +} + +class Point { + constructor() { + this.x = plot.chartLineX[0]; + this.y = plot.chartLineY[0]; + } + + async UpdatePoints() { + await this.ClearPoints(); + this.x = plot.chartLineX[position.x]; + this.y = plot.chartLineY[position.x]; + } + + async PrintPoints() { + await this.ClearPoints(); + await this.UpdatePoints(); + const svgns = 'http://www.w3.org/2000/svg'; + var point = document.createElementNS(svgns, 'circle'); + point.setAttribute('id', 'highlight_point'); + point.setAttribute('cx', this.x); + point.setAttribute('cy', this.y); + point.setAttribute('r', 1.75); + point.setAttribute( + 'style', + 'fill:' + constants.colorSelected + ';stroke:' + constants.colorSelected + ); + constants.chart.appendChild(point); + } + + async ClearPoints() { + let points = document.getElementsByClassName('highlight_point'); + for (let i = 0; i < points.length; i++) { + document.getElementsByClassName('highlight_point')[i].remove(); + } + if (document.getElementById('highlight_point')) + document.getElementById('highlight_point').remove(); + } + + UpdatePointDisplay() { + this.ClearPoints(); + this.UpdatePoints(); + this.PrintPoints(); + } +} + +class Segmented { + constructor() { + // initialize variables level, data, and elements + let level = null; + let fill = null; + let data = null; + let elements = null; + if ('axes' in singleMaidr) { + //axes.x.level + if ('x' in singleMaidr.axes) { + if ('level' in singleMaidr.axes.x) { + level = singleMaidr.axes.x.level; + } + } else if ('y' in singleMaidr.axes) { + if ('level' in singleMaidr.axes.y) { + level = singleMaidr.axes.y.level; + } + } + // axes.fill + if ('fill' in singleMaidr.axes) { + if ('level' in singleMaidr.axes.fill) { + fill = singleMaidr.axes.fill.level; + } + } + } + if ('data' in singleMaidr) { + data = singleMaidr.data; + } + if ('elements' in singleMaidr) { + elements = singleMaidr.elements; + } + + // gracefull failure: must have level + fill + data, elements optional + if (elements == null) { + LogError.LogAbsentElement('elements'); + constants.hasRect = 0; + } + if (level != null && fill != null && data != null) { + this.level = level; + this.fill = fill.reverse(); // typically fill is in reverse order + let dataAndELements = this.ParseData(data, elements); + this.plotData = dataAndELements[0]; + this.elements = dataAndELements[1]; + } else { + console.log( + 'Segmented chart missing level, fill, or data. Unable to create chart.' + ); + return; + } + + // column labels, both legend and tick + let legendX = ''; + let legendY = ''; + if ('axes' in singleMaidr) { + // legend labels + if (singleMaidr.axes.x) { + if (singleMaidr.axes.x.label) { + legendX = singleMaidr.axes.x.label; + } + } + if (singleMaidr.axes.y) { + if (singleMaidr.axes.y.label) { + legendY = singleMaidr.axes.y.label; + } + } + } + // labels override axes + if ('labels' in singleMaidr) { + if ('x' in singleMaidr.labels) { + legendX = singleMaidr.labels.x; + } + if ('y' in singleMaidr.labels) { + legendY = singleMaidr.labels.y; + } + } + + this.plotLegend = { + x: legendX, + y: legendY, + }; + + // title + this.title = ''; + if ('labels' in singleMaidr) { + if ('title' in singleMaidr.labels) { + this.title = singleMaidr.labels.title; + } + } + if (this.title == '') { + if ('title' in singleMaidr) { + this.title = singleMaidr.title; + } + } + + // subtitle + if ('labels' in singleMaidr) { + if ('subtitle' in singleMaidr.labels) { + this.subtitle = singleMaidr.labels.subtitle; + } + } + // caption + if ('labels' in singleMaidr) { + if ('caption' in singleMaidr.labels) { + this.caption = singleMaidr.labels.caption; + } + } + + // set the max and min values for the plot + this.SetMaxMin(); + + // create summary and all levels + this.CreateSummaryLevel(); + this.CreateAllLevel(); + + this.autoplay = null; + } + + ParseData(data, elements = null) { + let plotData = []; + let plotElements = []; + + if (elements.length != data.length) { + plotElements = null; + } + + // create a full 2d array of data using level and fill + for (let i = 0; i < this.level.length; i++) { + for (let j = 0; j < this.fill.length; j++) { + // loop through data, find matching level and fill, assign value + // if no match, assign null + for (let k = 0; k < data.length; k++) { + // init + if (!plotData[i]) { + plotData[i] = []; + if (plotElements != null) { + if (!plotElements[i]) { + plotElements[i] = []; + } + } + } + if (!plotData[i][j]) { + plotData[i][j] = 0; + if (plotElements != null) { + if (!plotElements[i][j]) { + plotElements[i][j] = null; + } + } + } + // set actual values + if (data[k].x == this.level[i] && data[k].fill == this.fill[j]) { + plotData[i][j] = data[k].y; + plotElements[i][j] = elements[k]; + break; + } + } + } + } + + return [plotData, plotElements]; + } + + CreateSummaryLevel() { + // create another y level that is the sum of all the other levels + + for (let i = 0; i < this.plotData.length; i++) { + let sum = 0; + for (let j = 0; j < this.plotData[i].length; j++) { + sum += this.plotData[i][j]; + } + this.plotData[i].push(sum); + } + + this.fill.push('Sum'); + } + + CreateAllLevel() { + // create another y level that plays all the other levels seperately + + for (let i = 0; i < this.plotData.length; i++) { + let all = []; + for (let j = 0; j < this.fill.length; j++) { + if (this.fill[j] != 'Sum') { + all.push(this.plotData[i][j]); + } + } + this.plotData[i].push(all); + } + + this.fill.push('All'); + } + + PlayTones() { + if (Array.isArray(this.plotData[position.x][position.y])) { + // we play a run of tones + position.z = 0; + constants.sepPlayId = setInterval( + function () { + // play this tone + audio.playTone(); + + // and then set up for the next one + position.z += 1; + + // and kill if we're done + if (position.z + 1 > plot.plotData[position.x][position.y].length) { + constants.KillSepPlay(); + position.z = -1; + } + }, + constants.sonifMode == 'on' ? constants.autoPlayPointsRate : 0 + ); + } else { + audio.playTone(); + } + } + + SetMaxMin() { + for (let i = 0; i < singleMaidr.data.length; i++) { + if (i == 0) { + constants.maxY = singleMaidr.data[i].y; + constants.minY = singleMaidr.data[i].y; + } else { + if (singleMaidr.data[i].y > constants.maxY) { + constants.maxY = singleMaidr.data[i].y; + } + if (singleMaidr.data[i].y < constants.minY) { + constants.minY = singleMaidr.data[i].y; + } + } + } + constants.maxX = this.level.length; + constants.autoPlayRate = Math.min( + Math.ceil(constants.AUTOPLAY_DURATION / (constants.maxX + 1)), + constants.MAX_SPEED + ); + constants.DEFAULT_SPEED = constants.autoPlayRate; + if (constants.autoPlayRate < constants.MIN_SPEED) { + constants.MIN_SPEED = constants.autoPlayRate; + } + } + + Select() { + this.UnSelectPrevious(); + if (this.elements) { + this.activeElement = this.elements[position.x][position.y]; + if (this.activeElement) { + this.activeElementColor = this.activeElement.style.fill; + let invertedColor = constants.ColorInvert(this.activeElementColor); + this.activeElement.style.fill = invertedColor; + } + } + } + + UnSelectPrevious() { + if (this.activeElement) { + this.activeElement.style.fill = this.activeElementColor; + this.activeElement = null; + } + } +} + class Control { constructor() { this.SetControls(); diff --git a/dist/maidr.min.js b/dist/maidr.min.js index 43daf67a..420ebba3 100644 --- a/dist/maidr.min.js +++ b/dist/maidr.min.js @@ -1 +1 @@ -class Constants{chart_container_id="chart-container";main_container_id="maidr-container";braille_container_id="braille-div";braille_input_id="braille-input";info_id="info";announcement_container_id="announcements";end_chime_id="end_chime";container_id="container";project_id="maidr";review_id_container="review_container";review_id="review";reviewSaveSpot;reviewSaveBrailleMode;chartId="";events=[];postLoadEvents=[];constructor(){}textMode="verbose";brailleMode="off";sonifMode="on";reviewMode="off";minX=0;maxX=0;minY=0;maxY=0;plotId="";chartType="";navigation=1;MAX_FREQUENCY=1e3;MIN_FREQUENCY=200;NULL_FREQUENCY=100;MAX_SPEED=500;MIN_SPEED=50;DEFAULT_SPEED=250;INTERVAL=20;AUTOPLAY_DURATION=5e3;vol=.5;MAX_VOL=30;autoPlayRate=this.DEFAULT_SPEED;colorSelected="#03C809";brailleDisplayLength=32;showRect=1;hasRect=1;hasSmooth=1;duration=.3;outlierDuration=.06;autoPlayOutlierRate=50;autoPlayPointsRate=30;colorUnselected="#595959";isTracking=1;visualBraille=!1;globalMinMax=!0;showDisplay=1;showDisplayInBraille=1;showDisplayInAutoplay=0;outlierInterval=null;isMac=navigator.userAgent.toLowerCase().includes("mac");control=this.isMac?"Cmd":"Ctrl";alt=this.isMac?"option":"Alt";home=this.isMac?"fn + Left arrow":"Home";end=this.isMac?"fn + Right arrow":"End";keypressInterval=2e3;tabMovement=null;debugLevel=3;canPlayEndChime=!1;manualData=!0;KillAutoplay(){this.autoplayId&&(clearInterval(this.autoplayId),this.autoplayId=null)}KillSepPlay(){this.sepPlayId&&(clearInterval(this.sepPlayId),this.sepPlayId=null)}SpeedUp(){constants.autoPlayRate-this.INTERVAL>this.MIN_SPEED&&(constants.autoPlayRate-=this.INTERVAL)}SpeedDown(){constants.autoPlayRate+this.INTERVAL<=this.MAX_SPEED&&(constants.autoPlayRate+=this.INTERVAL)}SpeedReset(){constants.autoPlayRate=constants.DEFAULT_SPEED}ColorInvert(t){let e=t.replace(/[^\d,]/g,"").split(",");return"rgb("+(255-e[0])+","+(255-e[1])+","+(255-e[2])+")"}}class Resources{constructor(){}language="en";knowledgeLevel="basic";strings={en:{basic:{upper_outlier:"Upper Outlier",lower_outlier:"Lower Outlier",min:"Minimum",max:"Maximum",25:"25%",50:"50%",75:"75%",q1:"25%",q2:"50%",q3:"75%",son_on:"Sonification on",son_off:"Sonification off",son_des:"Sonification descrete",son_comp:"Sonification compare",son_ch:"Sonification chord",son_sep:"Sonification separate",son_same:"Sonification combined",empty:"Empty"}}};GetString(t){return this.strings[this.language][this.knowledgeLevel][t]}}class Menu{whereWasMyFocus=null;constructor(){this.CreateMenu(),this.LoadDataFromLocalStorage()}menuHtml=`\n \n \n `;CreateMenu(){document.querySelector("body").insertAdjacentHTML("beforeend",this.menuHtml);let t=document.querySelectorAll("#close_menu, #menu .close");for(let e=0;e