Skip to content

Commit 7bc4919

Browse files
decouple logic and display (#287)
* decouple logic and display * minimize diff * s/digram/bigram * kill the magic in <collapsable-table> * no more human text in the code * <stats-table> rather than <collapsable-table>
1 parent a5587a0 commit 7bc4919

File tree

9 files changed

+292
-314
lines changed

9 files changed

+292
-314
lines changed

code/layout-analyzer.js

+65-242
Large diffs are not rendered by default.

code/collapsable-table.js code/stats-table.js

+15-34
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
class CollapsableTable extends HTMLElement {
1+
class StatsTable extends HTMLElement {
22
maxLinesCollapsed = 12;
33

44
// Elements built in constructor
@@ -19,14 +19,6 @@ class CollapsableTable extends HTMLElement {
1919
// Actually build the content of the element (+ remove the stupid tr)
2020
shadow.innerHTML = `
2121
<style>
22-
/* Mostly copy-pasted from '/css/heatmap.css', with some ajustments */
23-
h3 { border-bottom: 1px dotted; }
24-
25-
#header {
26-
text-align: right;
27-
margin-top: -1em;
28-
}
29-
3022
#wrapper {
3123
margin-bottom: 1em;
3224
display: flex;
@@ -48,37 +40,25 @@ class CollapsableTable extends HTMLElement {
4840
td:nth-child(2) { width: 4em; text-align: right; }
4941
5042
button {
51-
width: 30%;
43+
width: 20%;
44+
height: 1.5em;
5245
margin: auto;
5346
background-color: #88fa;
54-
border: 1px solid black;
55-
border-radius: 15px;
47+
cursor: pointer;
48+
clip-path: polygon(50% 100%, 0% 0%, 100% 0%);
49+
}
50+
button.showLess {
51+
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
5652
}
5753
</style>
5854
59-
<h3> ${this.id} </h3>
6055
<!-- Using a style attribute on top of the stylesheet, as it is used by
6156
the button 'click' event-listner -->
6257
<div id='wrapper' style='max-height: ${this.maxHeightCollapsed}px;'></div>
63-
<button style='display: none'> show more </button>
58+
<button style='display: none'></button>
6459
`;
6560

66-
// If we find a 'small' element, then move it in a '#header' div instead of
67-
// the '#wrapper' div. A 'slot' is probably better, but I can’t make it work
68-
const smallElement = this.querySelector('small');
6961
const wrapper = shadow.getElementById('wrapper');
70-
if (smallElement) {
71-
// Placing the 'small' element in a wrapper div, otherwise the 'text-align'
72-
// and 'margin-top' css properties don’t do anything.
73-
const smallElementWrapper = document.createElement('div');
74-
smallElementWrapper.id = 'header';
75-
smallElementWrapper.appendChild(smallElement.cloneNode(true));
76-
77-
shadow.insertBefore(smallElementWrapper, wrapper);
78-
// Remove the 'small' element from this.innerHTML, before moving that to shadow
79-
smallElement.remove();
80-
}
81-
8262
wrapper.innerHTML = this.innerHTML;
8363
this.innerHTML = ''; // Remove original content
8464

@@ -89,19 +69,19 @@ class CollapsableTable extends HTMLElement {
8969
const wrapper = shadow.getElementById('wrapper');
9070
if (wrapper.style.maxHeight == `${self.maxHeightCollapsed}px`) {
9171
wrapper.style.maxHeight = `${wrapper.children[0].offsetHeight}px`;
92-
this.innerText = 'show less';
72+
this.className = 'showLess';
9373
} else {
9474
wrapper.style.maxHeight = `${self.maxHeightCollapsed}px`;
95-
this.innerText = 'show more';
75+
this.className = '';
9676
}
9777
});
9878
}
9979

100-
updateTableData(tableSelector, title, values, precision) {
80+
updateTableData(tableSelector, values, precision) {
10181
const table = this.shadowRoot.querySelector(tableSelector);
10282

10383
table.innerHTML =
104-
`<tr><th colspan='2'>${title}</td></tr>` +
84+
`<tr><th colspan='2'>${table.title}</td></tr>` +
10585
Object.entries(values)
10686
.filter(([digram, freq]) => freq >= 10 ** -precision)
10787
.sort(([_, freq1], [__, freq2]) => freq2 - freq1)
@@ -115,4 +95,5 @@ class CollapsableTable extends HTMLElement {
11595
table.offsetHeight > this.maxHeightCollapsed ? 'block' : 'none';
11696
}
11797
}
118-
customElements.define('collapsable-table', CollapsableTable);
98+
99+
customElements.define('stats-table', StatsTable);

code/stats.js

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { getSupportedChars, analyzeKeyboardLayout } from './layout-analyzer.js';
2+
3+
window.addEventListener('DOMContentLoaded', () => {
4+
const inputField = document.querySelector('input');
5+
const keyboard = document.querySelector('x-keyboard');
6+
7+
const headingColor = getComputedStyle(document.querySelector('h1')).color;
8+
9+
let corpusName = '';
10+
let corpus = {};
11+
let keyChars = {};
12+
let impreciseData = false;
13+
14+
// display a percentage value
15+
const fmtPercent = (num, p) => `${Math.round(10 ** p * num) / 10 ** p}%`;
16+
const showPercent = (sel, num, precision) => {
17+
document.querySelector(sel).innerText = fmtPercent(num, precision);
18+
};
19+
const showPercentAll = (sel, nums, precision) => {
20+
document.querySelector(sel).innerText =
21+
nums.map(value => fmtPercent(value, precision)).join(' / ');
22+
};
23+
24+
const showNGrams = (ngrams) => {
25+
const sum = dict => Object.entries(dict).reduce((acc, [_, e]) => acc + e, 0);
26+
27+
showPercent('#sfu-total', sum(ngrams.sfb), 2);
28+
showPercent('#sku-total', sum(ngrams.skb), 2);
29+
30+
showPercent('#sfu-all', sum(ngrams.sfb), 2);
31+
showPercent('#extensions-all', sum(ngrams.lsb), 2);
32+
showPercent('#scissors-all', sum(ngrams.scissor), 2);
33+
34+
showPercent('#inward-all', sum(ngrams.inwardRoll), 1);
35+
showPercent('#outward-all', sum(ngrams.outwardRoll), 1);
36+
showPercent('#sku-all', sum(ngrams.skb), 2);
37+
38+
showPercent('#sks-all', sum(ngrams.sks), 1);
39+
showPercent('#sfs-all', sum(ngrams.sfs), 1);
40+
showPercent('#redirect-all', sum(ngrams.redirect), 1);
41+
showPercent('#bad-redirect-all', sum(ngrams.badRedirect), 2);
42+
43+
const achoppements = document.querySelector('#achoppements stats-table');
44+
achoppements.updateTableData('#sfu-bigrams', ngrams.sfb, 2);
45+
achoppements.updateTableData('#extended-rolls', ngrams.lsb, 2);
46+
achoppements.updateTableData('#scissors', ngrams.scissor, 2);
47+
48+
const bigrammes = document.querySelector('#bigrammes stats-table');
49+
bigrammes.updateTableData('#sku-bigrams', ngrams.skb, 2);
50+
bigrammes.updateTableData('#inward', ngrams.inwardRoll, 2);
51+
bigrammes.updateTableData('#outward', ngrams.outwardRoll, 2);
52+
53+
const trigrammes = document.querySelector('#trigrammes stats-table');
54+
trigrammes.updateTableData('#sks', ngrams.sks, 2);
55+
trigrammes.updateTableData('#sfs', ngrams.sfs, 2);
56+
trigrammes.updateTableData('#redirect', ngrams.redirect, 2);
57+
trigrammes.updateTableData('#bad-redirect', ngrams.badRedirect, 2);
58+
};
59+
60+
const showReport = () => {
61+
const report = analyzeKeyboardLayout(keyboard, corpus, keyChars, headingColor);
62+
63+
document.querySelector('#sfu stats-canvas').renderData({
64+
values: report.totalSfuSkuPerFinger,
65+
maxValue: 4,
66+
precision: 2,
67+
flipVertically: true,
68+
detailedValues: true,
69+
});
70+
71+
document.querySelector('#load stats-canvas').renderData({
72+
values: report.loadGroups,
73+
maxValue: 25,
74+
precision: 1
75+
});
76+
77+
const sumUpBar = bar => bar.good + bar.meh + bar.bad;
78+
const sumUpBarGroup = group => group.reduce((acc, bar) => acc + sumUpBar(bar), 0);
79+
80+
showPercentAll('#load small', report.loadGroups.map(sumUpBarGroup), 1);
81+
showPercent('#unsupported-all', report.totalUnsupportedChars, 3);
82+
83+
document.querySelector('#imprecise-data').style.display
84+
= report.impreciseData ? 'block' : 'none';
85+
86+
document
87+
.querySelector('#achoppements stats-table')
88+
.updateTableData('#unsupported', report.unsupportedChars, 3);
89+
90+
showNGrams(report.ngrams);
91+
};
92+
93+
// keyboard state: these <select> element IDs match the x-keyboard properties
94+
// -- but the `layout` property requires a JSON fetch
95+
const IDs = ['layout', 'geometry', 'corpus'];
96+
const setProp = (key, value) => {
97+
if (key === 'layout') {
98+
if (value) {
99+
const layoutFolder = document
100+
.querySelector(`#layout option[value="${value}"]`).dataset.folder;
101+
fetch(`../keymaps/${layoutFolder}/${value}.json`)
102+
.then(response => response.json())
103+
.then(data => {
104+
const selectedOption = document
105+
.querySelector('#layout option:checked')
106+
.textContent.trim() || value;
107+
inputField.placeholder = `zone de saisie ${selectedOption}`;
108+
keyboard.setKeyboardLayout(
109+
data.keymap,
110+
data.deadkeys,
111+
data.geometry.replace('ergo', 'iso'),
112+
);
113+
data.keymap.Enter = ['\r', '\n'];
114+
keyChars = getSupportedChars(data.keymap, data.deadkeys);
115+
showReport();
116+
});
117+
} else {
118+
keyboard.setKeyboardLayout();
119+
keyChars = {};
120+
inputField.placeholder = 'select a keyboard layout';
121+
}
122+
} else if (key === 'corpus') {
123+
if (value && value !== corpusName) {
124+
fetch(`../corpus/${value}.json`)
125+
.then(response => response.json())
126+
.then(data => {
127+
corpus = data;
128+
showReport();
129+
});
130+
corpusName = value;
131+
}
132+
} else {
133+
keyboard[key] = value;
134+
}
135+
document.getElementById(key).value = value;
136+
};
137+
138+
// store the keyboard state in the URL hash like it's 1995 again! :-)
139+
const state = {};
140+
const updateHashState = (key, value) => {
141+
state[key] = value;
142+
window.location.hash = '/' +
143+
IDs.map(prop => state[prop]).join('/').replace(/\/+$/, '');
144+
};
145+
const applyHashState = () => {
146+
const hash = window.location.hash || '/ergol//en+fr';
147+
const hashState = hash.split('/').slice(1);
148+
IDs.forEach((key, i) => {
149+
setProp(key, hashState[i] || '');
150+
state[key] = hashState[i] || '';
151+
});
152+
};
153+
IDs.forEach(key => {
154+
document
155+
.getElementById(key)
156+
.addEventListener('change', event => {
157+
updateHashState(key, event.target.value);
158+
});
159+
});
160+
window.addEventListener('hashchange', applyHashState);
161+
applyHashState();
162+
});

corpus/chardict.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
#!/bin/env python3
2-
""" Turn corpus texts into dictionaries of symbols and digrams. """
1+
#!/usr/bin/env python3
2+
""" Turn corpus texts into dictionaries of symbols, bigrams and trigrams. """
33

44
import json
55
from os import path, listdir
@@ -9,10 +9,10 @@
99

1010

1111
def parse_corpus(file_path):
12-
""" Count symbols and digrams in a text file. """
12+
""" Count symbols and bigrams in a text file. """
1313

1414
symbols = {}
15-
digrams = {}
15+
bigrams = {}
1616
trigrams = {}
1717
char_count = 0
1818
prev_symbol = None
@@ -28,12 +28,12 @@ def parse_corpus(file_path):
2828
symbols[symbol] = 0
2929
symbols[symbol] += 1
3030
if prev_symbol is not None:
31-
digram = prev_symbol + symbol
32-
if digram not in digrams:
33-
digrams[digram] = 0
34-
digrams[digram] += 1
31+
bigram = prev_symbol + symbol
32+
if bigram not in bigrams:
33+
bigrams[bigram] = 0
34+
bigrams[bigram] += 1
3535
if prev_prev_symbol is not None:
36-
trigram = prev_prev_symbol + digram
36+
trigram = prev_prev_symbol + bigram
3737
if trigram not in trigrams:
3838
trigrams[trigram] = 0
3939
trigrams[trigram] += 1
@@ -55,7 +55,7 @@ def sort_by_frequency(table, precision=3):
5555
results = {}
5656
results["corpus"] = file_path
5757
results["symbols"] = sort_by_frequency(symbols)
58-
results["digrams"] = sort_by_frequency(digrams, 4)
58+
results["bigrams"] = sort_by_frequency(bigrams, 4)
5959
results["trigrams"] = sort_by_frequency(trigrams)
6060
return results
6161

corpus/en+fr.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"æ": 0.001,
6565
"í": 0.001
6666
},
67-
"digrams": {
67+
"bigrams": {
6868
"th": 1.7253,
6969
"he": 1.627,
7070
"an": 1.6052,

corpus/en.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"é": 0.001,
5050
"œ": 0.001
5151
},
52-
"digrams": {
52+
"bigrams": {
5353
"th": 3.2291,
5454
"he": 2.7746,
5555
"an": 1.9598,

corpus/fr.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"á": 0.001,
5858
"ó": 0.001
5959
},
60-
"digrams": {
60+
"bigrams": {
6161
"en": 1.7967,
6262
"es": 1.672,
6363
"re": 1.6559,

corpus/merge.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/bin/env python3
1+
#!/usr/bin/env python3
22
""" Merge two corpus dictionaries. """
33

44
import json
@@ -8,7 +8,7 @@
88
def merge(filenames, filecount):
99
merged = {
1010
"symbols": {},
11-
"digrams": {},
11+
"bigrams": {},
1212
}
1313

1414
# merge dictionaries
@@ -33,7 +33,7 @@ def sort_by_frequency(table, precision=2):
3333
results = {}
3434
results["corpus"] = ""
3535
results["symbols"] = sort_by_frequency(merged["symbols"])
36-
results["digrams"] = sort_by_frequency(merged["digrams"])
36+
results["bigrams"] = sort_by_frequency(merged["bigrams"])
3737
return results
3838

3939

0 commit comments

Comments
 (0)