Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
52 changes: 52 additions & 0 deletions packages/p/pierre115/gnov/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# GNOV Scatterplot

The `gnov` package allows you to render a scatter plot as an SVG image. It takes a list of `(x, y)` points and draws them as circles on a 2D canvas. You can also apply optional flags to display regression lines or curves.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What means the name gnov?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Gno Visual aha but i thinks we can find a better name for this ...

@davd-gzl davd-gzl Sep 29, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If you keep the name (which I think is alright), I think it's always great to highlight what it means, so the reader can better remind of it!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As @Davphla said; rename the H1 to the full name, GnoVisual, so that it makes sense to the reader. Then, you can chose to abbreviate it if you want. I get the point of shortening names, but you should ty naming your packages so that they're recognizable from the import path. A good name would be 'p/pierre115/scatterplot` :)


## API references

```go
Testscatter(POINTS, "TITLE", "X_AXIS_TITLE", "Y_AXIS_TITLE", "FLAG")
```

`POINTS` strcuture is set with the following arguments :
```go
type Point struct {
X, Y float64 //Coordinate of the point
Color string // Color of the point
Label string // Associate a label to the point
}
```
`TITLE`, `X_AXIS_TITLE`, `Y_AXIS_TITLE` are strings.
Comment thread
leohhhn marked this conversation as resolved.
Outdated

`FlagRe` is a `Boolean` value: `true` to enable and false by default.

`Maxticks` is an `int` n used to divide the axis into n graduation marks.

`Width` and `height` are `int` to personalize the size of the scatterplot.

## Usage

`Usecase`
Comment thread
leohhhn marked this conversation as resolved.
Outdated
Comment thread
leohhhn marked this conversation as resolved.
Outdated

```go
ScatterPlot{
Points: []Point{
{X: 100, Y: 00, 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,
}
```

## Flags

`Lineary Regression flag` that display the `regression line` of the scatterplot can be actived by the bool `true`.
Comment thread
leohhhn marked this conversation as resolved.
Outdated
Each flag shows the `equation` of the regression in the top left of the Scatterplot.
2 changes: 2 additions & 0 deletions packages/p/pierre115/gnov/gnomod.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module = "gno.land/p/pierre115/gnov"
gno = "0.9"
68 changes: 68 additions & 0 deletions packages/p/pierre115/gnov/linearyflag.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package gnov

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
}
190 changes: 190 additions & 0 deletions packages/p/pierre115/gnov/scatterplot.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Package gnov (Gno Visual) provides functionality to render a scatter plot as an SVG image.
Comment thread
leohhhn marked this conversation as resolved.
Outdated
// It takes a list of points (x,y) and draws them as circles on a 2D .
// You can also apply Flags.
package gnov

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
}

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)
}

// Use the ideal steps for X and Y axis
func StepXY(sp ScatterPlot, maxX, maxY, minX, minY float64) (float64, float64, float64, float64, float64, float64) {
Comment thread
leohhhn marked this conversation as resolved.
Outdated

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
}

// Draw axes and axes titles, return SVG strings
func RenderAxes(sp ScatterPlot, Width, Height int, maxX, maxY, minX, minY float64) string {
Comment thread
leohhhn marked this conversation as resolved.
Outdated
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)
}

// N icesteps 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 {

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
}
61 changes: 61 additions & 0 deletions packages/p/pierre115/gnov/scatterplot_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package gnov

import (
"strings"
"testing"
)

// Test for basic scatter plot rendering
func TestScatterPlot(t *testing.T) {
sp := ScatterPlot{
Points: []Point{
{X: 0, Y: 0, Label: "A", Color: "red"},
{X: 1, Y: 2, Label: "B", Color: "blue"},
},
Title: "Test Scatter",
XAxis: "X Axis",
YAxis: "Y Axis",
}

got := sp.String()

if got == "" {
t.Fatal("Render output is empty")
}
if !strings.Contains(got, "Test Scatter") {
t.Error("title not found in render output")
}
if !strings.Contains(got, "X Axis") {
t.Error("X axis label not found")
}
if !strings.Contains(got, "Y Axis") {
t.Error("Y axis label not found")
}
if !strings.Contains(got, "<circle") {
t.Error("expected at least one circle (point) in render output")
}
}

// Test for scatter plot rendering with regression flag
func TestScatterPlotReFlag(t *testing.T) {
sp := ScatterPlot{
Points: []Point{
{X: 0, Y: 0},
{X: 1, Y: 2},
{X: 2, Y: 4},
},
Title: "With Regression",
XAxis: "X",
YAxis: "Y",
FlagRe: true,
}

got := sp.String()

if !strings.Contains(got, "Equation") {
t.Error("regression equation not found in render output")
}
if !strings.Contains(got, "<rect") {
t.Error("expected regression line rectangle in render output")
}
}
18 changes: 18 additions & 0 deletions packages/p/pierre115/gnov/type.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package gnov

// Points structure
type Point struct {
X, Y float64 // Coordinate of the point
Color string // Color of the point
Label string // Associate a label to the point
}

// Scatterplot structure
type ScatterPlot struct {
Points []Point // Points structure (points clouds)
Title string // Title of the scatter plot
XAxis, YAxis string // Title of X and Y axis
FlagRe bool // Optional flag for linear regression
maxTicks int // Optional max ticks for axis (default 10)
Width, Height int // Optional width and height of the svg (default 750x600)
}
Loading