In [1]:
files = ['clicks_2020-01-24 09:48:51_touchpad_14"_monitor.csv',
'clicks_2020-01-24 09:44:46_mouse_24"_monitor.csv',
'clicks_2020-01-23 16:00:32_mouse_24"_monitor.csv']
Test data from the application loaded into a simple data container. One row contains the data of a click. If not changed the first file from files is loaded.
| circle_x | circle_y | click_x | click_y | timestamp | radius |
|---|---|---|---|---|---|
| 391 | 207 | 426 | 232 | 2020-01-23 15:59:09.367584 | 30 |
In [2]:
import csv
import numpy as np
import pandas as pd
from dataclasses import dataclass
from datetime import datetime, timedelta
In [3]:
@dataclass
class CircleClick():
circle_x: int
circle_y: int
click_x: int
click_y: int
radius: int
timestamp: datetime
In [4]:
clicks = []
with open(files[0]) as src:
reader = csv.reader(src)
for row in reader:
circle_click = CircleClick(circle_x=int(row[0]), circle_y=int(row[1]),
click_x=int(row[2]), click_y=int(row[3]),
timestamp=datetime.strptime(row[4], '%Y-%m-%d %H:%M:%S.%f'),
radius=int(row[5]))
clicks.append(circle_click)
In [5]:
clicks[0]
Out[5]:
ID is the index of difficulty.
$ID = \log_2 \left(\dfrac{2D}{W}\right)$
D is the distance from the starting point to the center of the target.
W is the width of the target measured along the axis of motion. W can also be thought of as the allowed error tolerance in the final position, since the final point of the motion must fall within $\pm \frac{W}{2}$ of the target's center.
MT is the average time to complete the movement.
a and b are constants that depend on the choice of input device and are usually determined empirically by regression analysis. a defines the intersection on the y-axis and is often as interpreted as a delay. The b-parameter is a slope and describes an acceleration. Both paramters show the linear dependency in Fitts' Law.
$MT = a + b \cdot ID = a + b \cdot \log_2 \left(\dfrac{2D}{W}\right) $
In [6]:
def distance(x1: int, x2: int, y1: int, y2: int):
a = np.power(x1 - x2, 2)
b = np.power(y1 - y2, 2)
distance = np.sqrt(a + b)
return distance
distance(0, 1, 0, 1)
Out[6]:
In [7]:
@dataclass
class FittsModel:
D: float = 0
W: float = 0
ID: float = 0
MT: timedelta = timedelta(0)
def calculate(self, start: CircleClick, end: CircleClick):
"""The model calculates its values D, W, ID and MT
based on two clicks"""
self.D = distance(start.click_x,
end.circle_x + end.radius,
start.click_y,
end.circle_y + end.radius)
self.W = end.radius * 2
self.ID = np.log2(2 * self.D / self.W)
self.MT = end.timestamp - start.timestamp
@property
def MT_in_millis(self):
millis, micros = divmod(self.MT.microseconds, 1000)
return self.MT.total_seconds() * 1000 + millis + micros / 1000
Calculate all models that can be drawn on the graph later on.
In [8]:
models = []
for i in range(1, len(clicks)):
model = FittsModel()
model.calculate(clicks[i - 1], clicks[i])
models.append(model)
models[0]
Out[8]:
All data is put into a pandas dataframe for easier selection and matplot drawing
In [9]:
data = {'D': [], 'W': [], 'ID': [], 'MT': []}
for m in models:
data['D'].append(m.D)
data['W'].append(m.W)
data['ID'].append(m.ID)
data['MT'].append(m.MT_in_millis)
df = pd.DataFrame(data=data)
df
Out[9]:
In [10]:
widths = set([m.W for m in models])
widths
Out[10]:
In [11]:
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib
In [12]:
matplotlib.rcParams['figure.figsize']
Out[12]:
In [13]:
matplotlib.rcParams['figure.figsize'] = [12, 8]
In [14]:
df['ID'].mean()
Out[14]:
In [15]:
df.groupby(['W']).mean()
Out[15]:
In [16]:
df.groupby(['W']).median()
Out[16]:
In [17]:
from sklearn.linear_model import LinearRegression
In [18]:
# uncomment the next line to select a specific circle width
# widths = [100]
In [19]:
for width in widths:
_df = df[df['W'] == width]
model = LinearRegression()
model.fit(_df[['ID']], _df[['MT']])
min_x = min(df['ID'])
max_x = max(df['ID'])
predicted = model.predict([[min_x], [max_x]])
plt.scatter(x=_df['ID'], y=_df['MT'])
plt.plot([min_x, max_x], predicted)
plt.legend(widths)
plt.show()