From 49a5e4bfde087b37e9d2fc7f59094ca32f554d55 Mon Sep 17 00:00:00 2001 From: Robert Jelic <36573031+NoryiE@users.noreply.github.com> Date: Tue, 4 Mar 2025 01:53:14 +0100 Subject: [PATCH] Finished Graph Elements - Added Graph - Added BarChart - Added LineChart --- src/elements/BarChart.lua | 78 +++++++++++++++++ src/elements/Graph.lua | 174 ++++++++++++++++++++++++++++++++----- src/elements/LineChart.lua | 80 +++++++++++++++++ 3 files changed, 308 insertions(+), 24 deletions(-) create mode 100644 src/elements/BarChart.lua create mode 100644 src/elements/LineChart.lua diff --git a/src/elements/BarChart.lua b/src/elements/BarChart.lua new file mode 100644 index 0000000..d9eddee --- /dev/null +++ b/src/elements/BarChart.lua @@ -0,0 +1,78 @@ +local elementManager = require("elementManager") +local VisualElement = elementManager.getElement("VisualElement") +local BaseGraph = elementManager.getElement("Graph") +local tHex = require("libraries/colorHex") +--- @configDescription A bar chart element based on the graph element +---@configDefault false + +--- This is the bar chart class. It is based on the graph element. It draws bar based points. +--- @class BarChart : Graph +local BarChart = setmetatable({}, BaseGraph) +BarChart.__index = BarChart + +--- Creates a new BarChart instance +--- @shortDescription Creates a new BarChart instance +--- @return BarChart self The newly created BarChart instance +--- @private +function BarChart.new() + local self = setmetatable({}, BarChart):__init() + return self +end + +--- @shortDescription Initializes the BarChart instance +--- @param props table The properties to initialize the element with +--- @param basalt table The basalt instance +--- @return BarChart self The initialized instance +--- @protected +function BarChart:init(props, basalt) + BaseGraph.init(self, props, basalt) + self.set("type", "BarChart") + return self +end + +--- @shortDescription Renders the BarChart +--- @protected +function BarChart:render() + VisualElement.render(self) + + local width = self.get("width") + local height = self.get("height") + local minVal = self.get("minValue") + local maxVal = self.get("maxValue") + local series = self.get("series") + + local activeSeriesCount = 0 + local seriesList = {} + for _, s in pairs(series) do + if(s.visible)then + if #s.data > 0 then + activeSeriesCount = activeSeriesCount + 1 + table.insert(seriesList, s) + end + end + end + + local barGroupWidth = activeSeriesCount + local spacing = 1 + local totalGroups = math.min(seriesList[1] and seriesList[1].pointCount or 0, math.floor((width + spacing) / (barGroupWidth + spacing))) + + for groupIndex = 1, totalGroups do + local groupX = ((groupIndex-1) * (barGroupWidth + spacing)) + 1 + + for seriesIndex, s in ipairs(seriesList) do + local value = s.data[groupIndex] + if value then + local x = groupX + (seriesIndex - 1) + local normalizedValue = (value - minVal) / (maxVal - minVal) + local y = math.floor(height - (normalizedValue * (height-1))) + y = math.max(1, math.min(y, height)) + + for barY = y, height do + self:blit(x, barY, s.symbol, tHex[s.fgColor], tHex[s.bgColor]) + end + end + end + end +end + +return BarChart diff --git a/src/elements/Graph.lua b/src/elements/Graph.lua index 2952678..e7ef09c 100644 --- a/src/elements/Graph.lua +++ b/src/elements/Graph.lua @@ -1,62 +1,188 @@ local elementManager = require("elementManager") -local VisualElement = elementManager.getElement("elements/VisualElement") ----@configDescription +local VisualElement = elementManager.getElement("VisualElement") +local tHex = require("libraries/colorHex") +---@configDescription A point based graph element ---@configDefault false ----@class Graph : VisualElement +--- This is the base class for all graph elements. It is a point based graph. +--- @class Graph : VisualElement local Graph = setmetatable({}, VisualElement) Graph.__index = Graph -Graph.defineProperty(Graph, "data", {default = {}, type = "table", canTriggerRender = true}) +---@property minValue number 0 The minimum value of the graph Graph.defineProperty(Graph, "minValue", {default = 0, type = "number", canTriggerRender = true}) +---@property maxValue number 100 The maximum value of the graph Graph.defineProperty(Graph, "maxValue", {default = 100, type = "number", canTriggerRender = true}) -Graph.defineProperty(Graph, "graphColor", {default = colors.yellow, type = "color", canTriggerRender = true}) -Graph.defineProperty(Graph, "graphSymbol", {default = "\127", type = "string", canTriggerRender = true}) -- Default: "|" +---@property series table {} The series of the graph +Graph.defineProperty(Graph, "series", {default = {}, type = "table", canTriggerRender = true}) +--- Creates a new Graph instance +--- @shortDescription Creates a new Graph instance +--- @return Graph self The newly created Graph instance +--- @private function Graph.new() local self = setmetatable({}, Graph):__init() return self end +--- @shortDescription Initializes the Graph instance +--- @param props table The properties to initialize the element with +--- @param basalt table The basalt instance +--- @return Graph self The initialized instance +--- @protected function Graph:init(props, basalt) VisualElement.init(self, props, basalt) self.set("type", "Graph") + self.set("width", 20) + self.set("height", 10) return self end -function Graph:setPoint(index, value) - local data = self.get("data") - data[index] = value +--- @shortDescription Adds a series to the graph +--- @param name string The name of the series +--- @param symbol string The symbol of the series +--- @param bgCol number The background color of the series +--- @param fgCol number The foreground color of the series +--- @param pointCount number The number of points in the series +function Graph:addSeries(name, symbol, bgCol, fgCol, pointCount) + local series = self.get("series") + table.insert(series, { + name = name, + symbol = symbol or " ", + bgColor = bgCol or colors.white, + fgColor = fgCol or colors.black, + pointCount = pointCount or self.get("width"), + data = {}, + visible = true + }) self:updateRender() + return self end -function Graph:addPoint(value) - local data = self.get("data") - table.insert(data, value) - while #data > self.get("width") do - table.remove(data, 1) +--- @shortDescription Removes a series from the graph +--- @param name string The name of the series +--- @return Graph self The graph instance +function Graph:removeSeries(name) + local series = self.get("series") + for i, s in ipairs(series) do + if s.name == name then + table.remove(series, i) + break + end end self:updateRender() + return self end +--- @shortDescription Gets a series from the graph +--- @param name string The name of the series +--- @return table? series The series +function Graph:getSeries(name) + local series = self.get("series") + for _, s in ipairs(series) do + if s.name == name then + return s + end + end + return nil +end + +--- @shortDescription Changes the visibility of a series +--- @param name string The name of the series +--- @param visible boolean Whether the series should be visible +--- @return Graph self The graph instance +function Graph:changeSeriesVisibility(name, visible) + local series = self.get("series") + for _, s in ipairs(series) do + if s.name == name then + s.visible = visible + break + end + end + self:updateRender() + return self +end + +--- @shortDescription Adds a point to a series +--- @param name string The name of the series +--- @param value number The value of the point +--- @return Graph self The graph instance +function Graph:addPoint(name, value) + local series = self.get("series") + + for _, s in ipairs(series) do + if s.name == name then + table.insert(s.data, value) + while #s.data > s.pointCount do + table.remove(s.data, 1) + end + break + end + end + self:updateRender() + return self +end + +--- @shortDescription Focuses a series +--- @param name string The name of the series +--- @return Graph self The graph instance +function Graph:focusSeries(name) + local series = self.get("series") + for index, s in ipairs(series) do + if s.name == name then + table.remove(series, index) + table.insert(series, s) + break + end + end + self:updateRender() + return self +end + +--- @shortDescription Sets the point count of a series +--- @param name string The name of the series +--- @param count number The number of points in the series +--- @return Graph self The graph instance +function Graph:setSeriesPointCount(name, count) + local series = self.get("series") + for _, s in ipairs(series) do + if s.name == name then + s.pointCount = count + while #s.data > count do + table.remove(s.data, 1) + end + break + end + end + self:updateRender() + return self +end + +--- @shortDescription Renders the graph +--- @protected function Graph:render() VisualElement.render(self) - - local data = self.get("data") + local width = self.get("width") local height = self.get("height") local minVal = self.get("minValue") local maxVal = self.get("maxValue") - local symbol = self.get("graphSymbol") - local graphColor = self.get("graphColor") + local series = self.get("series") - for x = 1, width do - if data[x] then - local normalizedValue = (data[x] - minVal) / (maxVal - minVal) - local y = math.floor(height - (normalizedValue * (height-1))) - y = math.max(1, math.min(y, height)) + for _, s in pairs(series) do + if(s.visible)then + local dataCount = #s.data + local spacing = (width - 1) / math.max((dataCount - 1), 1) - self:textFg(x, y, symbol, graphColor) + for i, value in ipairs(s.data) do + local x = math.floor(((i-1) * spacing) + 1) + + local normalizedValue = (value - minVal) / (maxVal - minVal) + local y = math.floor(height - (normalizedValue * (height-1))) + y = math.max(1, math.min(y, height)) + + self:blit(x, y, s.symbol, tHex[s.bgColor], tHex[s.fgColor]) + end end end end diff --git a/src/elements/LineChart.lua b/src/elements/LineChart.lua new file mode 100644 index 0000000..c1d9d86 --- /dev/null +++ b/src/elements/LineChart.lua @@ -0,0 +1,80 @@ +local elementManager = require("elementManager") +local VisualElement = elementManager.getElement("VisualElement") +local Graph = elementManager.getElement("Graph") +local tHex = require("libraries/colorHex") +--- @configDescription A line chart element based on the graph element +---@configDefault false + +--- This is the line chart class. It is based on the graph element. It draws lines between points. +--- @class LineChart : Graph +local LineChart = setmetatable({}, Graph) +LineChart.__index = LineChart + +--- Creates a new LineChart instance +--- @shortDescription Creates a new LineChart instance +--- @return LineChart self The newly created LineChart instance +--- @private +function LineChart.new() + local self = setmetatable({}, LineChart):__init() + return self +end + +--- @shortDescription Initializes the LineChart instance +--- @param props table The properties to initialize the element with +--- @param basalt table The basalt instance +--- @return LineChart self The initialized instance +--- @protected +function LineChart:init(props, basalt) + Graph.init(self, props, basalt) + self.set("type", "LineChart") + return self +end + +local function drawLine(self, x1, y1, x2, y2, symbol, bgColor, fgColor) + local dx = x2 - x1 + local dy = y2 - y1 + local steps = math.max(math.abs(dx), math.abs(dy)) + + for i = 0, steps do + local t = steps == 0 and 0 or i / steps + local x = math.floor(x1 + dx * t) + local y = math.floor(y1 + dy * t) + if x >= 1 and x <= self.get("width") and y >= 1 and y <= self.get("height") then + self:blit(x, y, symbol, tHex[bgColor], tHex[fgColor]) + end + end +end + +--- @shortDescription Renders the LineChart +--- @protected +function LineChart:render() + VisualElement.render(self) + + local width = self.get("width") + local height = self.get("height") + local minVal = self.get("minValue") + local maxVal = self.get("maxValue") + local series = self.get("series") + + for _, s in pairs(series) do + if(s.visible)then + local lastX, lastY + local dataCount = #s.data + local spacing = (width - 1) / math.max((dataCount - 1), 1) + + for i, value in ipairs(s.data) do + local x = math.floor(((i-1) * spacing) + 1) + local normalizedValue = (value - minVal) / (maxVal - minVal) + local y = math.floor(height - (normalizedValue * (height-1))) + y = math.max(1, math.min(y, height)) + + if lastX then + drawLine(self, lastX, lastY, x, y, s.symbol, s.bgColor, s.fgColor) + end + lastX, lastY = x, y + end + end + end +end + +return LineChart