Project: Data Visualization
Build a simple bar chart driven by ViewModel data.
Rive Setup
- Create a ViewModel named
ChartVMwith numbersa,b, andc - Bind those properties to any external data source or animate them in the editor
- Attach this Node script to a group on your Artboard
Node Script
--!strict
export type BarChart = {
barWidth: Input<number>,
maxHeight: Input<number>,
paths: {Path},
paints: {Paint},
a: Property<number>?,
b: Property<number>?,
c: Property<number>?,
context: Context?, -- Store context for use in update()
}
local function clamp(value: number, minValue: number, maxValue: number): number
return math.max(minValue, math.min(maxValue, value))
end
local function rebuild(self: BarChart)
local values = {
self.a and self.a.value or 0,
self.b and self.b.value or 0,
self.c and self.c.value or 0,
}
for i, value in ipairs(values) do
local height = (clamp(value, 0, 100) / 100) * self.maxHeight
local x = (i - 2) * (self.barWidth + 20)
local halfW = self.barWidth / 2
local path = self.paths[i]
path:reset()
path:moveTo(Vector.xy(x - halfW, 0))
path:lineTo(Vector.xy(x + halfW, 0))
path:lineTo(Vector.xy(x + halfW, -height))
path:lineTo(Vector.xy(x - halfW, -height))
path:close()
end
end
function init(self: BarChart, context: Context): boolean
self.context = context -- Store for use in update()
self.paths = { Path.new(), Path.new(), Path.new() }
self.paints = {
Paint.with({ style = "fill", color = Color.rgb(255, 120, 90) }),
Paint.with({ style = "fill", color = Color.rgb(90, 180, 255) }),
Paint.with({ style = "fill", color = Color.rgb(120, 220, 140) }),
}
local vm = context:viewModel()
if vm then
self.a = vm:getNumber("a")
self.b = vm:getNumber("b")
self.c = vm:getNumber("c")
end
local function bind(prop: Property<number>?)
if prop then
prop:addListener(function()
rebuild(self)
context:markNeedsUpdate()
end)
end
end
bind(self.a)
bind(self.b)
bind(self.c)
rebuild(self)
return true
end
function update(self: BarChart)
rebuild(self)
if self.context then
self.context:markNeedsUpdate()
end
end
function draw(self: BarChart, renderer: Renderer)
for i = 1, #self.paths do
renderer:drawPath(self.paths[i], self.paints[i])
end
end
return function(): Node<BarChart>
return {
init = init,
update = update,
draw = draw,
barWidth = 50,
maxHeight = 140,
paths = {},
paints = {},
a = nil,
b = nil,
c = nil,
}
end
How It Works
Data Binding
The chart binds to three ViewModel number properties (a, b, c). Each property represents a bar's value from 0-100.
Reactive Updates
Property listeners automatically rebuild the chart when ViewModel values change:
prop:addListener(function()
rebuild(self)
context:markNeedsUpdate()
end)
Bar Calculation
Each bar's height is calculated as a percentage of maxHeight:
local height = (clamp(value, 0, 100) / 100) * self.maxHeight
Bars are positioned horizontally using the index to calculate x-offset.
Visual Design
Each bar has its own color from the paints array, giving visual distinction to the data series.
Extension Ideas
- Add labels with text components bound to the same ViewModel
- Animate bars in
advancefor easing transitions - Use gradients instead of flat colors
- Add a baseline and axis labels
- Support dynamic number of bars with a ViewModel list
- Add tooltips that show values on hover
Next Steps
- Continue to Catch the Stars
- Need a refresher? Review Quick Reference