-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdot_plot.lua
227 lines (190 loc) · 6.34 KB
/
dot_plot.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
--- Make a plot out of dots! Tight!
local dot_plot = {}
local null_utf8 = 0x2800
local pixel_map = {
{ 0x01, 0x08 },
{ 0x02, 0x10 },
{ 0x04, 0x20 },
{ 0x40, 0x80 }
}
--- courtesy https://stackoverflow.com/a/26071044
local bytemarkers = { {0x7FF,192}, {0xFFFF,224}, {0x1FFFFF,240} }
local function encode(decimal)
if decimal<128 then return string.char(decimal) end
local charbytes = {}
for bytes,vals in ipairs(bytemarkers) do
if decimal<=vals[1] then
for b=bytes+1,2,-1 do
local mod = decimal%64
decimal = (decimal-mod)/64
charbytes[b] = string.char(128+mod)
end
charbytes[1] = string.char(vals[2]+decimal)
break
end
end
return table.concat(charbytes)
end
local function interpolate(a, b, res)
local mid = {
x = (a.x + b.x) / 2,
y = (a.y + b.y) / 2
}
if res > 1 then
--- Surely there's something I'm just not getting...
-- return interpolate(a, mid, res-1), mid, interpolate(mid, b, res-1)
local ret = interpolate(a, mid, res-1)
ret[#ret+1] = mid
for _, v in pairs(interpolate(mid, b, res-1)) do
ret[#ret+1] = v
end
return ret
else
-- return mid
return {mid}
end
end
local function new_canvas(args)
local canvas = {}
--- transformation from cartesian space to screen-space to dot-space
-- (x,y): cartesian point
-- <x,y>: screen point
-- col,row [x_i,y_i]: dot point
-- <x> <- floor( ( (x - x_min) * 2 * c_width ) / (x_max - x_min) )
-- <y> <- floor( ( (y - y_min) * 4 * c_height ) / (y_max - y_min) )
-- col <- floor( <x> / 2 )
-- row <- floor( <y> / 4 )
-- x_i <- 1 + ( <x> % 2 )
-- y_i <- 4 - ( <y> % 4 )
local scale = {
x = (2 * args.width) / (args.x.max - args.x.min),
y = (4 * args.height) / (args.y.max - args.y.min)
}
canvas.set = function(self, x, y)
if args.verbose >= 2 then
print(string.format("Setting (%.1f, %.1f)...", x, y))
end
local px = {
x = math.floor((x - args.x.min) * scale.x),
y = math.floor((y - args.y.min) * scale.y)
}
local col = math.floor(px.x / 2)
local row = math.floor(px.y / 4)
local x_i = 1 + (px.x % 2)
local y_i = 4 - (px.y % 4)
if not self[row] then
self[row] = {}
end
self[row][col] = (self[row][col] or null_utf8) | pixel_map[y_i][x_i]
if args.verbose >= 2 then
print(string.format("Set (%d, %d) => %d, %d [%d, %d]", px.x, px.y, col, row, x_i, y_i))
end
end
canvas.render = function(self)
local pad = math.max(tostring(args.y.max):len(), tostring(args.y.min):len())
local pad_fmt = string.format("%%%dd", pad)
local ret = ""
for row=(args.height - 1), 0, -1 do
-- render padding & y-axis ticks
if args.y.ticks > 0 then
if row%args.y.ticks == 0 then
ret = ret .. pad_fmt:format(math.floor((row*4)/scale.y + args.y.min))
else
ret = ret .. string.rep(' ', pad)
end
end
-- render plot
for col=0, (args.width - 1) do
ret = ret .. encode(self[row][col] or null_utf8)
end
-- new line, unless this is the last line
if row > 0 then
ret = ret .. "\n"
end
end
-- render x-axis ticks
if args.x.ticks > 0 then
ret = ret .. "\n" .. string.format('%%%dd', pad+1):format(0)
local xpad_fmt = string.format("%%%dd", args.x.ticks)
for col=args.x.ticks, (args.width - 1), args.x.ticks do
ret = ret .. xpad_fmt:format(math.floor((col*2) / scale.x + args.x.min))
end
end
return ret
end
return canvas
end
local function getkeys(tbl)
local keys = {}
local n = 0
for k,_ in pairs(tbl) do
n = n+1
keys[n] = k
end
return keys
end
local function infer_args(data, args)
local xdata = getkeys(data)
-- width defaults to data length / 2
args.width = args.width or math.ceil(#xdata / 2)
-- height defaults to 1 character
args.height = args.height or 1
-- smoothing defaults to 1 iteration
args.smoothing = args.smoothing or 1
-- verbosity defaults to 0 (none)
args.verbose = args.verbose or 0
-- if y-axis settings aren't given, populate them
args.y = args.y or {}
-- y-axis ticks default to every 3 rows
args.y.ticks = args.y.ticks or 3
-- y-axis max/min default to inferred based on data
args.y.max = args.y.max or math.ceil(math.max(unpack(data)) + 0.01)
args.y.min = args.y.min or math.floor(math.min(unpack(data)))
-- if x-axis settings aren't given, populate them
args.x = args.x or {}
-- x-axis ticks default to 0 (off)
args.x.ticks = args.x.ticks or 0
-- y-axis max/min default to inferred based on data
args.x.max = args.x.max or math.ceil(math.max(unpack(xdata)))
args.x.min = args.x.min or math.floor(math.min(unpack(xdata)))
-- if verbose, print arguments
if args.verbose >= 1 then
local pretty = require('pl.pretty')
print("args = " .. pretty.write(args))
end
return args
end
-- Plot the given data using dots
--
-- @param data The data to plot
-- @param args Arguments for plotting, as follows:
-- - width: width of the plot, in characters. Default: half of number of data points, rounded up
-- - height: height of the plot, in characters. Default: 1
-- - smoothing: level of smoothing to apply. Default: 1
-- - verbose: level of debug output. Default: 0 (off)
-- - x, y: table of axis arguments, as follows:
-- - max: upper bound of axis. Default: upper bound of data
-- - min: lower bound of axis. Default: lower bound of data
-- - ticks: Axis tick period. Default: 3 rows for y, 0 (off) for x
dot_plot.plot = function(data, args)
-- merge arguments
args = infer_args(data, args or {})
-- draw to canvas
local canvas = new_canvas(args)
local prev = false
for x, y in ipairs(data) do
if prev and args.smoothing > 0 then
for _, pt in pairs(interpolate(prev, {x=x, y=y}, args.smoothing)) do
canvas:set(pt.x, pt.y)
end
end
canvas:set(x, y)
prev = {x=x, y=y}
end
if args.verbose >= 3 then
local pretty = require('pl.pretty')
print("canvas = " .. pretty.write(canvas))
end
return canvas:render()
end
return dot_plot