Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions packages/p/pierre115/gnovisual/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# GnoVisual

Package `gnovisual` renders scatter plots as SVG images. It takes a list of (x, y) points and draws them as circles on a 2D canvas, with optional support for displaying a linear regression line.

## Usage

```go
import "gno.land/p/pierre115/gnovisual"

func main() {
plot := scatterplot.ScatterPlot{
Points: []scatterplot.Point{
{X: 100, Y: 0, Label: "A"},
{X: 101, Y: 20, Label: "B"},
{X: 102, Y: 40, Label: "C"},
{X: 103, Y: 60, Label: "D"},
},
Title: "Sales Growth",
XAxis: "Years",
YAxis: "Sales",
FlagRe: true,
Maxticks: 20,
Width: 800,
Height: 800,
}

svg := plot.Render()
// Use the SVG output
}
```

## API

### Point

```go
type Point struct {
X, Y float64 // Coordinates of the point
Color string // Color of the point
Label string // Label associated with the point
}
```

### ScatterPlot

```go
type ScatterPlot struct {
Points []Point // Data points to plot
Title string // Plot title
XAxis string // X-axis label
YAxis string // Y-axis label
FlagRe bool // Enable linear regression line (default: false)
Maxticks int // Number of tick marks on axes
Width int // Plot width in pixels
Height int // Plot height in pixels
}
```

## Features

### Linear Regression

Set `FlagRe` to `true` to display a linear regression line fitted to your data points. The regression equation will be displayed in the top-left corner of the plot.

## Example

Here's a complete example showing sales growth over time:

```go
plot := scatterplot.ScatterPlot{
Points: []scatterplot.Point{
{X: 2020, Y: 150, Label: "Q1"},
{X: 2021, Y: 230, Label: "Q2"},
{X: 2022, Y: 310, Label: "Q3"},
{X: 2023, Y: 405, Label: "Q4"},
},
Title: "Quarterly Sales Performance",
XAxis: "Year",
YAxis: "Revenue (k$)",
FlagRe: true,
Maxticks: 10,
Width: 600,
Height: 400,
}
```

This will generate an SVG scatter plot with a regression line showing the trend in sales growth.
2 changes: 2 additions & 0 deletions packages/p/pierre115/gnovisual/gnomod.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module = "gno.land/p/pierre115/gnovisual"
gno = "0.9"
68 changes: 68 additions & 0 deletions packages/p/pierre115/gnovisual/linearyflag.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package gnovisual

import (
"math"

"gno.land/p/nt/ufmt"
)

// Function to get slope and intercept with linear way
func LinearRegression(points []Point) (slope, intercept float64) {
n := float64(len(points))
sumX := float64(0)
sumY := float64(0)
sumXY := float64(0)
sumX2 := float64(0)

for _, p := range points {
sumX += p.X
sumY += p.Y
sumXY += p.X * p.Y
sumX2 += p.X * p.X
}

slope = (n*sumXY - sumX*sumY) / (n*sumX2 - sumX*sumX)
intercept = (sumY - slope*sumX) / n
return
}

// Calcul the regression line and return as SVG string
func RenderReFlag(points []Point, minX, maxX, minY, maxY float64, canvasWidth, canvasHeight int) string {
slope, intercept := LinearRegression(points)

xStart := minX
xEnd := maxX
yStart := slope*xStart + intercept
yEnd := slope*xEnd + intercept

// Normalize with the canvas data
nx1 := 40 + (xStart-minX)/(maxX-minX)*float64(canvasWidth-60)
ny1 := float64(canvasHeight-40) - (yStart-minY)/(maxY-minY)*float64(canvasHeight-60)
nx2 := 40 + (xEnd-minX)/(maxX-minX)*float64(canvasWidth-60)
ny2 := float64(canvasHeight-40) - (yEnd-minY)/(maxY-minY)*float64(canvasHeight-60)

// calcul of angle
dx := nx2 - nx1
dy := ny2 - ny1
angleRad := math.Atan2(dy, dx)
angleDeg := angleRad * 180 / math.Pi

svgOut := ""

// SVG Rectangle as regression line
svgOut += ufmt.Sprintf(
`<rect x="%d" y="%d" width="%d" height="1" fill="black" transform="rotate(%.2f %d %d)"/>`,
int(nx1), int(ny1),
int(math.Hypot(nx2-nx1, ny2-ny1)),
angleDeg, int(nx1), int(ny1),
)

// Equation of the line
equation := ufmt.Sprintf("y = %.2fx + %.2f", slope, intercept)
svgOut += ufmt.Sprintf(
`<text x="50" y="20" style="font-size:12px;font-family:'Inter var',sans-serif;" fill="black">Equation : %s</text>`,
equation,
)

return svgOut
}
191 changes: 191 additions & 0 deletions packages/p/pierre115/gnovisual/scatterplot.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Package gnovisual provides functionality to render a scatter plot as an SVG image.
// It takes a list of points (x,y) and draws them as circles on a 2D .
// You can also apply Flags.
package gnovisual

import (
"math"

"gno.land/p/nt/ufmt"
)

const (
DefaultWidth = 750
DefaultHeight = 500
DefaultMaxTicks = 10
)

// Get max ticks for axis
func (sp ScatterPlot) GetMaxTicks() int {
if sp.maxTicks == 0 {
return DefaultMaxTicks
}
return sp.maxTicks
}

// Get width of the svg
func (sp ScatterPlot) GetWidth() int {
if sp.Width == 0 {
return DefaultWidth
}
return sp.Width
}

// Get height of the svg
func (sp ScatterPlot) GetHeight() int {
if sp.Height == 0 {
return DefaultHeight
}
return sp.Height
}

// NiceStep calculates a visually pleasing axis step size based on the data range and maximum number of ticks.
func niceStep(sp ScatterPlot, rangeVal float64) float64 {
var niceBase float64
maxTicks := sp.GetMaxTicks()
rawStep := rangeVal / float64(maxTicks)
exponent := math.Floor(math.Log10(rawStep))
fraction := rawStep / math.Pow(10, exponent)

switch {
case fraction < 1.5:
niceBase = 1
case fraction < 3:
niceBase = 2
case fraction < 7:
niceBase = 5
default:
niceBase = 10
}

return niceBase * math.Pow(10, exponent)
}

// StepXY use the ideal steps calculated by nicestep function for both axis
func StepXY(sp ScatterPlot, maxX, maxY, minX, minY float64) (float64, float64, float64, float64, float64, float64) {

rangeX := maxX - minX
stepX := niceStep(sp, rangeX)
startX := math.Floor(minX/stepX) * stepX
endX := math.Ceil(maxX/stepX) * stepX
rangeY := maxY - minY
stepY := niceStep(sp, rangeY)
startY := math.Floor(minY/stepY) * stepY
endY := math.Ceil(maxY/stepY) * stepY

return stepX, startX, endX, stepY, startY, endY
}

// RenderAxes renders the chart axes and their corresponding titles, returning the result as SVG-formatted strings for display.
func RenderAxes(sp ScatterPlot, Width, Height int, maxX, maxY, minX, minY float64) string {
svgOut := ""

// Axe Y
svgOut += ufmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="black"/>`, 40, Height-40, Width-60, 1)
svgOut += ufmt.Sprintf(
`<text x="%d" y="%d" style="font-family:'Inter var',sans-serif;font-size:%dpx;text-anchor:middle;" transform="rotate(-90 %d %d)" fill="black">%s</text>`,
10, Height/2, 12, 15, Height/2, sp.YAxis,
)

// Axe X
svgOut += ufmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="black"/>`, 40, 20, 1, Height-60)
svgOut += ufmt.Sprintf(
`<text x="%d" y="%d" style="font-family:'Inter var',sans-serif;font-size:%dpx;text-anchor:middle;" fill="black">%s</text>`,
Width/2, Height-10, 12, sp.XAxis,
)

// Scale helpers
scaleX := func(val float64) float64 {
return 40 + (val-minX)/(maxX-minX)*float64(Width-60)
}
scaleY := func(val float64) float64 {
return float64(Height-40) - (val-minY)/(maxY-minY)*float64(Height-60)
}

// Nicesteps calcul for graduation
stepX, startX, endX, stepY, startY, endY := StepXY(sp, maxX, maxY, minX, minY)

// Graduation X
for val := startX; val <= endX; val += stepX {
nx := scaleX(val)
y := float64(Height - 40)
svgOut += ufmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="black"/>`, int(nx), int(y), 1, 5)
svgOut += ufmt.Sprintf(`<text x="%d" y="%d" style="font-size:%dpx;text-anchor:middle;" fill="black">%.1f</text>`, int(nx), int(y)+18, 11, val)
}

// Graduation Y
for val := startY; val <= endY; val += stepY {
ny := scaleY(val)
x := float64(40)
svgOut += ufmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="black"/>`, int(x)-5, int(ny), 5, 1)
svgOut += ufmt.Sprintf(`<text x="%d" y="%d" style="font-size:%dpx;text-anchor:end;" fill="black">%.1f</text>`, int(x)-8, int(ny)+4, 11, val)
}

return svgOut
}

// Returns an img svg markup as a string, including a markdown header if a non-empty title is provided.
// The function need a Point struct, two axes titles and a flag.
// You can see existings flags in the Readme.md
func (sp ScatterPlot) String() string {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (sp ScatterPlot) String() string {
func (sp ScatterPlot) Render() string {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When i put Render its doesn't work so i let Strings for the moment


const (
pointRadius = 2
)

Width := sp.GetWidth()
Height := sp.GetHeight()

if len(sp.Points) == 0 {
return "\nscatterplot fails: no data provided"
}

// calcul min/max
minX, minY := sp.Points[0].X, sp.Points[0].Y
maxX, maxY := sp.Points[0].X, sp.Points[0].Y

for _, p := range sp.Points {
if p.X > maxX {
maxX = p.X
}
if p.X < minX {
minX = p.X
}
if p.Y > maxY {
maxY = p.Y
}
if p.Y < minY {
minY = p.Y
}
}

svgOut := ""
svgOut += RenderAxes(sp, Width, Height, maxX, maxY, minX, minY)

// Draw Points and labels
for _, p := range sp.Points {
nx := 40 + (p.X-minX)/(maxX-minX)*float64(Width-60)
ny := float64(Height-40) - (p.Y-minY)/(maxY-minY)*float64(Height-60)
svgOut += ufmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" fill="%s"/>`, int(nx), int(ny), pointRadius, p.Color)
if p.Label != "" {
svgOut += ufmt.Sprintf(
`<text x="%d" y="%d" style="font-family:'Inter var',sans-serif;font-size:10px;text-anchor:middle;" fill="#333">%s</text>`,
int(nx)+5, int(ny)+12, p.Label,
)
}
}

// Flags :
if sp.FlagRe == true {
svgOut += RenderReFlag(sp.Points, minX, maxX, minY, maxY, Width, Height)
}

// Draw Title
if sp.Title != "" {
svgOut += ufmt.Sprintf(`<text x="%d" y="%d" style="font-family:'Inter var',sans-serif;font-size:16px;text-anchor:middle;" fill="black">%s</text>`,
Width/2, 20, sp.Title,
)
}

return svgOut
}
Loading
Loading