From ef93d815911f21f4f7e7445eee9e3c6f87793c82 Mon Sep 17 00:00:00 2001 From: cyberbit Date: Thu, 28 Dec 2023 21:25:26 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A4=96=20update=20local=20build=20con?= =?UTF-8?q?fig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/tasks.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ce0e102..08af515 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,6 +9,8 @@ "command": "./build.sh", "presentation": { "close": true, + "focus": false, + "reveal": "silent", "panel": "shared" } } From 1c652b328d795b1c70cad11aa77e426826f75249 Mon Sep 17 00:00:00 2001 From: cyberbit Date: Thu, 28 Dec 2023 21:43:29 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=A6=20line=20chart=20adapter=20for?= =?UTF-8?q?=20#35?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 1 + src/telem/lib/output.lua | 5 + .../output/plotter/ChartLineOutputAdapter.lua | 139 +++ src/telem/lib/util.lua | 14 + src/telem/vendor/LICENSE-Plotter.txt | 21 + src/telem/vendor/init.lua | 6 +- src/telem/vendor/plotter.lua | 790 ++++++++++++++++++ 7 files changed, 974 insertions(+), 2 deletions(-) create mode 100644 src/telem/lib/output/plotter/ChartLineOutputAdapter.lua create mode 100644 src/telem/vendor/LICENSE-Plotter.txt create mode 100644 src/telem/vendor/plotter.lua diff --git a/LICENSE b/LICENSE index 9695664..1976bce 100644 --- a/LICENSE +++ b/LICENSE @@ -25,3 +25,4 @@ Portions of this software are copyright of their respective authors and released - ECNet2, Copyright 2020 Miguel Oliveira. For licensing see src/telem/vendor/LICENSE-ECNet2.txt - CCryptoLib, Copyright 2023 Miguel Oliveira. For licensing see src/telem/vendor/LICENSE-CCryptoLib.txt - RedRun, By JackMacWindows. For licensing see src/telem/vendor/redrun.lua + - Plotter, Copyright 2023 Daniel Marcolesco. For licensing see src/telem/vendor/LICENSE-Plotter.txt \ No newline at end of file diff --git a/src/telem/lib/output.lua b/src/telem/lib/output.lua index 4f10a6c..93558bd 100644 --- a/src/telem/lib/output.lua +++ b/src/telem/lib/output.lua @@ -10,6 +10,11 @@ return { label = require 'telem.lib.output.basalt.LabelOutputAdapter', }, + -- Plotter + plotter = { + line = require 'telem.lib.output.plotter.ChartLineOutputAdapter', + }, + -- Modem secureModem = require 'telem.lib.output.SecureModemOutputAdapter' } \ No newline at end of file diff --git a/src/telem/lib/output/plotter/ChartLineOutputAdapter.lua b/src/telem/lib/output/plotter/ChartLineOutputAdapter.lua new file mode 100644 index 0000000..73b84b9 --- /dev/null +++ b/src/telem/lib/output/plotter/ChartLineOutputAdapter.lua @@ -0,0 +1,139 @@ +local o = require 'telem.lib.ObjectModel' +local t = require 'telem.lib.util' +local vendor +local plotterFactory + +local OutputAdapter = require 'telem.lib.OutputAdapter' +local MetricCollection = require 'telem.lib.MetricCollection' + +local ChartLineOutputAdapter = o.class(OutputAdapter) +ChartLineOutputAdapter.type = 'ChartLineOutputAdapter' + +ChartLineOutputAdapter.MAX_ENTRIES = 50 +ChartLineOutputAdapter.X_TICK = 10 + +function ChartLineOutputAdapter:constructor (win, filter, bg, fg) + self:super('constructor') + + self.win = assert(win, 'Window is required') + self.filter = assert(filter, 'Filter is required') + + self.plotter = nil + self.plotData = {} + self.gridOffsetX = 0 + + self.filter = filter + + self.bg = bg or win.getBackgroundColor() or colors.black + self.fg = fg or win.getTextColor() or colors.white + + self:register() +end + +function ChartLineOutputAdapter:register () + if not vendor then + self:dlog('ChartLineOutputAdapter:boot :: Loading vendor modules...') + + vendor = require 'telem.vendor' + + self:dlog('ChartLineOutputAdapter:boot :: Vendor modules ready.') + end + + if not plotterFactory then + self:dlog('ChartLineOutputAdapter:boot :: Loading plotter...') + + plotterFactory = vendor.plotter + + self:dlog('ChartLineOutputAdapter:boot :: plotter ready.') + end + + self.plotter = plotterFactory(self.win) + + for i = 1, self.MAX_ENTRIES do + t.constrainAppend(self.plotData, self.plotter.NAN, self.MAX_ENTRIES) + end +end + +function ChartLineOutputAdapter:write (collection) + assert(o.instanceof(collection, MetricCollection), 'Collection must be a MetricCollection') + + local resultMetric = collection:find(self.filter) + + assert(resultMetric, 'could not find metric') + + -- TODO data width setting + self.gridOffsetX = self.gridOffsetX - t.constrainAppend(self.plotData, resultMetric and resultMetric.value or self.plotter.NAN, self.MAX_ENTRIES) + + -- TODO X_TICK setting + if self.gridOffsetX % self.X_TICK == 0 then + self.gridOffsetX = 0 + end + + local dataw = #{self.plotData} + + local actualmin, actualmax = math.huge, -math.huge + + for _, v in ipairs(self.plotData) do + -- skip NAN + if v ~= self.plotter.NAN then + if v < actualmin then actualmin = v end + if v > actualmax then actualmax = v end + end + end + + local flatlabel = nil + + -- NaN data + if actualmin == math.huge then + flatlabel = 'NaN' + + actualmin, actualmax = 0, 0 + end + + -- flat data + if actualmin == actualmax then + local minrange = 0.000001 + + if not flatlabel then + flatlabel = tostring(actualmin) + end + + actualmin = actualmin - minrange / 2 + actualmax = actualmax + minrange / 2 + end + + self.plotter:clear(self.bg) + + self.plotter:chartGrid(self.MAX_ENTRIES, actualmin, actualmax, self.gridOffsetX, colors.gray, { + xGap = 10, + yLinesMin = 5, -- yLinesMin: number >= 1 + yLinesFactor = 2 -- yLinesFactor: integer >= 2 + -- effective max density = yMinDensity * yBasis + }) + + self.plotter:chartLine(self.plotData, self.MAX_ENTRIES, actualmin, actualmax, self.fg) + + local maxString = tostring(actualmax) + local minString = tostring(actualmin) + + self.win.setVisible(false) + + self.plotter:render() + + if not flatlabel then + self.win.setCursorPos(self.plotter.box.term_width - #maxString + 1, 1) + self.win.write(actualmax) + + self.win.setCursorPos(self.plotter.box.term_width - #minString + 1, self.plotter.box.term_height) + self.win.write(actualmin) + else + self.win.setCursorPos(self.plotter.box.term_width - #flatlabel + 1, self.plotter.math.round(self.plotter.box.term_height / 2)) + self.win.write(flatlabel) + end + + self.win.setVisible(true) + + return self +end + +return ChartLineOutputAdapter \ No newline at end of file diff --git a/src/telem/lib/util.lua b/src/telem/lib/util.lua index 310833c..9cb48e3 100644 --- a/src/telem/lib/util.lua +++ b/src/telem/lib/util.lua @@ -61,6 +61,19 @@ local function shortnum(n) end end +local function constrainAppend (data, value, width) + local removed = 0 + + table.insert(data, value) + + while #data > width do + table.remove(data, 1) + removed = removed + 1 + end + + return removed +end + return { log = log, err = err, @@ -68,4 +81,5 @@ return { skpairs = skpairs, sleep = os.sleep or tsleep, shortnum = shortnum, + constrainAppend = constrainAppend, } \ No newline at end of file diff --git a/src/telem/vendor/LICENSE-Plotter.txt b/src/telem/vendor/LICENSE-Plotter.txt new file mode 100644 index 0000000..74a99ca --- /dev/null +++ b/src/telem/vendor/LICENSE-Plotter.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Daniel Marcolesco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/telem/vendor/init.lua b/src/telem/vendor/init.lua index c14b8f6..0ed5371 100644 --- a/src/telem/vendor/init.lua +++ b/src/telem/vendor/init.lua @@ -2,10 +2,12 @@ if package.path:find('telem/vendor') == nil then package.path = package.path .. local ecnet2 = require 'ecnet2' local random = require 'ccryptolib.random' +local plotter = require 'plotter' return { - ecnet2 = ecnet2, ccryptolib = { random = random - } + }, + ecnet2 = ecnet2, + plotter = plotter } \ No newline at end of file diff --git a/src/telem/vendor/plotter.lua b/src/telem/vendor/plotter.lua new file mode 100644 index 0000000..129bf08 --- /dev/null +++ b/src/telem/vendor/plotter.lua @@ -0,0 +1,790 @@ +-- Plotter by cyberbit +-- MIT License +-- Version 0.0.3 + +local pixelbox2 = (function () + -- Pixelbox Lite v2 by 9551-Dev + -- (v1: https://github.com/9551-Dev/apis/blob/main/pixelbox_lite.lua) + + local pixelbox = {} + local box_object = {} + + local t_cat = table.concat + + -- local sampling_lookup = { + -- {2,3,4,5,6}, + -- {4,1,6,3,5}, + -- {1,4,5,2,6}, + -- {2,6,3,5,1}, + -- {3,6,1,4,2}, + -- {4,5,2,3,1} + -- } + + local sampling_lookup = { + {3,5,2,4,6}, + {4,6,1,3,5}, + {1,5,2,4,6}, + {2,6,1,3,5}, + {1,3,6,4,2}, + {4,2,5,3,1} + } + + local texel_character_lookup = {} + local texel_foreground_lookup = {} + local texel_background_lookup = {} + local to_blit = {} + + local factorial_1 = 1 + local factorial_2 = factorial_1 * 2 + local factorial_3 = factorial_2 * 3 + local factorial_4 = factorial_3 * 4 + local factorial_5 = factorial_4 * 5 + + local function generate_identifier(s1,s2,s3,s4,s5,s6) + return s2 * factorial_1 + + s3 * factorial_2 + + s4 * factorial_3 + + s5 * factorial_4 + + s6 * factorial_5 + end + + local function calculate_texel(v1,v2,v3,v4,v5,v6) + local texel_data = {v1,v2,v3,v4,v5,v6} + + local state_lookup = {} + for i=1,6 do + local subpixel_state = texel_data[i] + local current_count = state_lookup[subpixel_state] + + state_lookup[subpixel_state] = current_count and current_count + 1 or 1 + end + + local sortable_states = {} + for k,v in pairs(state_lookup) do + sortable_states[#sortable_states+1] = { + value = k, + count = v + } + end + + table.sort(sortable_states,function(a,b) + return a.count > b.count + end) + + local texel_stream = {} + for i=1,6 do + local subpixel_state = texel_data[i] + + if subpixel_state == sortable_states[1].value then + texel_stream[i] = 1 + elseif subpixel_state == sortable_states[2].value then + texel_stream[i] = 0 + else + local sample_points = sampling_lookup[i] + for sample_index=1,5 do + local sample_subpixel_index = sample_points[sample_index] + local sample_state = texel_data [sample_subpixel_index] + + local common_state_1 = sample_state == sortable_states[1].value + local common_state_2 = sample_state == sortable_states[2].value + + if common_state_1 or common_state_2 then + texel_stream[i] = common_state_1 and 1 or 0 + + break + end + end + end + end + + local char_num = 128 + local stream_6 = texel_stream[6] + if texel_stream[1] ~= stream_6 then char_num = char_num + 1 end + if texel_stream[2] ~= stream_6 then char_num = char_num + 2 end + if texel_stream[3] ~= stream_6 then char_num = char_num + 4 end + if texel_stream[4] ~= stream_6 then char_num = char_num + 8 end + if texel_stream[5] ~= stream_6 then char_num = char_num + 16 end + + local state_1,state_2 + if #sortable_states > 1 then + state_1 = sortable_states[ stream_6+1].value + state_2 = sortable_states[2-stream_6 ].value + else + state_1 = sortable_states[1].value + state_2 = sortable_states[1].value + end + + return char_num,state_1,state_2 + end + + local function base_n_rshift(n,base,shift) + return math.floor(n/(base^shift)) + end + + local real_entries = 0 + local function generate_lookups() + for i = 0, 15 do + to_blit[2^i] = ("%x"):format(i) + end + + for encoded_pattern=0,6^6 do + local subtexel_1 = base_n_rshift(encoded_pattern,6,0) % 6 + local subtexel_2 = base_n_rshift(encoded_pattern,6,1) % 6 + local subtexel_3 = base_n_rshift(encoded_pattern,6,2) % 6 + local subtexel_4 = base_n_rshift(encoded_pattern,6,3) % 6 + local subtexel_5 = base_n_rshift(encoded_pattern,6,4) % 6 + local subtexel_6 = base_n_rshift(encoded_pattern,6,5) % 6 + + local pattern_lookup = {} + pattern_lookup[subtexel_6] = 5 + pattern_lookup[subtexel_5] = 4 + pattern_lookup[subtexel_4] = 3 + pattern_lookup[subtexel_3] = 2 + pattern_lookup[subtexel_2] = 1 + pattern_lookup[subtexel_1] = 0 + + local pattern_identifier = generate_identifier( + pattern_lookup[subtexel_1],pattern_lookup[subtexel_2], + pattern_lookup[subtexel_3],pattern_lookup[subtexel_4], + pattern_lookup[subtexel_5],pattern_lookup[subtexel_6] + ) + + if not texel_character_lookup[pattern_identifier] then + real_entries = real_entries + 1 + local character,sub_state_1,sub_state_2 = calculate_texel( + subtexel_1,subtexel_2, + subtexel_3,subtexel_4, + subtexel_5,subtexel_6 + ) + + local color_1_location = pattern_lookup[sub_state_1] + 1 + local color_2_location = pattern_lookup[sub_state_2] + 1 + + texel_foreground_lookup[pattern_identifier] = color_1_location + texel_background_lookup[pattern_identifier] = color_2_location + + texel_character_lookup[pattern_identifier] = string.char(character) + end + end + end + + function pixelbox.restore(box,color,keep_existing) + if not keep_existing then + local new_canvas = {} + + for y=1,box.height do + for x=1,box.width do + if not new_canvas[y] then new_canvas[y] = {} end + new_canvas[y][x] = color + end + end + + box.canvas = new_canvas + else + local canvas = box.canvas + + for y=1,box.height do + for x=1,box.width do + if not bc[y] then bc[y] = {} end + + if not canvas[y][x] then + canvas[y][x] = color + end + end + end + end + end + + local color_lookup = {} + local texel_body = {0,0,0,0,0,0} + function box_object:render() + local t = self.term + local blit_line,set_cursor = t.blit,t.setCursorPos + + local canv = self.canvas + + local char_line,fg_line,bg_line = {},{},{} + + local width,height = self.width,self.height + + local sy = 0 + for y=1,height,3 do + sy = sy + 1 + local layer_1 = canv[y] + local layer_2 = canv[y+1] + local layer_3 = canv[y+2] + + local n = 0 + for x=1,width,2 do + local xp1 = x+1 + local b1,b2,b3,b4,b5,b6 = + layer_1[x],layer_1[xp1], + layer_2[x],layer_2[xp1], + layer_3[x],layer_3[xp1] + + local char,fg,bg = " ",1,b1 + + local single_color = b2 == b1 + and b3 == b1 + and b4 == b1 + and b5 == b1 + and b6 == b1 + + if not single_color then + color_lookup[b6] = 5 + color_lookup[b5] = 4 + color_lookup[b4] = 3 + color_lookup[b3] = 2 + color_lookup[b2] = 1 + color_lookup[b1] = 0 + + local pattern_identifier = + color_lookup[b2] + + color_lookup[b3] * 2 + + color_lookup[b4] * 6 + + color_lookup[b5] * 24 + + color_lookup[b6] * 120 + + local fg_location = texel_foreground_lookup[pattern_identifier] + local bg_location = texel_background_lookup[pattern_identifier] + + texel_body[1] = b1 + texel_body[2] = b2 + texel_body[3] = b3 + texel_body[4] = b4 + texel_body[5] = b5 + texel_body[6] = b6 + + fg = texel_body[fg_location] + bg = texel_body[bg_location] + + char = texel_character_lookup[pattern_identifier] + end + + n = n + 1 + char_line[n] = char + fg_line [n] = to_blit[fg] + bg_line [n] = to_blit[bg] + end + + set_cursor(1,sy) + blit_line( + t_cat(char_line,""), + t_cat(fg_line, ""), + t_cat(bg_line, "") + ) + end + end + + function box_object:clear(color) + pixelbox.restore(self,color) + end + + function box_object:set_pixel(x,y,color) + -- print('set_pixel',x,y,color) + self.canvas[y][x] = color + end + + function box_object:resize(w,h,color) + self.term_width = w + self.term_height = h + self.width = w*2 + self.height = h*3 + + pixelbox.restore(self,color or self.background,true) + end + + local first_run = true + + function pixelbox.new(terminal,bg) + local box = {} + + box.background = bg or terminal.getBackgroundColor() or colors.black + + local w,h = terminal.getSize() + box.term = terminal + + setmetatable(box,{__index = box_object}) + + box.term_width = w + box.term_height = h + box.width = w*2 + box.height = h*3 + + pixelbox.restore(box,box.background) + + if first_run then + generate_lookups() + + first_run = false + end + + return box + end + + return pixelbox +end)() + +local Plotter = setmetatable({ _VERSION = '0.0.3' }, { + __call = function (class, ...) + local object = setmetatable({}, class) + + class.constructor(object, ...) + + return object + end +}) +Plotter.__index = Plotter + +-- constants +Plotter.NAN = '_NAN' +Plotter.RANGE_MIN = 0.000001 + +Plotter.math = { + --- round a number to the nearest integer + --- @param x number + --- @return integer + round = function (x) + return math.floor(x + 0.5) + end, + + --- scale s from smin and smax to tmin and tmax + --- something t + --- (via https://stats.stackexchange.com/a/281164) + --- @param s number + --- @param smin number + --- @param smax number + --- @param tmin number + --- @param tmax number + --- @return number + scale = function (s, smin, smax, tmin, tmax) + return ((s - smin) / (smax - smin)) * (tmax - tmin) + tmin + end +} + +--- make a new plotter, filling the provided window +--- @param win window +function Plotter:constructor(win) + self.box = pixelbox2.new(win) +end + +--- draw a line using canvas coordinates. OOB pixels will be processed, but not drawn. +---@param x1 integer start x +---@param y1 integer start y +---@param x2 integer end x +---@param y2 integer end y +---@param color colors drawing color +function Plotter:drawLine(x1, y1, x2, y2, color) + self:drawLineSometimes(x1, y1, x2, y2, color, 1, 0) +end + +--- draw a line using canvas coordinates, with specified on/off pattern. OOB pixels +--- will be processed, but not drawn. returns next pattern offset +---@param x1 integer start x +---@param y1 integer start y +---@param x2 integer end x +---@param y2 integer end y +---@param color colors drawing color +---@param onrate integer consecutive on pixels +---@param offrate? integer consecutive off pixels, defaults to onrate +---@param oncount? integer offset for on pixels, defaults to 0 +---@param offcount? integer offset for off pixels, defaults to 0 +---@return integer oncount next oncount +---@return integer offcount next offcount +function Plotter:drawLineSometimes(x1, y1, x2, y2, color, onrate, offrate, oncount, offcount) + if not offrate then offrate = onrate end + + if not oncount then oncount = 0 end + if not offcount then offcount = 0 end + + local dx = math.abs(x2 - x1) + local dy = -math.abs(y2 - y1) + local sx = x1 < x2 and 1 or -1 + local sy = y1 < y2 and 1 or -1 + local err = dx + dy + + while true do + -- don't draw OOB, but continue processing the line + if x1 < 1 or y1 < 1 or x1 > self.box.width or y1 > self.box.height then + -- no draw + else + if onrate > 0 and offrate == 0 then + self.box:set_pixel(x1, y1, color) + else + if oncount < onrate then + self.box:set_pixel(x1, y1, color) + + oncount = oncount + 1 + elseif offcount < offrate then + -- skip pixel + offcount = offcount + 1 + else + self.box:set_pixel(x1, y1, color) + + oncount = 1 + offcount = 0 + end + end + end + + if x1 == x2 and y1 == y2 then + break + end + + local e2 = 2 * err + + if e2 > dy then + err = err + dy + x1 = x1 + sx + end + + if e2 < dx then + err = err + dx + y1 = y1 + sy + end + end + + return oncount, offcount +end + +--- draw a line using canvas coordinates, filling the vertical space +--- between the line and a baseline +---@param x1 integer start x +---@param y1 integer start y +---@param x2 integer end x +---@param y2 integer end y +---@param yfrom integer y level for area baseline. the space between this y level and the drawn line will be filled +---@param color colors drawing color +function Plotter:drawAreaLine(x1, y1, x2, y2, yfrom, color) + self:drawAreaLineSometimes(x1, y1, x2, y2, yfrom, color, 1, 0) +end + +--- draw a line using canvas coordinates, filling the vertical space +--- between the line and a baseline, with specified on/off pattern. +---@param x1 integer start x +---@param y1 integer start y +---@param x2 integer end x +---@param y2 integer end y +---@param yfrom integer y level for area baseline. the space between yfrom and the drawn line will be filled +---@param color colors drawing color +---@param onrate integer consecutive on pixels +---@param offrate? integer consecutive off pixels, defaults to onrate +---@param oncount? integer offset for on pixels, defaults to 0 +---@param offcount? integer offset for off pixels, defaults to 0 +---@return integer oncount next oncount +---@return integer offcount next offcount +function Plotter:drawAreaLineSometimes(x1, y1, x2, y2, yfrom, color, onrate, offrate, oncount, offcount) + if not offrate then offrate = onrate end + + if not oncount then oncount = 0 end + if not offcount then offcount = 0 end + + local dx = math.abs(x2 - x1) + local dy = -math.abs(y2 - y1) + local sx = x1 < x2 and 1 or -1 + local sy = y1 < y2 and 1 or -1 + local err = dx + dy + + while true do + if onrate > 0 and offrate == 0 then + self:drawLineSometimes(x1, y1, x1, yfrom, color, onrate, offrate) + else + if oncount < onrate then + self:drawLineSometimes(x1, y1, x1, yfrom, color, onrate, offrate) + + oncount = oncount + 1 + elseif offcount < offrate then + -- skip pixel + offcount = offcount + 1 + else + self:drawLineSometimes(x1, y1, x1, yfrom, color, onrate, offrate) + + oncount = 1 + offcount = 0 + end + end + + if x1 == x2 and y1 == y2 then + break + end + + local e2 = 2 * err + + if e2 > dy then + err = err + dy + x1 = x1 + sx + end + + if e2 < dx then + err = err + dx + y1 = y1 + sy + end + end + + return oncount, offcount +end + +--- draw a box using canvas coordinates +---@param x1 integer start x +---@param y1 integer start y +---@param x2 integer end x +---@param y2 integer end y +---@param color colors drawing color +function Plotter:drawBox(x1, y1, x2, y2, color) + self:drawBoxSometimes(x1, y1, x2, y2, color, 1, 0) +end + +--- draw a box using canvas coordinates, with specified on/off pattern. +--- returns next pattern offset. lines are drawn in a circular fashion +---@param x1 integer start x +---@param y1 integer start y +---@param x2 integer end x +---@param y2 integer end y +---@param color colors drawing color +---@param onrate integer consecutive on pixels +---@param offrate? integer consecutive off pixels, defaults to onrate +---@param oncount? integer offset for on pixels, defaults to 0 +---@param offcount? integer offset for off pixels, defaults to 0 +function Plotter:drawBoxSometimes(x1, y1, x2, y2, color, onrate, offrate, oncount, offcount) + if not offrate then offrate = onrate end + + if not oncount then oncount = 0 end + if not offcount then offcount = 0 end + + oncount, offcount = self:drawLineSometimes(x1, y1, x2 - 1, y1, color, onrate, offrate, oncount, offcount) + oncount, offcount = self:drawLineSometimes(x2, y1, x2, y2 - 1, color, onrate, offrate, oncount, offcount) + oncount, offcount = self:drawLineSometimes(x2, y2, x1 + 1, y2, color, onrate, offrate, oncount, offcount) + oncount, offcount = self:drawLineSometimes(x1, y2, x1, y1 + 1, color, onrate, offrate, oncount, offcount) +end + +--- fill a box using canvas coordinates +---@param x1 integer start x +---@param y1 integer start y +---@param x2 integer end x +---@param y2 integer end y +---@param color colors drawing color +function Plotter:fillBox(x1, y1, x2, y2, color) + for x = x1, x2 do + for y = y1, y2 do + self.box:set_pixel(x, y, color) + end + end +end + +--- plot one-dimensional data as a line in scaled coordinates, where +--- the data index is used as the horizontal axis +--- @param data table data to plot. index = x, value = y +--- @param dataw integer number of data indexes to fit in the plot area (left to right) +--- @param miny number minimum of data values to fit in the plot area (bottom) +--- @param maxy number maximum of data values to fit in the plot area (top) +--- @param color colors drawing color +function Plotter:chartLine(data, dataw, miny, maxy, color) + local boxminx = 1 + local boxmaxx = self.box.width + local boxminy = 1 + local boxmaxy = self.box.height + + local lastx, lasty + + for x, y in ipairs(data) do + -- plotter.NAN values are not charted + if y == self.NAN then + lasty = nil + else + local nextx = self.math.round(self.math.scale(x, 1, dataw, boxminx, boxmaxx)) + local nexty = 1 + boxmaxy - self.math.round(self.math.scale(y, miny, maxy, boxminy, boxmaxy)) + + if nextx < boxminx or nexty < boxminy or nextx > boxmaxx or nexty > boxmaxy then + -- do nothing for OOB points + else + if type(lasty) == 'nil' then + self:drawLine(nextx, nexty, nextx, nexty, color) + + lastx = nextx + lasty = nexty + else + self:drawLine(lastx, lasty, nextx, nexty, color) + + lastx = nextx + lasty = nexty + end + end + end + end +end + +--- plot one-dimensional data as a line in scaled coordinates, where +--- the data index is used as the horizontal axis. chart will scale +--- data indexes to fit all provided data. +--- @param data table data to plot. index = x, value = y +--- @param color colors drawing color +function Plotter:chartLineAuto(data, color) + local dataw = #data + + local actualmin, actualmax = math.huge, -math.huge + + for _, v in ipairs(data) do + -- skip NAN + if v ~= self.NAN then + if v < actualmin then actualmin = v end + if v > actualmax then actualmax = v end + end + end + + -- center static data + if actualmin == actualmax then + actualmin = actualmin - self.RANGE_MIN / 2 + actualmax = actualmax + self.RANGE_MIN / 2 + end + + self:chartLine(data, dataw, actualmin, actualmax, color) + + return actualmin, actualmax +end + +--- plot one-dimensional data as an area in scaled coordinates, where +--- the data index is used as the horizontal axis +--- @param data table data to plot. index = x, value = y +--- @param dataw integer number of data indexes to fit in the plot area (left to right) +--- @param miny number minimum of data values to fit in the plot area (bottom) +--- @param maxy number maximum of data values to fit in the plot area (top) +--- @param areay number data value to use as baseline for area fill. larger values will fill towards -y, smaller values will fill towards +y. +--- @param color colors drawing color +function Plotter:chartArea(data, dataw, miny, maxy, areay, color) + local boxminx = 1 + local boxmaxx = self.box.width + local boxminy = 1 + local boxmaxy = self.box.height + + local lastx, lasty + + for x, y in ipairs(data) do + -- plotter.NAN values are not charted + if y == self.NAN then + lasty = nil + else + local nextx = self.math.round(self.math.scale(x, 1, dataw, boxminx, boxmaxx)) + local nexty = 1 + boxmaxy - self.math.round(self.math.scale(y, miny, maxy, boxminy, boxmaxy)) + local nextareay = 1 + boxmaxy - self.math.round(self.math.scale(areay, miny, maxy, boxminy, boxmaxy)) + + if nextareay > boxmaxy then nextareay = boxmaxy end + if nextareay < boxminy then nextareay = boxminy end + + if nextx < boxminx or nexty < boxminy or nextx > boxmaxx or nexty > boxmaxy then + -- do nothing for OOB points + else + if type(lasty) == 'nil' then + self:drawAreaLine(nextx, nexty, nextx, nexty, nextareay, color) + + lastx = nextx + lasty = nexty + else + self:drawAreaLine(lastx, lasty, nextx, nexty, nextareay, color) + + lastx = nextx + lasty = nexty + end + end + end + end +end + +--- plot one-dimensional data as an area in scaled coordinates, where +--- the data index is used as the horizontal axis. chart will scale +--- data indexes to fit all provided data. +--- @param data table data to plot. index = x, value = y +--- @param areay number data value to use as baseline for area fill. larger values will fill towards -y, smaller values will fill towards +y. +--- @param color colors drawing color +function Plotter:chartAreaAuto(data, areay, color) + local dataw = #data + + local actualmin, actualmax = math.huge, -math.huge + + for _, v in ipairs(data) do + -- skip NAN + if v ~= self.NAN then + if v < actualmin then actualmin = v end + if v > actualmax then actualmax = v end + end + end + + -- center static data + if actualmin == actualmax then + actualmin = actualmin - self.RANGE_MIN / 2 + actualmax = actualmax + self.RANGE_MIN / 2 + end + + self:chartArea(data, dataw, actualmin, actualmax, areay, color) + + return actualmin, actualmax +end + +--- plot two-dimensional data as points in scaled coordinates, where +--- the data is a table of {x, y} pairs. +--- @param data table data to plot. at each index: {x, y} +--- @param minx number minimum of data values to fit in the plot area (left) +--- @param maxx number maximum of data values to fit in the plot area (right) +--- @param miny number minimum of data values to fit in the plot area (bottom) +--- @param maxy number maximum of data values to fit in the plot area (top) +--- @param color colors drawing color +function Plotter:chartXY(data, minx, maxx, miny, maxy, color) + local boxminx = 1 + local boxmaxx = self.box.width + local boxminy = 1 + local boxmaxy = self.box.height + + for _, v in ipairs(data) do + local x, y = table.unpack(v) + + local nextx = self.math.round(self.math.scale(x, minx, maxx, boxminx, boxmaxx)) + local nexty = 1 + boxmaxy - self.math.round(self.math.scale(y, miny, maxy, boxminy, boxmaxy)) + + if nextx < boxminx or nexty < boxminy or nextx > boxmaxx or nexty > boxmaxy then + -- do nothing for OOB points + else + self:drawLine(nextx, nexty, nextx, nexty, color) + end + end +end + +function Plotter:chartGrid(dataw, miny, maxy, xOffset, color, styles) + local xGap, yLinesMin, yLinesFactor = 10, 5, 2 + + if not color then color = colors.gray end + + -- override grid styles + if type(styles) == 'table' then + xGap = styles.xGap or xGap + yLinesMin = styles.yLinesMin or yLinesMin + yLinesFactor = styles.yLinesFactor or yLinesFactor + end + + local gridData = {} + + local yPow = math.floor(math.log(maxy - miny, yLinesFactor)) + + local ySpace = yLinesFactor ^ yPow / yLinesMin + + local yOffset = miny % ySpace + + for x = xOffset + 1, dataw, xGap do + for y = miny - yOffset, maxy, ySpace do + table.insert(gridData, {x, y}) + end + end + + self:chartXY(gridData, 1, dataw, miny, maxy, color) +end + +--- clear plot using specified color +--- @param color colors +function Plotter:clear(color) + self.box:clear(color) +end + +--- render plot +function Plotter:render() + self.box:render() +end + +return Plotter \ No newline at end of file From 1c7b107437f4c4e6e8b7aff60646160a4d3f44a7 Mon Sep 17 00:00:00 2001 From: cyberbit Date: Fri, 29 Dec 2023 03:26:40 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=A6=20basalt=20graph=20adapter=20f?= =?UTF-8?q?or=20#11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.sh | 4 + src/telem/lib/output.lua | 1 + .../lib/output/basalt/GraphOutputAdapter.lua | 131 ++++++++++++++++++ .../output/plotter/ChartLineOutputAdapter.lua | 12 +- src/telem/lib/util.lua | 39 ++++++ 5 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 src/telem/lib/output/basalt/GraphOutputAdapter.lua diff --git a/build.sh b/build.sh index 54d44e8..8afaa59 100755 --- a/build.sh +++ b/build.sh @@ -30,5 +30,9 @@ mkdir -p dist/release luamin -f dist/telem.lua > dist/release/telem.min.lua luamin -f dist/vendor.lua > dist/release/vendor.min.lua +echo 'publishing to computer #0...' +cp dist/telem.lua /home/codespace/.local/share/craftos-pc/computer/0/telem/init.lua +cp dist/vendor.lua /home/codespace/.local/share/craftos-pc/computer/0/telem/vendor.lua + # echo 'tarring...' # tar -cf src.tar src \ No newline at end of file diff --git a/src/telem/lib/output.lua b/src/telem/lib/output.lua index 93558bd..513a715 100644 --- a/src/telem/lib/output.lua +++ b/src/telem/lib/output.lua @@ -8,6 +8,7 @@ return { -- Basalt basalt = { label = require 'telem.lib.output.basalt.LabelOutputAdapter', + graph = require 'telem.lib.output.basalt.GraphOutputAdapter', }, -- Plotter diff --git a/src/telem/lib/output/basalt/GraphOutputAdapter.lua b/src/telem/lib/output/basalt/GraphOutputAdapter.lua new file mode 100644 index 0000000..595b641 --- /dev/null +++ b/src/telem/lib/output/basalt/GraphOutputAdapter.lua @@ -0,0 +1,131 @@ +local o = require 'telem.lib.ObjectModel' +local t = require 'telem.lib.util' + +local OutputAdapter = require 'telem.lib.OutputAdapter' +local MetricCollection = require 'telem.lib.MetricCollection' + +local GraphOutputAdapter = o.class(OutputAdapter) +GraphOutputAdapter.type = 'GraphOutputAdapter' + +GraphOutputAdapter.MAX_ENTRIES = 50 +GraphOutputAdapter.SCALE_TICK = 10 + +local function graphtrackrange (self) + local min = self.graphdata[1] + local max = self.graphdata[1] + + for k,v in ipairs(self.graphdata) do + if v < min then min = v end + if v > max then max = v end + end + + return min,max +end + +function GraphOutputAdapter:constructor (frame, filter, bg, fg, fontSize) + self:super('constructor') + + self.bBaseFrame = assert(frame, 'Frame is required') + self.filter = assert(filter, 'Filter is required') + + self.graphdata = {} + + self:register(bg, fg, fontSize) +end + +function GraphOutputAdapter:register (bg, fg, fontSize) + local currentmin = 0 + local currentmax = 1000 + + self.tick = 0 + + self.bInnerFrame = self.bBaseFrame:addFrame() + :setBackground(bg) + :setSize('{parent.w}', '{parent.h}') + + local fGraph = self.bInnerFrame:addFrame('fGraph'):setBackground(colors.black) + :setPosition(1,1) + :setSize('{parent.w - 2}', '{parent.h - 6}') + + local fLabel = self.bInnerFrame:addFrame('fLabel'):setBackground(colors.black) + :setSize('{parent.w - 2}', 4) + :setPosition(1,'{parent.h - 5}') + + local fLabelMax = self.bInnerFrame:addFrame('fLabelMax'):setBackground(colors.black) + :setSize(6, 1) + :setPosition('{parent.w - 7}',1) + + local fLabelMin = self.bInnerFrame:addFrame('fLabelMin'):setBackground(colors.black) + :setSize(6, 1) + :setPosition('{parent.w - 7}','{fLabel.y - 2}') + + self.label = fLabel:addLabel() + :setText("-----") + :setPosition('{parent.w/2-self.w/2}', 2) + :setForeground(colors.white) + :setBackground(colors.black) + + self.graph = fGraph:addGraph() + :setPosition(1,1) + :setSize('{parent.w - 1}', '{parent.h - 1}') + :setMaxEntries(self.MAX_ENTRIES) + :setBackground(colors.black) + :setGraphColor(colors.red) + :setGraphSymbol('\127') + + self.graphscale = fGraph:addGraph() + :setGraphType('scatter') + :setPosition(1,'{parent.h - 1}') + :setSize('{parent.w - 1}', 2) + :setMaxEntries(self.MAX_ENTRIES) + :setBackground(colors.transparent) + :setGraphSymbol('|') + + self.labelmax = fLabelMax:addLabel() + :setPosition(1,1) + :setText('-----') + :setForeground(colors.white) + :setBackground(colors.black) + + self.labelmin = fLabelMin:addLabel() + :setPosition(1,1) + :setText('-----') + :setForeground(colors.white) + :setBackground(colors.black) + + self.graph:setMinValue(currentmin):setMaxValue(currentmax) +end + +function GraphOutputAdapter:write (collection) + assert(o.instanceof(collection, MetricCollection), 'Collection must be a MetricCollection') + + local resultMetric = collection:find(self.filter) + + assert(resultMetric, 'could not find metric') + + t.constrainAppend(self.graphdata, resultMetric.value, self.MAX_ENTRIES) + + local newmin, newmax = graphtrackrange(self) + + self.graph:setMinValue(newmin):setMaxValue(newmax) + + self.graph:addDataPoint(resultMetric.value) + + self.label:setFontSize(2) + self.label:setText(t.shortnum(resultMetric.value)) + + if self.tick == self.SCALE_TICK then + self.graphscale:addDataPoint(100) + self.tick = 1 + else + self.graphscale:addDataPoint(50) + self.tick = self.tick + 1 + end + + self.labelmax:setText(t.shortnum(newmax)) + self.labelmin:setText(t.shortnum(newmin)) + + return self +end + +return GraphOutputAdapter \ No newline at end of file diff --git a/src/telem/lib/output/plotter/ChartLineOutputAdapter.lua b/src/telem/lib/output/plotter/ChartLineOutputAdapter.lua index 73b84b9..4a0d3e9 100644 --- a/src/telem/lib/output/plotter/ChartLineOutputAdapter.lua +++ b/src/telem/lib/output/plotter/ChartLineOutputAdapter.lua @@ -95,7 +95,7 @@ function ChartLineOutputAdapter:write (collection) local minrange = 0.000001 if not flatlabel then - flatlabel = tostring(actualmin) + flatlabel = t.shortnum2(actualmin) end actualmin = actualmin - minrange / 2 @@ -113,19 +113,21 @@ function ChartLineOutputAdapter:write (collection) self.plotter:chartLine(self.plotData, self.MAX_ENTRIES, actualmin, actualmax, self.fg) - local maxString = tostring(actualmax) - local minString = tostring(actualmin) + local maxString = t.shortnum2(actualmax) + local minString = t.shortnum2(actualmin) self.win.setVisible(false) self.plotter:render() + self.win.setTextColor(self.fg) + self.win.setBackgroundColor(self.bg) if not flatlabel then self.win.setCursorPos(self.plotter.box.term_width - #maxString + 1, 1) - self.win.write(actualmax) + self.win.write(maxString) self.win.setCursorPos(self.plotter.box.term_width - #minString + 1, self.plotter.box.term_height) - self.win.write(actualmin) + self.win.write(minString) else self.win.setCursorPos(self.plotter.box.term_width - #flatlabel + 1, self.plotter.math.round(self.plotter.box.term_height / 2)) self.win.write(flatlabel) diff --git a/src/telem/lib/util.lua b/src/telem/lib/util.lua index 9cb48e3..d3f8526 100644 --- a/src/telem/lib/util.lua +++ b/src/telem/lib/util.lua @@ -61,6 +61,44 @@ local function shortnum(n) end end +-- based on https://rosettacode.org/wiki/Suffixation_of_decimal_numbers#Python +local function shortnum2(num, digits, base) + if not base then base = 10 end + + local suffixes = {'', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'X', 'W', 'V', 'U', 'googol'} + + local exponent_distance = 10 + if base == 2 then + exponent_distance = 10 + else + exponent_distance = 3 + end + + num = string.gsub(num, ',', '') + local num_sign = string.sub(num, 1, 1) == '+' or string.sub(num, 1, 1) == '-' and string.sub(num, 1, 1) or '' + + num = math.abs(tonumber(num)) + + local suffix_index = 0 + if base == 10 and num >= 1e100 then + suffix_index = 13 + num = num / 1e100 + elseif num > 1 then + local magnitude = math.floor(math.log(num, base)) + suffix_index = math.min(math.floor(magnitude / exponent_distance), 12) + num = num / (base ^ (exponent_distance * suffix_index)) + end + + local num_str = '' + if digits then + num_str = string.format('%.' .. digits .. 'f', num) + else + num_str = string.format('%.3f', num):gsub('0+$', ''):gsub('%.$', '') + end + + return num_sign .. num_str .. suffixes[suffix_index + 1] .. (base == 2 and 'i' or '') +end + local function constrainAppend (data, value, width) local removed = 0 @@ -81,5 +119,6 @@ return { skpairs = skpairs, sleep = os.sleep or tsleep, shortnum = shortnum, + shortnum2 = shortnum2, constrainAppend = constrainAppend, } \ No newline at end of file From 715301d8130d64d1396cf64b3ec42b65c431063a Mon Sep 17 00:00:00 2001 From: cyberbit Date: Fri, 29 Dec 2023 03:32:45 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A4=96=20disable=20codespace=20deploy?= =?UTF-8?q?ment=20in=20build=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.sh b/build.sh index 8afaa59..4f7b299 100755 --- a/build.sh +++ b/build.sh @@ -30,9 +30,9 @@ mkdir -p dist/release luamin -f dist/telem.lua > dist/release/telem.min.lua luamin -f dist/vendor.lua > dist/release/vendor.min.lua -echo 'publishing to computer #0...' -cp dist/telem.lua /home/codespace/.local/share/craftos-pc/computer/0/telem/init.lua -cp dist/vendor.lua /home/codespace/.local/share/craftos-pc/computer/0/telem/vendor.lua +# echo 'publishing to computer #0...' +# cp dist/telem.lua /home/codespace/.local/share/craftos-pc/computer/0/telem/init.lua +# cp dist/vendor.lua /home/codespace/.local/share/craftos-pc/computer/0/telem/vendor.lua # echo 'tarring...' # tar -cf src.tar src \ No newline at end of file