4 |
5 |
6 |
7 |
11 |
12 | CharaChorder Utilities
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "CharaChorder Utilities",
3 | "name": "CharaChorder Utilities",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/ChordDiagnostic.css:
--------------------------------------------------------------------------------
1 | #wrapper {
2 | display: flex;
3 | flex-direction: column;
4 | width: 100%;
5 | align-items: center;
6 | }
7 |
8 | #wrapper #visualization {
9 | flex: 1 1;
10 | min-height: 0px;
11 | min-width: 100%;
12 | }
13 |
14 | #visualization {
15 | height: 100%;
16 | text-align: left;
17 | }
18 |
19 | .vis-item.green {
20 | background-color: green;
21 | border-color: green;
22 | }
23 |
24 | .vis-item.red {
25 | background-color: red;
26 | border-color: red;
27 | }
28 |
29 | #timing-table {
30 | width: 100%;
31 | border-collapse: collapse;
32 | }
33 |
34 | #timing-table th {
35 | padding: 8px;
36 | text-align: left;
37 | font-weight: bold;
38 | }
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/functions/createCsv.js:
--------------------------------------------------------------------------------
1 | import Papa from 'papaparse';
2 |
3 | export default function createCsv(data, filename = 'data.csv', includeHeader = true) {
4 | const config = {
5 | header: includeHeader
6 | };
7 | const csv = Papa.unparse(data, config);
8 |
9 | // Create a Blob from the CSV string
10 | const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
11 |
12 | // Create a hidden anchor element
13 | const link = document.createElement('a');
14 | const url = URL.createObjectURL(blob);
15 |
16 | link.setAttribute('href', url);
17 | link.setAttribute('download', filename);
18 | link.style.visibility = 'hidden';
19 |
20 | // Append, trigger the download, and remove the anchor element
21 | document.body.appendChild(link);
22 | link.click();
23 | document.body.removeChild(link);
24 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CharaChorder Utilities
2 |
3 | Welcome to the CharaChorder Utilities project! This project contains a set of tools for CharaChorder users.
4 |
5 | The best way to check them out is just by visiting the site here: [CharaChorder Utilities](https://typing-tech.github.io/CharaChorder-utilities/).
6 |
7 | ## Contributing
8 |
9 | Clone the repo and run:
10 |
11 | ### `npm install`
12 |
13 | Then, to start the server and begin developing run:
14 |
15 | ### `npm start`
16 |
17 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
18 |
19 | The page will reload when you make changes.\
20 | You may also see any lint errors in the console.
21 |
22 | I am open to any contributions--if you have an improvement you would like to make you can make a PR, and I will review it.
23 |
24 | ## Reporting issues
25 |
26 | If you encounter any problems or have any suggestions for improvement, please open an issue.
--------------------------------------------------------------------------------
/src/components/LinearWithValueLabel.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import PropTypes from 'prop-types';
3 | import LinearProgress from '@mui/material/LinearProgress';
4 | import Typography from '@mui/material/Typography';
5 | import Box from '@mui/material/Box';
6 |
7 | function LinearProgressWithLabel(props) {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 | {`${Math.round(props.value)}%`}
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | LinearProgressWithLabel.propTypes = {
23 | value: PropTypes.number.isRequired, // Value between 0 and 100.
24 | };
25 |
26 | export default LinearProgressWithLabel;
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 CharaChorder Community Projects
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/pages/HomePage.js:
--------------------------------------------------------------------------------
1 | import { Typography } from '@mui/material';
2 | import React from 'react';
3 |
4 | function HomePage() {
5 | return (
6 |
7 | Welcome to CharaChorder Utilites!
8 |
9 | This site contains a collection of tools for CharaChorder users.
10 |
11 |
12 | Most of the chord and practice tools require you to upload your exported chord library from Dot I/O. You can do that in the top right hand corner. After you have done it once, you will only need to load it again if you have added chords.
13 |
14 |
15 | Explore each of the pages and report any issues on the GitHub Page.
16 |
17 |
18 | Disclaimer: This site is not affiliated, associated, authorized, endorsed by, or in any way officially connected with CharaChorder. The official CharaChorder website can be found at CharaChorder.com.
19 |
20 |
110 | );
111 | }
112 |
113 | ChordTools.propTypes = {
114 | chordLibrary: PropTypes.array,
115 | setChordLibrary: PropTypes.func,
116 | };
117 |
118 | export default ChordTools;
--------------------------------------------------------------------------------
/src/functions/anagramWorker.js:
--------------------------------------------------------------------------------
1 | function countWordOccurrences(textInput, word) {
2 | var words = getCleanWordsFromString(textInput);
3 | let count = 0;
4 | for (let i = 0; i < words.length; i++) {
5 | if (words[i] === word) {
6 | count++;
7 | }
8 | }
9 | return count;
10 | }
11 |
12 | function getCleanWordsFromString(inputString) {
13 | // Replace all new line and tab characters with a space character, and remove consecutive spaces
14 | const textWithSpaces = inputString.replace(/[\n\t]/g, ' ').replace(/\s+/g, ' ');
15 |
16 | // Split the text into an array of words
17 | const origWords = textWithSpaces.split(' ').map(word => word.replace(/[^\w\s]/g, ''));
18 | const words = origWords
19 | // Remove empty strings from the array
20 | .filter(word => word.trim().length > 0)
21 | // Convert all words to lower case
22 | .map(word => word.toLowerCase());
23 | return words;
24 | }
25 |
26 | function findPartialAnagrams(inputString) {
27 | const words = getCleanWordsFromString(inputString);
28 | const result = [];
29 | const uniqueWords = [...new Set(words)];
30 |
31 | // Calculate the total number of iterations (for progress calculation)
32 | const totalIterations = uniqueWords.length * (uniqueWords.length - 1);
33 | console.time("Analyzing Time");
34 | console.log("Expected total iterations:", totalIterations);
35 | let currentIteration = 0;
36 |
37 | for (let i = 0; i < uniqueWords.length; i++) {
38 | // For each word, compare it to the other words in the array
39 | for (let j = 0; j < uniqueWords.length; j++) {
40 | // Skip the current word if it is being compared to itself
41 | if (i !== j) {
42 | // Increment current iteration and calculate progress
43 | currentIteration++;
44 |
45 | const progress = (currentIteration / totalIterations) * 100;
46 | if (currentIteration % 100000 === 0) {
47 | //console.log(progress)
48 | postMessage({ type: 'progress', progress });
49 | }
50 |
51 | // Check if the words are equal
52 | if (uniqueWords[i] !== uniqueWords[j]) {
53 | // If they are not equal, check if they are partial anagrams
54 | const uniqueLetters1 = [...new Set(uniqueWords[i].split(''))];
55 | const uniqueLetters2 = [...new Set(uniqueWords[j].split(''))];
56 |
57 | // Check if each unique letter in one word appears in the other word
58 | if (uniqueLetters1.every(letter => uniqueLetters2.includes(letter)) && uniqueLetters2.every(letter => uniqueLetters1.includes(letter))) {
59 | // Check if the pair has already been added to the result array
60 | if (!result.some(pair => pair.includes(uniqueWords[i]) && pair.includes(uniqueWords[j]))) {
61 | if (result.length === 0) {
62 | result.push([uniqueWords[i], uniqueWords[j]]);
63 | } else {
64 | let found = false;
65 | for (let k = 0; k < result.length; k++) {
66 | if (result[k].includes(uniqueWords[i]) || result[k].includes(uniqueWords[j])) {
67 | if (!(result[k].includes(uniqueWords[i]))) {
68 | result[k].push(uniqueWords[i]);
69 | }
70 | if (!(result[k].includes(uniqueWords[j]))) {
71 | result[k].push(uniqueWords[j]);
72 | }
73 | found = true;
74 | break;
75 | }
76 | }
77 | if (!found) {
78 | result.push([uniqueWords[i], uniqueWords[j]]);
79 | }
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
87 | console.timeEnd("Analyzing Time");
88 | console.time("Sorting Time"); // Start the timer with label "Sorting Time"
89 | const resultWithCounts = [];
90 |
91 | for (const group of result) {
92 | const groupWithCounts = group.map(word => {
93 | return { word: word, count: countWordOccurrences(inputString, word) };
94 | });
95 | resultWithCounts.push(groupWithCounts);
96 | }
97 |
98 | // Sort the result array by the largest list of partial anagrams first
99 | resultWithCounts.sort((a, b) => b.length - a.length);
100 | console.timeEnd("Sorting Time"); // End the timer with label "Sorting Time"
101 |
102 | return resultWithCounts;
103 | }
104 |
105 | export function anagramWorkerFunction(e) {
106 | if (e.data.type === 'computeAnagrams') {
107 | postMessage({ type: 'progress', progress: 0 }); // Initial progress message
108 | const result = findPartialAnagrams(e.data.input);
109 | postMessage({ type: 'result', result }); // Final result
110 | }
111 | }
112 |
113 | // eslint-disable-next-line no-restricted-globals
114 | self.onmessage = anagramWorkerFunction;
--------------------------------------------------------------------------------
/src/components/ChordDiagnostic.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from 'react';
2 | import { Button, Container, TextField, Table, TableBody, TableCell, TableHead, TableRow, Typography, Box } from '@mui/material';
3 | import 'vis-timeline/dist/vis-timeline-graph2d.min.css';
4 | import './ChordDiagnostic.css';
5 | import { DataSet } from "vis-data/peer";
6 | import { Timeline } from "vis-timeline";
7 |
8 | const TableRowComponent = ({ event, time, startTime }) => {
9 | const relativeTime = time - startTime;
10 | const eventType = event.type === 'keydown' ? 'Press' : 'Release';
11 | return (
12 |
13 | {eventType}
14 | {event.code}
15 | {relativeTime.toFixed(2) + ' ms'}
16 |
17 | );
18 | };
19 |
20 | const ChordDiagnostic = () => {
21 | const containerRef = useRef(null);
22 | const [events, setEvents] = useState([]);
23 | const [pressMessage, setPressMessage] = useState('');
24 | const [releaseMessage, setReleaseMessage] = useState('');
25 | const [pressToReleaseMessage, setPressToReleaseMessage] = useState('');
26 | const timelineRef = useRef(null);
27 | const [textFieldValue, setTextFieldValue] = useState('');
28 | const plotTimeoutRef = useRef(null);
29 |
30 | const handleKeyEvents = (event) => {
31 | setEvents([...events, { event, time: performance.now() }]);
32 | setTextFieldValue(event.target.value);
33 |
34 | if (plotTimeoutRef.current) {
35 | clearTimeout(plotTimeoutRef.current);
36 | }
37 |
38 | const delay = 500;
39 | plotTimeoutRef.current = setTimeout(handlePlot, delay);
40 | };
41 |
42 | // Clear the timeout when the component is unmounted
43 | useEffect(() => {
44 | return () => {
45 | if (plotTimeoutRef.current) {
46 | clearTimeout(plotTimeoutRef.current);
47 | }
48 | };
49 | }, []);
50 |
51 | const handlePlot = () => {
52 | const container = containerRef.current;
53 | const initialTime = events[0].time;
54 |
55 | const items = new DataSet(events.map(({ event, time }) => {
56 | const color = event.type === 'keyup' ? 'red' : 'green';
57 | const relativeTime = time - initialTime;
58 | return {
59 | content: event.code,
60 | start: relativeTime,
61 | title: `${relativeTime.toFixed(2)} ms`,
62 | style: `color: white`,
63 | className: color
64 | };
65 | }));
66 |
67 | const options = {
68 | orientation: 'top',
69 | align: 'left',
70 | order: (a, b) => b.time - a.time,
71 | showMajorLabels: false,
72 | format: {
73 | minorLabels: (date) => date.valueOf().toString(),
74 | }
75 | };
76 |
77 | // Clearing the previous timeline
78 | container.innerHTML = '';
79 |
80 | // Creating the new timeline
81 | const timeline = new Timeline(container, items, options);
82 | timelineRef.current = timeline;
83 |
84 | // Filter the events to only include those before the first Backspace
85 | const filteredEvents = [];
86 | for (const eventObj of events) {
87 | if (eventObj.event.code === 'Backspace') break; // Stop processing after the first Backspace
88 | filteredEvents.push(eventObj);
89 | }
90 |
91 | let firstPressTime = null;
92 | let lastPressTime = null;
93 | let firstReleaseTime = null;
94 | let lastReleaseTime = null;
95 | let pressToReleaseTime = null;
96 |
97 | filteredEvents.forEach(({ event, time }) => {
98 | if (event.type === 'keydown') {
99 | if (firstPressTime === null) {
100 | firstPressTime = time;
101 | }
102 | lastPressTime = time;
103 | } else if (event.type === 'keyup') {
104 | if (firstReleaseTime === null) {
105 | firstReleaseTime = time;
106 | }
107 | lastReleaseTime = time;
108 | }
109 | });
110 |
111 | const pressDifference = lastPressTime !== null && firstPressTime !== null
112 | ? (lastPressTime - firstPressTime).toFixed(2) + ' ms'
113 | : 'N/A';
114 |
115 | const releaseDifference = lastReleaseTime !== null && firstReleaseTime !== null
116 | ? (lastReleaseTime - firstReleaseTime).toFixed(2) + ' ms'
117 | : 'N/A';
118 |
119 | if (lastPressTime !== null && firstReleaseTime !== null) {
120 | pressToReleaseTime = (firstReleaseTime - lastPressTime).toFixed(2) + ' ms';
121 | } else {
122 | pressToReleaseTime = 'N/A';
123 | }
124 |
125 | // Setting messages
126 | setPressMessage('The time between the first and last press was ' + pressDifference + '.');
127 | setReleaseMessage('The time between the first and last release was ' + releaseDifference + '.');
128 | setPressToReleaseMessage('The time between the last press and the first release was ' + pressToReleaseTime + '.');
129 | };
130 |
131 | const handleReset = () => {
132 | if (timelineRef.current) {
133 | timelineRef.current.destroy(); // Destroy the timeline instance
134 | }
135 | setEvents([]);
136 | setPressMessage('');
137 | setReleaseMessage('');
138 | setTextFieldValue('');
139 | setPressToReleaseMessage('');
140 | };
141 |
142 | return (
143 |
144 |
145 |
146 |
147 | This tool is designed to help you look at the press and release timings of a chord.
148 | To use, try to chord the word in the input box and then shortly after it will be plotted.
149 | There is also a table of the presses and releases.
150 | Credit to Tangent Chang (andy23512) from the CharaChorder Discord for this tool.
151 |
152 |
194 |
195 | );
196 | };
197 |
198 | export default ChordDiagnostic;
199 |
--------------------------------------------------------------------------------
/src/functions/wordAnalysis.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | export function calculateBigrams(text) {
4 | var words = text.split(' ');
5 | var wordCount = words.length;
6 |
7 | // Use the filter method and the indexOf method to remove any duplicates
8 | var uniqueWords = words.filter(function (word, index) {
9 | return words.indexOf(word) === index;
10 | });
11 |
12 | // Create a list of bigrams by iterating over each word in the array
13 | var bigrams = [];
14 | for (var i = 0; i < words.length; i++) {
15 | // Remove any leading or trailing spaces from the word
16 | var word = words[i].trim();
17 |
18 | // Skip over any empty words (which can happen if the text has multiple consecutive spaces)
19 | if (word.length >= 2) {
20 | // Check for line breaks in the word
21 | if (word.indexOf('\n') === -1) {
22 | for (var j = 0; j < word.length - 1; j++) {
23 | var lowered = word.toLowerCase()
24 | var bigram = lowered.substring(j, j + 2);
25 | const [letter1, letter2] = bigram.split('');
26 | if (!/^[a-z]$/.test(letter1) || !/^[a-z]$/.test(letter2)) {
27 | // If either letter is not a lowercase letter, skip this iteration
28 | continue;
29 | }
30 | // sort the characters in the bigram so that permutations are counted together
31 | bigram = bigram.split('').sort().join('');
32 | var reversedbigram = bigram.split('').reverse().join('');
33 | bigram = bigram + " | " + reversedbigram;
34 | bigrams.push(bigram);
35 | }
36 | }
37 | }
38 | }
39 |
40 | // create an object to hold the frequency of each bigram
41 | var bigramCounts = {};
42 | for (let i = 0; i < bigrams.length; i++) {
43 | let bigram = bigrams[i];
44 | if (bigram in bigramCounts) {
45 | bigramCounts[bigram]++;
46 | } else {
47 | bigramCounts[bigram] = 1;
48 | }
49 | }
50 |
51 | return [bigramCounts, wordCount, uniqueWords.length];
52 | }
53 |
54 | export function createFrequencyTable(bigramCounts) {
55 | // Create a matrix of zeroes with 26 rows and 26 columns
56 | // (assuming you only have lowercase letters in your input)
57 | const matrix = Array(26)
58 | .fill()
59 | .map(() => Array(26).fill(0));
60 |
61 | const normalizedMatrix = Array(26)
62 | .fill()
63 | .map(() => Array(26).fill(0));
64 |
65 | // Loop through each key-value pair in the input object
66 | for (const [key, value] of Object.entries(bigramCounts)) {
67 | // Split the key into two letters
68 | const [letter1, letter2] = key.split(" ")[0].split("");
69 |
70 | // Check if either letter1 or letter2 is not a lowercase letter
71 | if (!/^[a-z]$/.test(letter1) || !/^[a-z]$/.test(letter2)) {
72 | // If either letter is not a lowercase letter, skip this iteration
73 | continue;
74 | }
75 |
76 | // Convert the letters to their ASCII codes (a = 97, b = 98, etc.)
77 | const ascii1 = letter1.charCodeAt(0) - 97;
78 | const ascii2 = letter2.charCodeAt(0) - 97;
79 |
80 | matrix[ascii1][ascii2] = value;
81 | matrix[ascii2][ascii1] = value;
82 |
83 | // Normalize the value to be in the range 0-255
84 | const normalizedValue = (value / Math.max(...Object.values(bigramCounts))) * 255
85 |
86 | // Update the matrix with the value from the input object
87 | normalizedMatrix[ascii1][ascii2] = normalizedValue;
88 | normalizedMatrix[ascii2][ascii1] = normalizedValue; // (assuming you want the matrix to be symmetrical)
89 | }
90 | return [matrix, normalizedMatrix]
91 | }
92 |
93 | async function calcStats(text, chords, minReps, lemmatizeChecked) {
94 | const counts = await findUniqueWords(chords, text, lemmatizeChecked);
95 | return [counts.sortedWords, counts.uniqueWordCount, counts.chordWordCount];
96 | }
97 |
98 | export function findMissingChords(textToAnalyze, chordLibrary) {
99 | const chords = new Set();
100 | chordLibrary.forEach(({ chordOutput }) => chordOutput && chords.add(chordOutput));
101 | return calcStats(textToAnalyze, chords, 5, false);
102 | }
103 |
104 | async function processWords(words, lemmatize) {
105 | for (let i = 0; i < words.length; i++) {
106 | words[i] = words[i].toLowerCase().replace(/[^\w\s]/g, '');
107 | if (lemmatize) {
108 | // const doc = nlp(words[i])
109 | // doc.verbs().toInfinitive()
110 | // doc.nouns().toSingular()
111 | // const lemma = doc.out('text')
112 | // words[i] = lemma;
113 | }
114 | }
115 | return words;
116 | }
117 |
118 | async function findUniqueWords(chords, text, lemmatize) {
119 | // Split the text into an array of words
120 | var words = text.split(/\s+/);
121 | words = await processWords(words, lemmatize);
122 |
123 | var wordCounts = {};
124 | var uniqueWordCount = 0;
125 | var chordWordCount = 0;
126 | var countedChords = {};
127 |
128 | // Count the number of times each word appears in the text
129 | for (var i = 0; i < words.length; i++) {
130 | var word = words[i].trim();
131 | if (word === "") {
132 | continue;
133 | }
134 | if (word.length > 1) {
135 | if (!(word in wordCounts)) {
136 | wordCounts[word] = 1;
137 | uniqueWordCount++;
138 | } else {
139 | wordCounts[word]++;
140 | }
141 | }
142 | }
143 |
144 | // Create a dictionary of words that do not appear in the chords set
145 | var sortedWords = {};
146 | for (let i = 0; i < words.length; i++) {
147 | let word = words[i].trim();
148 | if (word === "") {
149 | continue;
150 | }
151 | if (word.length > 1) {
152 | if (!chords.has(word)) {
153 | if (!(word in sortedWords)) {
154 | sortedWords[word] = 1;
155 | } else {
156 | sortedWords[word]++;
157 | }
158 | } else {
159 | if (!(word in countedChords)) {
160 | countedChords[word] = true;
161 | chordWordCount++;
162 | }
163 | }
164 | }
165 | }
166 |
167 | var descSortedWords = Object.entries(sortedWords).sort((a, b) => b[1]*b[0].length - a[1]*a[0].length);
168 | return {
169 | sortedWords: descSortedWords,
170 | uniqueWordCount: uniqueWordCount,
171 | chordWordCount: chordWordCount
172 | };
173 | }
174 |
175 | export function getPhraseFrequency(text, phraseLength, minRepetitions, chordLibrary) {
176 | const chords = new Set();
177 | chordLibrary.forEach(({ chordOutput }) => chordOutput && chords.add(chordOutput));
178 |
179 | // Replace all new line and tab characters with a space character, and remove consecutive spaces
180 | const textWithSpaces = text.replace(/[\n\t]/g, ' ').replace(/\s+/g, ' ');
181 |
182 | // Split the text into an array of words
183 | const origWords = textWithSpaces.split(' ').map(word => word.replace(/[^\w\s]/g, ''));
184 | const words = origWords
185 | // Remove empty strings from the array
186 | .filter(word => word.trim().length > 0)
187 | // Convert all words to lower case
188 | .map(word => word.toLowerCase());
189 |
190 |
191 | // Create a dictionary to store the phrases and their frequency
192 | const phraseFrequency = {};
193 |
194 | // Iterate over the words array and add each phrase to the dictionary
195 | for (let i = 0; i < words.length; i++) {
196 | for (let j = 2; j <= phraseLength; j++) {
197 | // Get the current phrase by joining the next `j` words with a space character
198 | const phrase = words.slice(i, i + j).join(' ');
199 | // Check if the phrase fits within the bounds of the `words` array
200 | if (i + j <= words.length) {
201 | // Split the phrase into a list of words
202 | const phraseWords = phrase.split(' ');
203 | // Check if the phrase contains at least two words
204 | if (phraseWords.length >= 2) {
205 | // If the phrase is already in the dictionary, increment its frequency. Otherwise, add it to the dictionary with a frequency of 1.
206 | if (phrase in phraseFrequency) {
207 | phraseFrequency[phrase]++;
208 | } else {
209 | phraseFrequency[phrase] = 1;
210 | }
211 | }
212 | }
213 | }
214 | }
215 |
216 | // Filter the sorted phrase frequency dictionary by the minimum number of repetitions
217 | const filteredPhraseFrequency = {};
218 | Object.keys(phraseFrequency).forEach(phrase => {
219 | if (phraseFrequency[phrase] >= minRepetitions) {
220 | filteredPhraseFrequency[phrase] = phraseFrequency[phrase];
221 | }
222 | });
223 |
224 | // Remove entries from the filtered phrase frequency object that are already in the chords set
225 | const lowerCaseChords = new Set(Array.from(chords).map(phrase => phrase.trim().toLowerCase()));
226 | const filteredPhraseFrequencyWithoutChords = Object.keys(filteredPhraseFrequency)
227 | .filter(phrase => !lowerCaseChords.has(phrase))
228 | .reduce((obj, phrase) => {
229 | obj[phrase] = filteredPhraseFrequency[phrase];
230 | return obj;
231 | }, {});
232 |
233 | let sortableArray = Object.entries(filteredPhraseFrequencyWithoutChords);
234 | sortableArray.sort((a, b) => {
235 | const scoreA = a[0].length * a[1];
236 | const scoreB = b[0].length * b[1];
237 | return scoreB - scoreA;
238 | });
239 |
240 | return sortableArray;
241 | }
--------------------------------------------------------------------------------
/src/pages/CCXDebugging.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Typography, Button, TextField } from '@mui/material';
3 | import List from '@mui/material/List';
4 | import ListItem from '@mui/material/ListItem';
5 | import { Snackbar, Alert } from '@mui/material';
6 |
7 | function CCX() {
8 | const [port, setPort] = useState(null);
9 | const [writer, setWriter] = useState(null);
10 | const [isTesting, setIsTesting] = useState(false);
11 | const [receivedData, setReceivedData] = useState("");
12 | const [openSnackbar, setOpenSnackbar] = useState(false);
13 | const [supportsSerial, setSupportsSerial] = useState(null);
14 |
15 | const handleOpenSnackbar = () => {
16 | setOpenSnackbar(true);
17 | };
18 |
19 | const handleCloseSnackbar = () => {
20 | setOpenSnackbar(false);
21 | };
22 |
23 | useEffect(() => {
24 | if (port !== null) {
25 | listenToPort();
26 | }
27 | }, [port]);
28 |
29 | useEffect(() => {
30 | if ('serial' in navigator) {
31 | setSupportsSerial(true)
32 | }
33 | else {
34 | setSupportsSerial(false)
35 | }
36 | })
37 |
38 | const connectDevice = async () => {
39 | try {
40 | const newPort = await navigator.serial.requestPort();
41 | await newPort.open({ baudRate: 115200 });
42 |
43 | const textEncoder = new TextEncoderStream();
44 | const writableStreamClosed = textEncoder.readable.pipeTo(newPort.writable);
45 | const newWriter = textEncoder.writable.getWriter();
46 |
47 | setWriter(newWriter);
48 | setPort(newPort);
49 | } catch {
50 | alert("Serial Connection Failed");
51 | }
52 | };
53 |
54 | const listenToPort = async () => {
55 | if (port === null) return;
56 |
57 | const textDecoder = new TextDecoderStream();
58 | const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
59 | const reader = textDecoder.readable.getReader();
60 |
61 | while (true) {
62 | const { value, done } = await reader.read();
63 | console.log(value);
64 |
65 | if (done) {
66 | reader.releaseLock();
67 | break;
68 | }
69 | setReceivedData((prev) => prev + value);
70 | }
71 | };
72 |
73 | const toggleTest = async () => {
74 | if (!isTesting) {
75 | if (writer) {
76 | await writer.write("VERSION\r\n");
77 | await writer.write("VAR B2 C1 1\r\n");
78 | }
79 | setIsTesting(true);
80 | } else {
81 | if (writer) {
82 | await writer.write("VAR B2 C1 0\r\n");
83 | }
84 | setIsTesting(false);
85 | copyToClipboard()
86 | }
87 | };
88 |
89 | const copyToClipboard = () => {
90 | if (navigator.clipboard) {
91 | navigator.clipboard.writeText(receivedData);
92 | } else {
93 | // Fallback for older browsers
94 | const textArea = document.createElement("textarea");
95 | textArea.value = receivedData;
96 | document.body.appendChild(textArea);
97 | textArea.focus();
98 | textArea.select();
99 | document.execCommand('copy');
100 | document.body.removeChild(textArea);
101 | }
102 | handleOpenSnackbar();
103 | };
104 |
105 | return (
106 |
107 | CharaChorder X Debugging
108 | {supportsSerial === null ? (
109 | "Checking for Serial API support..."
110 | ) : supportsSerial ? (
111 | <>
112 |
113 | In order to help the CharaChorder team debug why your
114 | keyboard may not be working with the CCX, please perform the following
115 | steps and then copy and send the output once you are done.
116 | Read all instructions first before starting with step 1.
117 |
118 |
119 |
120 | 0: Make sure your CCX is updated to CCOS 1.1.3 or a later version. Click here for instructions on how to update your device.
121 |
122 |
123 | 1: Unplug your keyboard from the CCX and make sure
124 | the CCX is plugged into your computer.
125 |
126 |
127 | 2: Click "Connect" and use Chrome's serial connection to
128 | choose your CCX. Click "Start Test" (you should see a
129 | "VERSION" and "VAR") line appear in the Serial Data box.
130 |
131 |
132 | 3: Plug in your keyboard to the CCX and wait a
133 | couple of seconds before moving to the next step.
134 |
135 | 4: Press and release the letter "a".
136 |
137 | 5: Next, press and hold the letter "a", press and hold the letter "s", and keep
138 | adding one letter at a time until you are pressing and holding all keys in "asdfjkl;" and
139 | then release all at once. Make sure to press the keys in order.
140 |
141 | 6: Press and release the "Left Shift" key.
142 |
143 | 7: Press the "Left Shift" key, press "Left Alt" key,
144 | and then release both.
145 |
146 |
147 | 8: Press the "a" key, press "Left Shift" key, and
148 | then release both.
149 |
150 |
151 | 9: Click Stop Test. The results are copied to your clipboard. You can
152 | send this to your CharaChorder support rep.
153 |
154 |
155 |
156 |
159 | Serial Data:
160 |
168 |
174 |
175 | Copied to clipboard!
176 |
177 |
178 | What is this test doing?
179 |
180 | When keyboard manufacturers make a keyboard that is plug and play,
181 | they have a few formatting options when it comes time to actually send what
182 | you, the user, type on your keyboard to your computer. There
183 | is a standard format that most keyboards use and the
184 | CCX works out of the box with that one (boot protocol); however,
185 | there are others that may be non-standard and when this
186 | happens, your CCX may not work properly.
187 | This test turns on a debugging command and then records
188 | the outputs from your device as you press various keys.
189 | The codes you see in the box are the codes that your
190 | keyboard is sending to the CharaChorder X and then the
191 | CharaChorder X has to interpet what it is, do its own
192 | processing/magic of chording and then send on to the
193 | computer the right text. If your keyboard isn't sending,
194 | for example, "KYBRPT 00 0000040000000000" for the
195 | letter "a", this test will let CharaChorder support see what it IS sending
196 | and then they may be able to infer/make a game plan to support
197 | what your keyboard is sending.
198 |
199 | >
200 | ) : (
201 |
202 | Your browser does not support the Serial API. Please use
203 | Google Chrome, Microsoft Edge, or another browser
204 | that supports the Serial API in order to use this tool.
205 |
206 | )}
207 |
208 | );
209 | }
210 |
211 | export default CCX;
--------------------------------------------------------------------------------
/src/components/ChordStatistics.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
3 | import { Snackbar, Alert } from '@mui/material';
4 |
5 | const ChordStatistics = ({ chordLibrary }) => {
6 | const [chordStats, setChordStats] = useState({});
7 | const [letterCounts, setLetterCounts] = useState([]);
8 | const [dupWords, setDupWords] = useState([]);
9 | const [uniqueChordOutputs, setUniqueChordOutputs] = useState(new Set());
10 | const canvasRef = React.useRef(null);
11 | const [openSnackbar, setOpenSnackbar] = useState(false);
12 |
13 | const handleOpenSnackbar = () => {
14 | setOpenSnackbar(true);
15 | };
16 |
17 | const handleCloseSnackbar = () => {
18 | setOpenSnackbar(false);
19 | };
20 |
21 | useEffect(() => {
22 | const calculateStatistics = () => {
23 | const lengthCounts = {};
24 | const chordMapCounts = {};
25 | const lettersCounts = {};
26 | const newDupWords = [];
27 | const uniqueChordOutputs = new Set();
28 |
29 | // Adapted to work with array of objects
30 | chordLibrary.forEach(({ chordInput, chordOutput }) => {
31 | let chordMap = chordInput.replace(/[\s+]+/g, ' ');
32 | const chordMapLength = chordMap.split(' ').length;
33 |
34 | lengthCounts[chordMapLength] = (lengthCounts[chordMapLength] || 0) + 1;
35 | uniqueChordOutputs.add(chordOutput);
36 | const chord = chordOutput;
37 |
38 | chordMapCounts[chord] = (chordMapCounts[chord] || 0) + 1;
39 |
40 | if (chordMapCounts[chord] > 1) newDupWords.push(chord);
41 |
42 | const letters = chordMap.split(' ');
43 | letters.forEach(letter => {
44 | lettersCounts[letter] = (lettersCounts[letter] || 0) + 1;
45 | });
46 | });
47 |
48 | setUniqueChordOutputs(uniqueChordOutputs);
49 | setChordStats(lengthCounts);
50 | setLetterCounts(Object.entries(lettersCounts).sort((a, b) => b[1] - a[1]));
51 | setDupWords(newDupWords);
52 | };
53 |
54 | calculateStatistics();
55 | }, [chordLibrary]);
56 |
57 | useEffect(() => {
58 | const currentCanvasRef = canvasRef.current;
59 | if (!currentCanvasRef) return;
60 |
61 | const generateBannerContent = (numChords, numUniqueChords, lengthCounts) => {
62 | let xAxis = [];
63 | let counts = [];
64 | for (var length in lengthCounts) {
65 | xAxis.push(length);
66 | counts.push(lengthCounts[length]);
67 | }
68 |
69 | // Set the canvas size
70 | var canvas = document.createElement("canvas");
71 | canvas.width = 250;
72 | canvas.height = 125;
73 |
74 | // Get the canvas context
75 | var ctx = canvas.getContext("2d");
76 |
77 | // Set the font and text baseline
78 | ctx.font = "16px Georgia";
79 | ctx.textBaseline = "top";
80 |
81 | // To change the color on the rectangle, just manipulate the context
82 | ctx.strokeStyle = "rgb(255, 255, 255)";
83 | ctx.fillStyle = "rgb(0, 0, 0)";
84 | ctx.beginPath();
85 | ctx.roundRect(3, 3, canvas.width - 5, canvas.height - 5, 10);
86 | ctx.stroke();
87 | ctx.fill();
88 |
89 | ctx.beginPath();
90 | // Set the fill color to white
91 | ctx.fillStyle = "#FFFFFF";
92 |
93 | // Draw the text on the canvas
94 | ctx.fillText("Number of chords: " + numChords, 10, 10);
95 | ctx.fillText("Number of unique words: " + numUniqueChords, 10, 30);
96 |
97 | // Set the font for the label text
98 | ctx.font = "12px Georgia";
99 |
100 | // Measure the label text
101 | var labelText = "Generated with CharaChorder-utilities";
102 | var labelWidth = ctx.measureText(labelText).width;
103 |
104 | ctx.fillStyle = "#666";
105 |
106 | // Draw the label text at the bottom right corner of the canvas
107 | ctx.fillText(labelText, canvas.width - labelWidth - 10, canvas.height - 20);
108 |
109 | // Set the chart area width and height
110 | const chartWidth = 125;
111 | const chartHeight = 25;
112 | const labelHeight = 10; // height of the label area below the chart
113 | const columnSpacing = 2; // space between columns
114 |
115 | // Calculate the maximum count value
116 | const maxCount = Math.max(...counts);
117 |
118 | // Calculate the column width based on the number of columns and column spacing
119 | const columnWidth = (chartWidth - (counts.length - 1) * columnSpacing) / counts.length;
120 |
121 | // Set the starting x and y positions for the columns
122 | let xPos = 100;
123 | let yPos = canvas.height - 50;
124 |
125 | ctx.font = "12px monospace";
126 | ctx.fillStyle = "white";
127 | ctx.textAlign = "left";
128 | ctx.textBaseline = "top";
129 | ctx.fillText("Chord length", 5, yPos + labelHeight / 2);
130 | ctx.textAlign = "center";
131 | // Iterate through the counts and draw the columns
132 | for (let i = 0; i < counts.length; i++) {
133 | // Calculate the column height based on the count value and the maximum count
134 | const columnHeight = (counts[i] / maxCount) * chartHeight;
135 |
136 | // Draw the column
137 | ctx.fillRect(xPos, yPos - columnHeight, columnWidth, columnHeight);
138 |
139 | // Draw the label below the column
140 | ctx.fillText(xAxis[i], xPos + columnWidth / 2, yPos + labelHeight / 2);
141 |
142 | // Increment the x position for the next column
143 | xPos += columnWidth;
144 | }
145 |
146 | return canvas;
147 | }
148 |
149 | // Function to handle clipboard operations
150 | const copyToClipboard = () => {
151 | const canvas = generateBannerContent(chordLibrary.length, uniqueChordOutputs.size, chordStats);
152 | canvas.toBlob((blob) => {
153 | const item = new ClipboardItem({ "image/png": blob });
154 | navigator.clipboard.write([item]).then(() => {
155 | handleOpenSnackbar(); // Open Snackbar on successful clipboard write
156 | });
157 | });
158 | };
159 |
160 | // Function to draw on canvas
161 | const drawOnCanvas = () => {
162 | const canvas = generateBannerContent(chordLibrary.length, uniqueChordOutputs.size, chordStats);
163 | const ctx = currentCanvasRef.getContext('2d');
164 | ctx.clearRect(0, 0, currentCanvasRef.width, currentCanvasRef.height);
165 | ctx.drawImage(canvas, 0, 0);
166 | };
167 |
168 | // Attach click event for clipboard operations
169 | currentCanvasRef.addEventListener("click", copyToClipboard);
170 |
171 | // Draw on canvas
172 | drawOnCanvas();
173 |
174 | // Cleanup
175 | return () => {
176 | currentCanvasRef.removeEventListener("click", copyToClipboard);
177 | };
178 | }, [chordLibrary, uniqueChordOutputs, chordStats]);
179 |
180 | return (
181 |
182 | Click image to copy to clipboard
183 |
184 | Duplicate Words ({dupWords.length}): {dupWords.join(', ')}
185 |
186 |
187 |
136 |
137 | Either upload a .csv file that has words sorted by importance in the first column (such as a CharaChorder Nexus export),
138 | type words separated by commas, or choose from a predetermined set of word lists. Note: using the word lists can take
139 | quite a bit of time, so you may have to wait a few minutes.
140 |
141 |
142 |
146 | {csvWords.length > 0 && (
147 | <>
148 | {csvWords.length} words loaded. How many do you want to generate chords for?
149 | setNumRowsToUse(Number(e.target.value))}
154 | InputProps={{
155 | inputProps: {
156 | min: 0,
157 | step: 50,
158 | max: csvWords.length
159 | }
160 | }}
161 | />
162 |
165 | >
166 | )}
167 |
168 |
169 | {csvWords.length === 0 && (
170 | <>
171 |
172 | - or -
173 |
174 |
184 |
185 |
186 |
187 |
188 | )
189 | }}
190 | />
191 |
203 | >
204 | )}
205 |
206 |
207 |
208 |
209 | Select number of keys for the chord inputs
210 |
218 | Selected range: {sliderValue[0]} - {sliderValue[1]}
219 |