summaryrefslogtreecommitdiff
path: root/lulua/plot.py
diff options
context:
space:
mode:
authorLars-Dominik Braun <lars@6xq.net>2021-10-24 09:47:25 +0200
committerLars-Dominik Braun <lars@6xq.net>2021-10-27 15:44:31 +0200
commita2104773180458a9184466e32075f470f371207c (patch)
treee5a8fe0448efe775c9c8aa6283108c24fc314b39 /lulua/plot.py
parent5c494a2cfd17aaa92a15d907a47ac5172f6f458b (diff)
downloadlulua-a2104773180458a9184466e32075f470f371207c.tar.gz
lulua-a2104773180458a9184466e32075f470f371207c.tar.bz2
lulua-a2104773180458a9184466e32075f470f371207c.zip
report: Add triad analysis
Create and add a new plot that should indicate how difficult typing common triads is.
Diffstat (limited to 'lulua/plot.py')
-rw-r--r--lulua/plot.py158
1 files changed, 148 insertions, 10 deletions
diff --git a/lulua/plot.py b/lulua/plot.py
index fdfc16c..9fb5cf1 100644
--- a/lulua/plot.py
+++ b/lulua/plot.py
@@ -20,9 +20,6 @@
import sys, argparse, json, unicodedata, pickle, logging, math
from operator import itemgetter
-from bokeh.plotting import figure
-from bokeh.models import ColumnDataSource, LinearAxis, Range1d
-from bokeh.embed import json_item
from .layout import *
from .keyboard import defaultKeyboards
@@ -30,9 +27,31 @@ from .util import limit, displayText
from .writer import Writer
from .carpalx import Carpalx, models
+def setPlotStyle (p):
+ """ Set common plot styles """
+
+ # Suppress warnings from bokeh if the legend is empty.
+ if p.legend:
+ p.legend.location = "top_left"
+ # Hide glyph on click on legend
+ p.legend.click_policy = "hide"
+ p.legend.label_text_font = 'IBM Plex Sans Arabic'
+ p.legend.border_line_color = None
+ p.legend.background_fill_color = None
+ p.legend.inactive_fill_color = 'black'
+ p.legend.inactive_fill_alpha = 0.1
+
+ # no border fill
+ p.border_fill_color = None
+ p.background_fill_alpha = 0.5
+
def letterfreq (args):
""" Map key combinations to their text, bin it and plot sorted distribution """
+ from bokeh.plotting import figure
+ from bokeh.models import ColumnDataSource, LinearAxis, Range1d
+ from bokeh.embed import json_item
+
# show unicode class "letters other" only
whitelistCategory = {'Lo'}
@@ -89,15 +108,12 @@ def letterfreq (args):
p.vbar(x='letters', width=0.5, top='rel', color="#dc322f", source=source, y_range_name='single')
p.add_layout(LinearAxis(y_range_name="single"), 'right')
+ setPlotStyle (p)
# styling
p.xgrid.grid_line_color = None
- p.xaxis.major_label_text_font_size = "1.5em"
- p.xaxis.major_label_text_font_size = "1.5em"
- p.xaxis.major_label_text_font = 'IBM Plex Sans Arabic'
- p.yaxis.major_label_text_font = 'IBM Plex Sans Arabic'
- # no border fill
- p.border_fill_color = None
- p.background_fill_alpha = 0.5
+ for axis, size, font in ((p.xaxis, '1.5em', 'IBM Plex Sans Arabic'), (p.yaxis, '1em', 'IBM Plex Sans')):
+ axis.major_label_text_font_size = size
+ axis.major_label_text_font = font
json.dump (json_item (p), sys.stdout)
@@ -153,3 +169,125 @@ def triadfreq (args):
return 0
+def triadEffortData (args):
+ """
+ Plot cumulated triad frequency vs cumulative effort.
+
+ More frequent triads should be easier to type and thus we expect an
+ exponential distribution for optimized layouts and linear distribution
+ for everything else.
+ """
+
+ import numpy as np
+
+ stats = pickle.load (sys.stdin.buffer)
+
+ # XXX: add layout to stats?
+ keyboard = defaultKeyboards['ibmpc105']
+ layout = defaultLayouts[args.layout].specialize (keyboard)
+ writer = Writer (layout)
+
+ # letter-based binning, in case multiple buttons are mapped to the same
+ # letter.
+ binned = defaultdict (lambda: dict (weight=0, effort=Carpalx (models['mod01'], writer), textTriad=None))
+ weightSum = 0
+ for triad, weight in stats['triads'].triads.items ():
+ textTriad = tuple (layout.getText (t) for t in triad)
+ data = binned[textTriad]
+ data['weight'] += weight
+ data['effort'].addTriad (triad, weight)
+ data['textTriad'] = textTriad
+ data['layers'] = tuple (layout.modifierToLayer (x.modifier)[0] for x in triad)
+ weightSum += weight
+
+ # Now bin into equally-sized buckets to reduce amount of data
+ nBins = 200
+ binWidth = weightSum//nBins
+ cumulativeWeight = 0
+ cumulativeEffort = 0
+ x = []
+ y = []
+ for data in sorted (binned.values (), key=lambda x: x['weight'], reverse=True):
+ cumulativeWeight += data['weight']
+ cumulativeEffort += data['effort'].effort * data['weight']
+ if not x or x[-1] + binWidth <= cumulativeWeight:
+ x.append (cumulativeWeight)
+ y.append (cumulativeEffort)
+ x.append (cumulativeWeight)
+ y.append (cumulativeEffort)
+
+ x = np.true_divide (x, cumulativeWeight)
+ y = np.true_divide (y, cumulativeEffort)
+
+ pickle.dump (dict (x=x, y=y, layout=layout), sys.stdout.buffer, pickle.HIGHEST_PROTOCOL)
+
+def triadEffortPlot (args):
+ """ Plot concatenated pickled data from triadEffortData """
+
+ from .stats import unpickleAll
+ # Initializing bokeh is an expensive operation and this module is imported
+ # alot, so only do it when necessary.
+ from bokeh.palettes import Set3
+ from bokeh.plotting import figure
+ from bokeh.models import RadioButtonGroup, CustomJS, Slope
+ from bokeh.embed import json_item
+ from bokeh.layouts import column
+
+ p = figure(
+ plot_width=1000,
+ plot_height=500,
+ sizing_mode='scale_both',
+ x_range=(0, 1),
+ y_range=(0, 1),
+ output_backend="webgl",
+ )
+ data = list (unpickleAll (sys.stdin.buffer))
+ colors = Set3[len(data)]
+ lines = dict ()
+ for o, color in zip (data, colors):
+ name = o['layout'].name
+ assert name not in lines
+ lines[name] = p.line (o['x'], o['y'], line_width=1, color=color,
+ legend_label=name, name=name)
+
+ # color: base1
+ slope = Slope(gradient=1, y_intercept=0,
+ line_color='#93a1a1', line_dash='dashed', line_width=1)
+ p.add_layout(slope)
+
+ setPlotStyle (p)
+ for axis, size, font in ((p.xaxis, '1em', 'IBM Plex Sans'), (p.yaxis, '1em', 'IBM Plex Sans')):
+ axis.major_label_text_font_size = size
+ axis.major_label_text_font = font
+
+ LABELS = ["All", "Standard", "Usable"]
+ visible = {
+ 0: list (lines.keys ()),
+ 1: ['ar-asmo663', 'ar-linux', 'ar-osx'],
+ 2: ['ar-lulua', 'ar-ergoarabic', 'ar-malas', 'ar-linux', 'ar-osx'],
+ }
+ ranges = {
+ 0: [(0, 1), (0, 1)],
+ 1: [(0, 0.5), (0, 0.4)],
+ 2: [(0, 0.5), (0, 0.4)],
+ }
+ presets = RadioButtonGroup (labels=LABELS, active=0)
+ # Set visibility and x/yranges on click. Not sure if there’s a more pythonic way.
+ presets.js_on_click(CustomJS(
+ args=dict(lines=lines, plot=p, visible=visible, ranges=ranges),
+ code="""
+ for (const [k, line] of Object.entries (lines)) {
+ line.visible = visible[this.active].includes (k);
+ }
+ const xrange = plot.x_range;
+ xrange.start = ranges[this.active][0][0];
+ xrange.end = ranges[this.active][0][1];
+ const yrange = plot.y_range;
+ yrange.start = ranges[this.active][1][0];
+ yrange.end = ranges[this.active][1][1];
+ """))
+
+ json.dump (json_item (column (p, presets)), sys.stdout)
+
+ return 0
+