123Your first data view · Step 3 of 3
Chart the answer
You'll learn
- Draw bars with
DrawCanvas:chart - Add scales and an axis
- Pan, zoom and brush
- Time
- 15 min
- Builds on
- Step 2: Ask with SQL
- File
view.lua
A chart shows at a glance what the totals say. In this step the answer becomes a bar chart, and a chart of sales per day lets you drag across some days to ask the question again for those days alone.
Draw the answer as bars
Replace the answer's table with a canvas, and chart totals on it:
local ui = datumhue.ui
New line: local chart = datumhue.chart
New line: local color = datumhue.color
local data = datumhue.args.dirs.data
local sales = data:read("sales.csv"):csv()
local totals = sales:query([[
SELECT item, SUM(sold) AS total
FROM data
GROUP BY item
ORDER BY total DESC
]])
local page = ui.root:panel({
width = "100%",
height = "100%",
flex_direction = "column",
padding = 16,
gap = 12,
})
page:label({
text = "Bakery sales: " .. sales:count() .. " rows",
font_size = 20,
})
local row = page:panel({
flex_direction = "row",
flex_grow = 1,
gap = 12,
})
row:table({
data = sales,
columns = {
{ column = "day", label = "Day" },
{ column = "item", label = "Item" },
{ column = "sold", label = "Sold" },
},
width = 300,
})
New line: local margin =
New line: { left = 50, right = 16, top = 16, bottom = 32 }
New line:
New line: local canvas = datumhue.draw.new({})
New line: canvas:mount({
New line: parent = row,
flex_grow = 1,
New line: auto_size = true,
})
New line: local items = chart.scale.band({
New line: domain = { "Bread", "Buns", "Cake" },
New line: })
New line: local counts = chart.scale.linear({ nice = true })
New line: local plot = canvas:chart({
New line: margin = margin,
New line: x_scale = items,
New line: y_scale = counts,
New line: })
New line: plot:axis({ scale = items, side = "bottom" })
New line: plot:axis({ scale = counts, side = "left", grid = true })
New line: plot:bars({
New line: data = totals,
New line: category_column = "item",
New line: value_column = "total",
New line: color = color("#e69875"),
New line: })
In this example: datumhue.ui datumhue.color datumhue.args Dir:read Bytes:csv DataHandle:query datumhue.ui.root UiElement:panel UiElement:label DataHandle:count UiElement:table datumhue.draw.new DrawCanvas:mount datumhue.chart.scale.band datumhue.chart.scale.linear DrawCanvas:chart PlotArea:axis PlotArea:bars
Three orange bars rise beside the table, tallest for bread.
datumhue.draw.newmakes a canvas to draw on.DrawCanvas:mountputs it in the row, where it grows into the free room, andauto_sizedraws it at the size it gets.DrawCanvas:chartmakes a plot area that covers the canvas. Itsmarginleaves room inside its edges for the axes and their labels.- A scale maps data to positions.
chart.scale.bandgives each item a slot of its own;chart.scale.linearmaps counts to heights, andnicerounds its top to a tidy number. PlotArea:axisdraws a scale's axis along one side, andgriddraws its lines across the plot.PlotArea:barsreadstotalsitself: one bar peritem, as tall as itstotal.
Brush a range to ask again
Add a second query, sales per day, and chart it as a line above the bars. Dragging across the line asks for the totals of the days you cover:
local ui = datumhue.ui
local chart = datumhue.chart
local color = datumhue.color
local data = datumhue.args.dirs.data
local sales = data:read("sales.csv"):csv()
local totals = sales:query([[
SELECT item, SUM(sold) AS total
FROM data
GROUP BY item
ORDER BY total DESC
]])
New line: local daily = sales:query([[
New line: SELECT day, SUM(sold) AS sold
New line: FROM data
New line: GROUP BY day
New line: ORDER BY day
New line: ]])
local page = ui.root:panel({
width = "100%",
height = "100%",
flex_direction = "column",
padding = 16,
gap = 12,
})
page:label({
text = "Bakery sales: " .. sales:count() .. " rows",
font_size = 20,
})
local row = page:panel({
flex_direction = "row",
flex_grow = 1,
gap = 12,
})
row:table({
data = sales,
columns = {
{ column = "day", label = "Day" },
{ column = "item", label = "Item" },
{ column = "sold", label = "Sold" },
},
width = 300,
})
New line: local charts = row:panel({
New line: flex_direction = "column",
New line: flex_grow = 1,
New line: gap = 12,
New line: })
local margin =
{ left = 50, right = 16, top = 16, bottom = 32 }
New line: local week = datumhue.draw.new({})
New line: week:mount({
New line: parent = charts,
New line: flex_grow = 1,
New line: auto_size = true,
New line: })
New line: local days = chart.scale.linear({})
New line: local per_day = chart.scale.linear({ nice = true })
New line: local trend = week:chart({
New line: margin = margin,
New line: x_scale = days,
New line: y_scale = per_day,
New line: })
New line: trend:axis({ scale = days, side = "bottom", ticks = 7 })
New line: trend:axis({ scale = per_day, side = "left", grid = true })
New line: trend:line({
New line: data = daily,
New line: x_column = "day",
New line: y_column = "sold",
New line: color = color("#7fbbb3"),
New line: width = 3,
New line: })
New line:
local canvas = datumhue.draw.new({})
canvas:mount({
New line: parent = charts,
flex_grow = 1,
auto_size = true,
})
local items = chart.scale.band({
domain = { "Bread", "Buns", "Cake" },
})
local counts = chart.scale.linear({ nice = true })
local plot = canvas:chart({
margin = margin,
x_scale = items,
y_scale = counts,
})
plot:axis({ scale = items, side = "bottom" })
plot:axis({ scale = counts, side = "left", grid = true })
New line: local bars = plot:bars({
data = totals,
category_column = "item",
value_column = "total",
color = color("#e69875"),
})
New line:
New line: local in_range = [[
New line: SELECT item, SUM(sold) AS total
New line: FROM data
New line: WHERE day BETWEEN ? AND ?
New line: GROUP BY item
New line: ]]
New line: trend:pan_zoom({
New line: y = false,
New line: pan_button = "right",
New line: box_zoom_button = false,
New line: })
New line: trend:brush({
New line: on_brush = function(range)
New line: if range == nil then
New line: bars:replace_data(totals)
New line: else
New line: local args = { range.x_min, range.x_max }
New line: bars:replace_data(sales:query(in_range, args))
New line: end
New line: end,
New line: })
In this example: datumhue.ui datumhue.color datumhue.args Dir:read Bytes:csv DataHandle:query datumhue.ui.root UiElement:panel UiElement:label DataHandle:count UiElement:table datumhue.draw.new DrawCanvas:mount datumhue.chart.scale.linear DrawCanvas:chart PlotArea:axis PlotArea:line datumhue.chart.scale.band PlotArea:bars PlotArea:pan_zoom PlotArea:brush Mark:replace_data
Sales climb toward the weekend in the line above the bars. Drag across days 6 and 7 with the left button: the bars show the weekend's totals. Click the chart to see the whole week again.
dailyis a second query on the same sales, one row per day.- The line and the bars each get a canvas of their own, stacked in the
chartscolumn. PlotArea:linejoins one point per row ofdaily.PlotArea:pan_zoomzooms the days with the mouse wheel and pans them with the right button, since the left is the brush's.box_zoom_button = falseturns off the box zoom that would take the right button otherwise, andy = falsekeeps the counts still.PlotArea:brushselects days as you drag with the left button. On release it callson_brushwith the range, in days; a click calls it withnil.- The
?marks inin_rangetake the range's ends, in order.Mark:replace_datagives the bars the new answer, and their scale fits its totals.
Try this
- Add
trend:crosshair(), and read the sales under the pointer. - Write the brushed days into the heading: keep the label in a local,
and set its
textinon_brush. - Mark each day on the line with
PlotArea:points, bound todaily.
Recap
DrawCanvas:chartmakes a plot area. Scales map data to positions, and axes label them.- Marks such as
PlotArea:barsandPlotArea:lineread aDataHandlethemselves. PlotArea:pan_zoomandPlotArea:brushadd gestures, andMark:replace_datashows the answer to a new query.- Query and chart data goes further: histograms, small multiples, and charts that filter each other.
New in this step