charts.gno
3.51 Kb ยท 123 lines
1// Charts is a package that provides functions to generate ASCII charts in markdown format.
2// It includes functions for line charts, bar charts, and column charts.
3//
4// Both line and column charts are implemented in a way that offers normalization of data;
5// In short terms precision of the chart can be adjusted by changing the number of points or columns.
6// Line chart offers a way to adjust the spacing between points as well, which allows for more control over the chart.
7//
8// e.g. Let's say there is a data set of a value of dollar per day in the year of 2025 in a slice of floats.
9// If those values are passed to the `GenerateLineChart` in this manner:
10// `GenerateLineChart(values, 12, 5, "Dollar per day", "Days", "Dollars")`
11// The chart will be normalized to 12 points, with 5 characters of spacing between each point, but more importantly,
12// these points will represent a rough monthly average of the dollar value (months are not of equal length, so this is not an exact science).
13
14package charts
15
16import (
17 "strings"
18
19 "gno.land/p/nt/ufmt"
20)
21
22const height = 15
23
24// normalizeData reduces the number of data points to maxColumns by averaging
25// Returns nil if values is empty or maxColumns is zero or negative
26func normalizeData(values []float64, maxColumns int) []float64 {
27 if len(values) == 0 || maxColumns <= 0 {
28 return nil
29 }
30
31 if maxColumns >= len(values) {
32 result := make([]float64, len(values))
33 copy(result, values)
34 return result
35 }
36
37 result := make([]float64, maxColumns)
38 groupSize := float64(len(values)) / float64(maxColumns)
39
40 for i := 0; i < maxColumns; i++ {
41 start := int(float64(i) * groupSize)
42 end := int(float64(i+1) * groupSize)
43 if end > len(values) {
44 end = len(values)
45 }
46
47 sum := 0.0
48 count := end - start
49 for j := start; j < end; j++ {
50 sum += values[j]
51 }
52 if count > 0 {
53 result[i] = sum / float64(count)
54 }
55 }
56
57 return result
58}
59
60// findMaxValue returns the maximum value in a slice of float64
61func findMaxValue(values []float64) float64 {
62 if len(values) == 0 {
63 return 0
64 }
65
66 maxVal := values[0]
67 for _, v := range values[1:] {
68 if v > maxVal {
69 maxVal = v
70 }
71 }
72 return maxVal
73}
74
75// calculateMaxValueStringLength returns the maximum string length when formatting values
76func calculateMaxValueStringLength(values []float64) int {
77 maxValStrLen := 0
78 for _, v := range values {
79 valStr := ufmt.Sprintf("%.2f", v)
80 if len(valStr) > maxValStrLen {
81 maxValStrLen = len(valStr)
82 }
83 }
84 return maxValStrLen
85}
86
87// formatChartHeader formats the chart header with title and markdown code block
88func formatChartHeader(title string) string {
89 output := ""
90 if title != "" {
91 output += "## " + title + "\n"
92 }
93 return output
94}
95
96// formatYAxisTitle adds the y-axis title to the chart
97func formatYAxisTitle(yAxisTitle string) string {
98 if yAxisTitle != "" {
99 return yAxisTitle + "\n\n"
100 }
101 return ""
102}
103
104// formatXAxisTitle adds the x-axis title to the chart
105func formatXAxisTitle(xAxisTitle string, width int) string {
106 if xAxisTitle != "" {
107 padding := strings.Repeat(" ", width/2-len(xAxisTitle)/2)
108 return " " + padding + xAxisTitle + "\n"
109 }
110 return ""
111}
112
113// formatYAxisLabel formats a y-axis label with proper padding
114func formatYAxisLabel(value float64, scale float64, yAxisWidth int) string {
115 yValue := ufmt.Sprintf("%.2f", value/scale)
116 padding := strings.Repeat(" ", yAxisWidth-len(yValue))
117 return ufmt.Sprintf("%s%s | ", padding, yValue)
118}
119
120// formatYAxisSpace returns spacing for y-axis when no label is needed
121func formatYAxisSpace(yAxisWidth int) string {
122 return strings.Repeat(" ", yAxisWidth) + " | "
123}