Lots of charts
First things first: I've uploaded a new version with some minor ergonomic improvements for non-mobile devices: some of the menu buttons now have keyboard shortcuts. They were noticeably more difficult to click on with a mouse than on a touchscreen.
There are also tooltips when you hover on the menu buttons which should hopefully make it easy to learn about and remember the keyboard shortcuts.
If you're on a mobile device, nothing should change. No need to upgrade.
To test this new version I've been spending more time running Carousel on my laptop. Today's program is a library for plotting charts in various ways.
I want this for myself, but there's also an element of academic curiosity here. There are various tools for drawing charts: gnuplot, JGraph, Excel. Programming languages like R, Matlab and Python also have common tools. One property all these approaches share* is an ad hoc, accreting set of configuration options. Look at gnuplot's plot, for example. Tutorials tend to be cookbooks for some common uses followed by a link to the manual. Not to be too critical -- I'm never going to match these tools on features -- but I've wondered for a long time if there might be a cleaner set of building blocks that fit my brain better. This is a start, so let's get started. Feel free to follow along by pasting the following code snippets into a screen in Carousel.
* - Matplotlib for Python might be an exception. Its API looks fine at first glance, albeit more verbose. But I haven't actually used it myself yet.
Here's a table with some randomly generated data:
data = {}
-- 16 sample points x 5 units apart = 80
for i=1,16 do
table.insert(data, {i*5, rand(60), rand(60)})
end
I'm going to ignore for this post the question of how to get data into Carousel. Particularly on mobile. (Not a very clean story. Yet.)
Also, I'm deliberately shaping this data to simplify rendering. A previous post is more flexible in this respect, but we'll not go there today. We'll skip the code for panning and zooming, even if positioning the chart on screen now gets a bit verbose:
-- decide how large a 80:60 image you can fit
H = Safe_height-Menu_bottom
if Safe_width/80 < H/60 then
-- landscape
v = {left=50, right=Safe_width-50}
local h = (v.right-v.left)/80*60
v.top = Menu_bottom + (H-h)/2
v.bottom = v.top + h
else
-- portrait
v = {top=Menu_bottom+50, bottom=Safe_height-50}
local w = (v.bottom-v.top)/60*80
v.left = (Safe_width-w)/2
v.right = v.left + w
end
-- at this point, 80/(v.right-v.left) == 60/(v.bottom-v.top)
line(v.left, v.top, v.left, v.bottom)
line(v.left, v.bottom, v.right, v.bottom)
Hit 'run' and we finally have some bare axes.

(Won't automatically redraw when rotating the device. Hit 'run' again if you rotate.)
The variable `v` (for 'viewport') now provides a bounding box, and here are a couple of helpers to translate data to within this bounding box:
function vx(x) return v.left + scale(x) end function vy(y) return v.bottom - scale(y) end function scale(d) return d/80*(v.right-v.left) end
Now we can draw a couple of scatterplots based on `data`:
function col(c)
return function(data, i) return data[i][c] end
end
function plot(data, f, ...)
for i=1,#data do f(data, i, ...) end
end
function points(data, i, x, y)
circle('fill', vx(x(data, i)), vy(y(data, i)), 3)
end
color(1,0,0)
plot(data, points, col(1), col(2))
color(0,0,1)
plot(data, points, col(1), col(3))

Slow down to read the definition of `col`. It's a way to extract columns from the data, but it returns a function that `plot` can call. No special new syntax for columns, just plain Lua. And it's flexible enough to handle data where the rows are key-value tables rather than arrays as above. I can pass a column name rather than a column number to `col`, and it'll just work.
`plot` takes another function as its argument above: `points`, which provides the strategy to follow to plot my data. Here are some other strategies I can swap in (I based their names on the gnuplot manual):
function lines(data, i, x, y)
if i == 1 then return end
line(vx(x(data, i-1)), vy(y(data, i-1)), vx(x(data, i)), vy(y(data, i)))
end
function linespoints(data, i, x, y)
circle('fill', vx(x(data, i)), vy(y(data, i)), 3)
if i == 1 then return end
line(vx(x(data, i-1)), vy(y(data, i-1)), vx(x(data, i)), vy(y(data, i)))
end
function impulses(data, i, x, y)
local vx = vx(x(data, i))
line(vx, vy(y(data, i)), vx, vy(0))
end
function steps(data, i, x, y)
if i == 1 then return end
local x1, y1 = x(data, i-1), y(data, i-1)
local x2, y2 = x(data, i), y(data, i)
line(vx(x1), vy(y1), vx(x1), vy(y2))
line(vx(x1), vy(y2), vx(x2), vy(y2))
end
function fsteps(data, i, x, y)
if i == 1 then return end
local x1, y1 = x(data, i-1), y(data, i-1)
local x2, y2 = x(data, i), y(data, i)
line(vx(x1), vy(y1), vx(x2), vy(y1))
line(vx(x2), vy(y1), vx(x2), vy(y2))
end
function histeps(data, i, x, y)
local x2, y2 = x(data, i), y(data, i)
circle('fill', vx(x2), vy(y2), 3)
if i > 1 then
local x1, y1 = x(data, i-1), y(data, i-1)
local px, py = (x1+x2)/2, (y1+y2)/2
line(vx(x1), vy(y1), vx(px), vy(y1))
line(vx(px), vy(y1), vx(px), vy(y2))
line(vx(px), vy(y2), vx(x2), vy(y2))
end
if i < #data then
local x3, y3 = x(data, i+1), y(data, i+1)
local nx, ny = (x2+x3)/2, (y2+y3)/2
line(vx(x2), vy(y2), vx(nx), vy(y2))
line(vx(nx), vy(y2), vx(nx), vy(y3))
line(vx(nx), vy(y3), vx(x3), vy(y3))
end
end
Here are the corresponding images for these strategies, all for the exact same data points, and with links to the corresponding gnuplot documentation. Just replace the word 'points' in the calls to `plot` above with:





All the above strategies require two columns of data, for x and y coordinates. But `plot` can handle any number of columns if a strategy needs it. Let's first regenerate a few more columns:
data = {}
for i=1,16 do
local angle1 = rand(60)/60*pi
table.insert(data, {i*5, rand(60), rand(60), rand(60)/60*10, rand(60)/60*10, angle1, angle1+rand(60)/60*pi, tostring(rand(1000))})
end
Don't bother reading this. I'm basically generating 2 large numbers, 2 small numbers and 2 angles between 0 and 360 degrees.
Here's a strategy with more arguments, that uses more columns for each object on the chart:
function circles(data, i, x, y, radius, drawmode) if type(radius) == 'function' then radius = radius(data, i) end circle(optional(drawmode, data, i, 'fill'), vx(x(data, i)), vy(y(data, i)), scale(radius)) end function optional(f, data, i, default) if f == nil then return default end if type(f) == 'function' then return f(data, i) end return f end
Now we can draw circles with a fixed radius:
plot(data, circles, col(1), col(2), 1)

Circles with outlines but not filled in, thanks to the `optional` helper above:
plot(data, circles, col(1), col(2), 1, 'line')

Circles with a radius based on column 4:
plot(data, circles, col(1), col(2), col(4), 'line')

More strategies:
function boxes(data, i, x, y, w)
local w = optional(w, data, i, 5)
local y = y(data, i)
rect('fill', vx(x(data, i)-w/2), vy(y), scale(w), vy(0) - vy(y))
end
function arcs(data, i, x, y, radius, angle1, angle2, drawmode)
if type(radius) == 'function' then radius = radius(data, i) end
if type(angle1) == 'function' then angle1 = angle1(data, i) end
if type(angle2) == 'function' then angle2 = angle2(data, i) end
arc(optional(drawmode, data, i, 'fill'), vx(x(data, i)), vy(y(data, i)), scale(radius), angle1, angle2)
end
function ellipses(data, i, x, y, radiusx, radiusy, drawmode)
if type(radiusx) == 'function' then radiusx = radiusx(data, i) end
if type(radiusy) == 'function' then radiusy = radiusy(data, i) end
ellipse(optional(drawmode, data, i, 'fill'), vx(x(data, i)), vy(y(data, i)), scale(radiusx), scale(radiusy))
end
function financebars(data, i, x, open, close, high, low)
local vx = vx(x(data, i))
line(vx, vy(low(data, i)), vx, vy(high(data, i)))
local open = vy(open(data, i))
line(vx-2, open, vx, open)
local close = vy(close(data, i))
line(vx, close, vx+2, close)
end
function candlesticks(data, i, x, open, close, high, low)
local vx = vx(x(data, i))
local open = vy(open(data, i))
local close = vy(close(data, i))
local lo = min(open, close)
local hi = max(open, close)
rect('fill', vx-2, lo, 5, hi-lo)
local low = vy(low(data, i))
local high = vy(high(data, i))
line(vx, hi, vx, low)
line(vx, lo, vx, high)
end
function boxerrorbars(data, i, x, y, dy, w, drawmode)
local w = optional(w, data, i, 4)
local x = x(data, i)
local y, dy = y(data, i), dy(data, i)
rect(optional(drawmode, data, i, 'fill'), vx(x-w/2), vy(y), scale(w), vy(0) - vy(y))
line(vx(x), vy(y-dy/2), vx(x), vy(y+dy/2))
end
function labels(data, i, x, y, text)
love.graphics.print(text(data, i), vx(x(data, i)), vy(y(data, i)))
end
Now we can try a few more examples.
boxes, basically like single-type histograms:
plot(data, boxes, col(1), col(2))

boxerrorbars, which depends on a hack of using a transparent color:
color(0,0,1, 0.5) plot(data, boxerrorbars, col(1), col(2), col(4))

plot(data, ellipses, col(1), col(2), col(4), col(5), 'line')

arcs, where we use our angle columns:
plot(data, arcs, col(1), col(2), col(4), col(6), col(7))

It turns out there's a whole variety of charts with error bars:
function xerrorbars(data, i, x, y, dx)
local vy = vy(y(data, i))
circle('fill', vx(x(data, i)), vy, 3)
local x, dx = x(data, i), dx(data, i)
line(vx(x-dx/2), vy, vx(x+dx/2), vy)
end
function xerrorlines(data, i, x, y, dx)
do
local vy = vy(y(data, i))
circle('fill', vx(x(data, i)), vy, 3)
local x, dx = x(data, i), dx(data, i)
line(vx(x-dx/2), vy, vx(x+dx/2), vy)
end
if i == 1 then return end
line(vx(x(data, i-1)), vy(y(data, i-1)), vx(x(data, i)), vy(y(data, i)))
end
function xerrorbars2(data, i, x, y, xlo, xhi)
local vy = vy(y(data, i))
circle('fill', vx(x(data, i)), vy, 3)
line(vx(xlo(data, i)), vy, vx(xhi(data, i)), vy)
end
function xerrorlines2(data, i, x, y, xlo, xhi)
do
local vy = vy(y(data, i))
circle('fill', vx(x(data, i)), vy, 3)
line(vx(xlo(data, i)), vy, vx(xhi(data, i)), vy)
end
if i == 1 then return end
line(vx(x(data, i-1)), vy(y(data, i-1)), vx(x(data, i)), vy(y(data, i)))
end
function yerrorbars(data, i, x, y, dy)
local vx = vx(x(data, i))
circle('fill', vx, vy(y(data, i)), 3)
local y, dy = y(data, i), dy(data, i)
line(vx, vy(y-dy/2), vx, vy(y+dy/2))
end
function yerrorlines(data, i, x, y, dy)
do
local vx = vx(x(data, i))
circle('fill', vx, vy(y(data, i)), 3)
local y, dy = y(data, i), dy(data, i)
line(vx, vy(y-dy/2), vx, vy(y+dy/2))
end
if i == 1 then return end
line(vx(x(data, i-1)), vy(y(data, i-1)), vx(x(data, i)), vy(y(data, i)))
end
function yerrorbars2(data, i, x, y, ylo, yhi)
local vx = vx(x(data, i))
circle('fill', vx, vy(y(data, i)), 3)
line(vx, vy(ylo(data, i)), vx, vy(yhi(data, i)))
end
function yerrorlines2(data, i, x, y, ylo, yhi)
do
local vx = vx(x(data, i))
circle('fill', vx, vy(y(data, i)), 3)
line(vx, vy(ylo(data, i)), vx, vy(yhi(data, i)))
end
if i == 1 then return end
line(vx(x(data, i-1)), vy(y(data, i-1)), vx(x(data, i)), vy(y(data, i)))
end
function xyerrorbars(data, i, x, y, dx, dy)
local x, dx = x(data, i), dx(data, i)
local y, dy = y(data, i), dy(data, i)
circle('fill', vx(x), vy(y), 3)
line(vx(x-dx/2), vy(y), vx(x+dx/2), vy(y))
line(vx(x), vy(y-dy/2), vx(x), vy(y+dy/2))
end
function xyerrorlines(data, i, x, y, dx, dy)
do
local x, dx = x(data, i), dx(data, i)
local y, dy = y(data, i), dy(data, i)
circle('fill', vx(x), vy(y), 3)
line(vx(x-dx/2), vy(y), vx(x+dx/2), vy(y))
line(vx(x), vy(y-dy/2), vx(x), vy(y+dy/2))
end
if i == 1 then return end
line(vx(x(data, i-1)), vy(y(data, i-1)), vx(x(data, i)), vy(y(data, i)))
end
function xyerrorbars2(data, i, x, y, xlo, xhi, ylo, yhi)
circle('fill', vx(x), vy(y), 3)
line(vx(xlo(data, i)), vy(y), vx(xhi(data, i)), vy(y))
line(vx(x), vy(ylo(data, i)), vx(x), vy(yhi(data, i)))
end
function xyerrorlines2(data, i, x, y, xlo, xhi, ylo, yhi)
circle('fill', vx(x), vy(y))
line(vx(xlo(data, i)), vy(y), vx(xhi(data, i)), vy(y))
line(vx(x), vy(ylo(data, i)), vx(x), vy(yhi(data, i)))
if i == 1 then return end
line(vx(x(data, i-1)), vy(y(data, i-1)), vx(x(data, i)), vy(y(data, i)))
end
function boxxyerror(data, i, x, y, dx, dy, drawmode)
rect(optional(drawmode, data, i, 'line'), vx(x(data, i)), vy(y(data, i)), scale(dx(data, i)), scale(dy(data, i)))
end
function boxerrorbars(data, i, x, y, dy, w, drawmode)
local w = optional(w, data, i, 4)
local x = x(data, i)
local y, dy = y(data, i), dy(data, i)
rect(optional(drawmode, data, i, 'fill'), vx(x-w/2), vy(y), scale(w), vy(0) - vy(y))
line(vx(x), vy(y-dy/2), vx(x), vy(y+dy/2))
end
I'll just show one example from this set:
plot(data, xyerrorbars, col(1), col(2), col(4), col(5))

Alright, this post is getting long. Here's all the scaffolding code in one place:
-- decide how large a 80:60 image you can fit
H = Safe_height-Menu_bottom
if Safe_width/80 < H/60 then
-- landscape
v = {left=50, right=Safe_width-50}
local h = (v.right-v.left)/80*60
v.top = Menu_bottom + (H-h)/2
v.bottom = v.top + h
else
-- portrait
v = {top=Menu_bottom+50, bottom=Safe_height-50}
local w = (v.bottom-v.top)/60*80
v.left = (Safe_width-w)/2
v.right = v.left + w
end
-- at this point, 80/(v.right-v.left) == 60/(v.bottom-v.top)
line(v.left, v.top, v.left, v.bottom)
line(v.left, v.bottom, v.right, v.bottom)
function vx(x) return v.left + scale(x) end
function vy(y) return v.bottom - scale(y) end
function scale(d) return d/80*(v.right-v.left) end
function col(c)
return function(data, i) return data[i][c] end
end
function plot(data, f, ...)
for i=1,#data do f(data, i, ...) end
end
function optional(f, data, i, default)
if f == nil then return default end
if type(f) == 'function' then return f(data, i) end
return f
end
This is the core. Paste it into a screen. Save my strategies above in a separate screen or two. Remember to first run the abbreviations on one of the example screens. Or if you've deleted that screen, here it is again:
-- Some abbreviations to reduce typing. g = love.graphics pt, line = g.points, g.line rect, poly = g.rectangle, g.polygon circle, arc, ellipse = g.circle, g.arc, g.ellipse color = g.setColor min, max = math.min, math.max floor, ceil = math.floor, math.ceil abs, rand = math.abs, math.random pi, cos, sin = math.pi, math.cos, math.sin touches = love.touch.getTouches touch = love.touch.getPosition audio = love.audio.newSource
Basically I keep a scratch screen around that looks like this:
run_screen('abbrev')
run_screen('plot')
run_screen('plots')
run_screen('errorplots')
data = {}
for i=1,16 do
local angle1 = rand(60)/60*pi
table.insert(data, {i*5, rand(60), rand(60), rand(60)/60*10, rand(60)/60*10, angle1, angle1+rand(60)/60*pi, tostring(rand(1000))})
end
color(0,0,1, 0.3)
plot(data, xyerrorbars, col(1), col(2), col(4), col(5))
Now I can replace `data` and the `plot` commands as needed. This seems to cover most of my needs, around half of gnuplot's subplot types. Of course, this is just for quick-and-dirty visualization, not publication-quality images. No support for 3D or heatmaps or polar coordinates. The next feature I want is histograms.
Files
Get Lua Carousel
Lua Carousel
Write programs on desktop and mobile
| Status | In development |
| Category | Tool |
| Author | Kartik Agaram |
| Tags | LÖVE |
More posts
- Programming on your device with your preferred language70 days ago
- Lua Carousel: program on the device you have, with docs at your fingertipsMay 12, 2025
- Pong Wars, MMO editionFeb 16, 2025
- New version after 41 days, and stop-motion animationFeb 15, 2025
- Drawing with a pen on a pendulumJan 11, 2025
- New version after 16 daysJan 04, 2025
- New version after 9 daysDec 19, 2024
- New version after 3 daysNov 17, 2024
- New version after 40 daysNov 14, 2024
- Turn your phone or tablet into a chess clockNov 01, 2024
Leave a comment
Log in with itch.io to leave a comment.