From 13ecae6eb086ffc15a32e794243cb5b83309c8a8 Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Mon, 30 Sep 2019 11:39:49 +0200 Subject: stats/render: Add keyboard heatmap rendering --- lulua/data/render-svg.css | 4 ++++ lulua/render.py | 37 +++++++++++++++++++++++++++++++++---- lulua/stats.py | 19 ++++++++++++++++++- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/lulua/data/render-svg.css b/lulua/data/render-svg.css index 714c56d..b67b75d 100644 --- a/lulua/data/render-svg.css +++ b/lulua/data/render-svg.css @@ -20,6 +20,10 @@ font-size: 40%; font-family: sans-serif; .button .cap { fill: #eee8d5; } +.button .highlight { + fill: #dc322f; /* red */ + filter: blur(0.5em); +} .button.finger-little .shadow { fill: #dc322f; /* red */ } diff --git a/lulua/render.py b/lulua/render.py index b821232..66e5c82 100644 --- a/lulua/render.py +++ b/lulua/render.py @@ -37,7 +37,7 @@ RendererSettings = namedtuple ('RendererSetting', ['buttonMargin', 'middleGap', class Renderer: """ Keyboard to SVG renderer """ - __slots__ = ('keyboard', 'layout', 'settings', 'cursor', 'writer') + __slots__ = ('keyboard', 'layout', 'settings', 'cursor', 'writer', 'keyHighlight') defaultSettings = RendererSettings ( buttonMargin=0.2, @@ -47,11 +47,12 @@ class Renderer: shadowOffset=0.05, ) - def __init__ (self, keyboard, layout=None, writer=None, settings=None): + def __init__ (self, keyboard, layout=None, writer=None, settings=None, keyHighlight=None): self.keyboard = keyboard self.layout = layout self.writer = writer self.settings = settings or self.defaultSettings + self.keyHighlight = keyHighlight or {} self.cursor = [0, 0] @@ -120,7 +121,7 @@ class Renderer: else: buttonText = list (map (displayText, self.layout.getButtonText (btn))) - # background rect + # background rect if any text if any (buttonText): b = svgwrite.shapes.Rect ( insert=((xoff+settings.shadowOffset)*em, (yoff+settings.shadowOffset)*em), @@ -131,6 +132,7 @@ class Renderer: g.add (b) else: gclass.append ('unused') + # main key rect b = svgwrite.shapes.Rect ( insert=(xoff*em, yoff*em), size=(width*em, settings.buttonWidth*em), @@ -160,6 +162,17 @@ class Renderer: class_='marker') g.add (l) + # highlight rect + highlight = self.keyHighlight.get (btn.name, 0) + b = svgwrite.shapes.Rect ( + insert=(xoff*em, yoff*em), + size=(width*em, settings.buttonWidth*em), + rx=settings.rounded*em, + ry=settings.rounded*em, + class_='highlight', + style=f'opacity: {highlight}') + g.add (b) + # clock-wise from bottom-left to bottom-right textParam = [ (-0.5, 0.6, 'layer-1'), @@ -200,7 +213,12 @@ def renderSvg (args): layout = defaultLayouts[args.layout].specialize (keyboard) writer = Writer (layout) - r = Renderer (keyboard, layout=layout, writer=writer) + keyHeat = {} + if args.heatmap: + maxHeat = max (args.heatmap['buttons'].values ()) + keyHeat = dict ((k, v/maxHeat) for k, v in args.heatmap['buttons'].items ()) + + r = Renderer (keyboard, layout=layout, writer=writer, keyHighlight=keyHeat) rendered, (w, h) = r.render () d = svgwrite.Drawing(args.output, size=(w*em, h*em), profile='full') d.defs.add (d.style (args.style.read ().decode ('utf-8'))) @@ -307,6 +325,13 @@ def renderKeyman (args): text = ' '.join ([f'U+{ord (x):04X}' for x in text]) fd.write (f'+ [{comb}] > {text}\n') +def yamlload (s): + try: + with open (s) as fd: + return yaml.safe_load (fd) + except FileNotFoundError: + raise argparse.ArgumentTypeError(f'Cannot open file {s}') + def render (): parser = argparse.ArgumentParser(description='Render keyboard into output format.') parser.add_argument('-l', '--layout', metavar='LAYOUT', help='Keyboard layout name') @@ -321,6 +346,10 @@ def render (): # ourselves type=argparse.FileType('rb'), help='Include external stylesheet into SVG') + sp.add_argument('--heatmap', + metavar='FILE', + type=yamlload, + help='Highlight keys based on heatmap data') sp.set_defaults (func=renderSvg) sp = subparsers.add_parser('xmodmap') sp.set_defaults (func=renderXmodmap) diff --git a/lulua/stats.py b/lulua/stats.py index 4cd20bd..7695f39 100644 --- a/lulua/stats.py +++ b/lulua/stats.py @@ -18,7 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -import sys, operator, pickle, argparse, logging +import sys, operator, pickle, argparse, logging, yaml from operator import itemgetter from itertools import chain, groupby, product from collections import defaultdict @@ -221,6 +221,21 @@ def pretty (args): effort.addTriads (stats['triads'].triads) print ('total effort (carpalx)', effort.effort) +def keyHeatmap (args): + stats = pickle.load (sys.stdin.buffer) + + keyboard = defaultKeyboards[args.keyboard] + layout = defaultLayouts[args.layout].specialize (keyboard) + writer = Writer (layout) + + buttons = {} + buttonPresses = sum (stats['simple'].buttons.values ()) + data = {'total': buttonPresses, 'buttons': buttons} + for k, v in sorted (stats['simple'].buttons.items (), key=itemgetter (1)): + assert k.name not in data + buttons[k.name] = v + yaml.dump (data, sys.stdout) + def main (): parser = argparse.ArgumentParser(description='Process statistics files.') parser.add_argument('-l', '--layout', metavar='LAYOUT', help='Keyboard layout name') @@ -239,6 +254,8 @@ def main (): sp.add_argument('-s', '--sort', choices={'weight', 'effort', 'combined'}, default='weight', help='Sorter') sp.add_argument('-n', '--limit', type=int, default=0, help='Sorter') sp.set_defaults (func=triadfreq) + sp = subparsers.add_parser('keyheatmap') + sp.set_defaults (func=keyHeatmap) logging.basicConfig (level=logging.INFO) args = parser.parse_args() -- cgit v1.2.3