Skip to content

Commit

Permalink
decouple logic and display (#287)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
fabi1cazenave authored Nov 24, 2024
1 parent a5587a0 commit 7bc4919
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 314 deletions.
307 changes: 65 additions & 242 deletions code/layout-analyzer.js

Large diffs are not rendered by default.

49 changes: 15 additions & 34 deletions code/collapsable-table.js → code/stats-table.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class CollapsableTable extends HTMLElement {
class StatsTable extends HTMLElement {
maxLinesCollapsed = 12;

// Elements built in constructor
Expand All @@ -19,14 +19,6 @@ class CollapsableTable extends HTMLElement {
// Actually build the content of the element (+ remove the stupid tr)
shadow.innerHTML = `
<style>
/* Mostly copy-pasted from '/css/heatmap.css', with some ajustments */
h3 { border-bottom: 1px dotted; }
#header {
text-align: right;
margin-top: -1em;
}
#wrapper {
margin-bottom: 1em;
display: flex;
Expand All @@ -48,37 +40,25 @@ class CollapsableTable extends HTMLElement {
td:nth-child(2) { width: 4em; text-align: right; }
button {
width: 30%;
width: 20%;
height: 1.5em;
margin: auto;
background-color: #88fa;
border: 1px solid black;
border-radius: 15px;
cursor: pointer;
clip-path: polygon(50% 100%, 0% 0%, 100% 0%);
}
button.showLess {
clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
}
</style>
<h3> ${this.id} </h3>
<!-- Using a style attribute on top of the stylesheet, as it is used by
the button 'click' event-listner -->
<div id='wrapper' style='max-height: ${this.maxHeightCollapsed}px;'></div>
<button style='display: none'> show more </button>
<button style='display: none'></button>
`;

// If we find a 'small' element, then move it in a '#header' div instead of
// the '#wrapper' div. A 'slot' is probably better, but I can’t make it work
const smallElement = this.querySelector('small');
const wrapper = shadow.getElementById('wrapper');
if (smallElement) {
// Placing the 'small' element in a wrapper div, otherwise the 'text-align'
// and 'margin-top' css properties don’t do anything.
const smallElementWrapper = document.createElement('div');
smallElementWrapper.id = 'header';
smallElementWrapper.appendChild(smallElement.cloneNode(true));

shadow.insertBefore(smallElementWrapper, wrapper);
// Remove the 'small' element from this.innerHTML, before moving that to shadow
smallElement.remove();
}

wrapper.innerHTML = this.innerHTML;
this.innerHTML = ''; // Remove original content

Expand All @@ -89,19 +69,19 @@ class CollapsableTable extends HTMLElement {
const wrapper = shadow.getElementById('wrapper');
if (wrapper.style.maxHeight == `${self.maxHeightCollapsed}px`) {
wrapper.style.maxHeight = `${wrapper.children[0].offsetHeight}px`;
this.innerText = 'show less';
this.className = 'showLess';
} else {
wrapper.style.maxHeight = `${self.maxHeightCollapsed}px`;
this.innerText = 'show more';
this.className = '';
}
});
}

updateTableData(tableSelector, title, values, precision) {
updateTableData(tableSelector, values, precision) {
const table = this.shadowRoot.querySelector(tableSelector);

table.innerHTML =
`<tr><th colspan='2'>${title}</td></tr>` +
`<tr><th colspan='2'>${table.title}</td></tr>` +
Object.entries(values)
.filter(([digram, freq]) => freq >= 10 ** -precision)
.sort(([_, freq1], [__, freq2]) => freq2 - freq1)
Expand All @@ -115,4 +95,5 @@ class CollapsableTable extends HTMLElement {
table.offsetHeight > this.maxHeightCollapsed ? 'block' : 'none';
}
}
customElements.define('collapsable-table', CollapsableTable);

customElements.define('stats-table', StatsTable);
162 changes: 162 additions & 0 deletions code/stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { getSupportedChars, analyzeKeyboardLayout } from './layout-analyzer.js';

window.addEventListener('DOMContentLoaded', () => {
const inputField = document.querySelector('input');
const keyboard = document.querySelector('x-keyboard');

const headingColor = getComputedStyle(document.querySelector('h1')).color;

let corpusName = '';
let corpus = {};
let keyChars = {};
let impreciseData = false;

// display a percentage value
const fmtPercent = (num, p) => `${Math.round(10 ** p * num) / 10 ** p}%`;
const showPercent = (sel, num, precision) => {
document.querySelector(sel).innerText = fmtPercent(num, precision);
};
const showPercentAll = (sel, nums, precision) => {
document.querySelector(sel).innerText =
nums.map(value => fmtPercent(value, precision)).join(' / ');
};

const showNGrams = (ngrams) => {
const sum = dict => Object.entries(dict).reduce((acc, [_, e]) => acc + e, 0);

showPercent('#sfu-total', sum(ngrams.sfb), 2);
showPercent('#sku-total', sum(ngrams.skb), 2);

showPercent('#sfu-all', sum(ngrams.sfb), 2);
showPercent('#extensions-all', sum(ngrams.lsb), 2);
showPercent('#scissors-all', sum(ngrams.scissor), 2);

showPercent('#inward-all', sum(ngrams.inwardRoll), 1);
showPercent('#outward-all', sum(ngrams.outwardRoll), 1);
showPercent('#sku-all', sum(ngrams.skb), 2);

showPercent('#sks-all', sum(ngrams.sks), 1);
showPercent('#sfs-all', sum(ngrams.sfs), 1);
showPercent('#redirect-all', sum(ngrams.redirect), 1);
showPercent('#bad-redirect-all', sum(ngrams.badRedirect), 2);

const achoppements = document.querySelector('#achoppements stats-table');
achoppements.updateTableData('#sfu-bigrams', ngrams.sfb, 2);
achoppements.updateTableData('#extended-rolls', ngrams.lsb, 2);
achoppements.updateTableData('#scissors', ngrams.scissor, 2);

const bigrammes = document.querySelector('#bigrammes stats-table');
bigrammes.updateTableData('#sku-bigrams', ngrams.skb, 2);
bigrammes.updateTableData('#inward', ngrams.inwardRoll, 2);
bigrammes.updateTableData('#outward', ngrams.outwardRoll, 2);

const trigrammes = document.querySelector('#trigrammes stats-table');
trigrammes.updateTableData('#sks', ngrams.sks, 2);
trigrammes.updateTableData('#sfs', ngrams.sfs, 2);
trigrammes.updateTableData('#redirect', ngrams.redirect, 2);
trigrammes.updateTableData('#bad-redirect', ngrams.badRedirect, 2);
};

const showReport = () => {
const report = analyzeKeyboardLayout(keyboard, corpus, keyChars, headingColor);

document.querySelector('#sfu stats-canvas').renderData({
values: report.totalSfuSkuPerFinger,
maxValue: 4,
precision: 2,
flipVertically: true,
detailedValues: true,
});

document.querySelector('#load stats-canvas').renderData({
values: report.loadGroups,
maxValue: 25,
precision: 1
});

const sumUpBar = bar => bar.good + bar.meh + bar.bad;
const sumUpBarGroup = group => group.reduce((acc, bar) => acc + sumUpBar(bar), 0);

showPercentAll('#load small', report.loadGroups.map(sumUpBarGroup), 1);
showPercent('#unsupported-all', report.totalUnsupportedChars, 3);

document.querySelector('#imprecise-data').style.display
= report.impreciseData ? 'block' : 'none';

document
.querySelector('#achoppements stats-table')
.updateTableData('#unsupported', report.unsupportedChars, 3);

showNGrams(report.ngrams);
};

// keyboard state: these <select> element IDs match the x-keyboard properties
// -- but the `layout` property requires a JSON fetch
const IDs = ['layout', 'geometry', 'corpus'];
const setProp = (key, value) => {
if (key === 'layout') {
if (value) {
const layoutFolder = document
.querySelector(`#layout option[value="${value}"]`).dataset.folder;
fetch(`../keymaps/${layoutFolder}/${value}.json`)
.then(response => response.json())
.then(data => {
const selectedOption = document
.querySelector('#layout option:checked')
.textContent.trim() || value;
inputField.placeholder = `zone de saisie ${selectedOption}`;
keyboard.setKeyboardLayout(
data.keymap,
data.deadkeys,
data.geometry.replace('ergo', 'iso'),
);
data.keymap.Enter = ['\r', '\n'];
keyChars = getSupportedChars(data.keymap, data.deadkeys);
showReport();
});
} else {
keyboard.setKeyboardLayout();
keyChars = {};
inputField.placeholder = 'select a keyboard layout';
}
} else if (key === 'corpus') {
if (value && value !== corpusName) {
fetch(`../corpus/${value}.json`)
.then(response => response.json())
.then(data => {
corpus = data;
showReport();
});
corpusName = value;
}
} else {
keyboard[key] = value;
}
document.getElementById(key).value = value;
};

// store the keyboard state in the URL hash like it's 1995 again! :-)
const state = {};
const updateHashState = (key, value) => {
state[key] = value;
window.location.hash = '/' +
IDs.map(prop => state[prop]).join('/').replace(/\/+$/, '');
};
const applyHashState = () => {
const hash = window.location.hash || '/ergol//en+fr';
const hashState = hash.split('/').slice(1);
IDs.forEach((key, i) => {
setProp(key, hashState[i] || '');
state[key] = hashState[i] || '';
});
};
IDs.forEach(key => {
document
.getElementById(key)
.addEventListener('change', event => {
updateHashState(key, event.target.value);
});
});
window.addEventListener('hashchange', applyHashState);
applyHashState();
});
20 changes: 10 additions & 10 deletions corpus/chardict.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/bin/env python3
""" Turn corpus texts into dictionaries of symbols and digrams. """
#!/usr/bin/env python3
""" Turn corpus texts into dictionaries of symbols, bigrams and trigrams. """

import json
from os import path, listdir
Expand All @@ -9,10 +9,10 @@


def parse_corpus(file_path):
""" Count symbols and digrams in a text file. """
""" Count symbols and bigrams in a text file. """

symbols = {}
digrams = {}
bigrams = {}
trigrams = {}
char_count = 0
prev_symbol = None
Expand All @@ -28,12 +28,12 @@ def parse_corpus(file_path):
symbols[symbol] = 0
symbols[symbol] += 1
if prev_symbol is not None:
digram = prev_symbol + symbol
if digram not in digrams:
digrams[digram] = 0
digrams[digram] += 1
bigram = prev_symbol + symbol
if bigram not in bigrams:
bigrams[bigram] = 0
bigrams[bigram] += 1
if prev_prev_symbol is not None:
trigram = prev_prev_symbol + digram
trigram = prev_prev_symbol + bigram
if trigram not in trigrams:
trigrams[trigram] = 0
trigrams[trigram] += 1
Expand All @@ -55,7 +55,7 @@ def sort_by_frequency(table, precision=3):
results = {}
results["corpus"] = file_path
results["symbols"] = sort_by_frequency(symbols)
results["digrams"] = sort_by_frequency(digrams, 4)
results["bigrams"] = sort_by_frequency(bigrams, 4)
results["trigrams"] = sort_by_frequency(trigrams)
return results

Expand Down
2 changes: 1 addition & 1 deletion corpus/en+fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"æ": 0.001,
"í": 0.001
},
"digrams": {
"bigrams": {
"th": 1.7253,
"he": 1.627,
"an": 1.6052,
Expand Down
2 changes: 1 addition & 1 deletion corpus/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"é": 0.001,
"œ": 0.001
},
"digrams": {
"bigrams": {
"th": 3.2291,
"he": 2.7746,
"an": 1.9598,
Expand Down
2 changes: 1 addition & 1 deletion corpus/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"á": 0.001,
"ó": 0.001
},
"digrams": {
"bigrams": {
"en": 1.7967,
"es": 1.672,
"re": 1.6559,
Expand Down
6 changes: 3 additions & 3 deletions corpus/merge.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/env python3
#!/usr/bin/env python3
""" Merge two corpus dictionaries. """

import json
Expand All @@ -8,7 +8,7 @@
def merge(filenames, filecount):
merged = {
"symbols": {},
"digrams": {},
"bigrams": {},
}

# merge dictionaries
Expand All @@ -33,7 +33,7 @@ def sort_by_frequency(table, precision=2):
results = {}
results["corpus"] = ""
results["symbols"] = sort_by_frequency(merged["symbols"])
results["digrams"] = sort_by_frequency(merged["digrams"])
results["bigrams"] = sort_by_frequency(merged["bigrams"])
return results


Expand Down
Loading

0 comments on commit 7bc4919

Please sign in to comment.