-
Notifications
You must be signed in to change notification settings - Fork 7
feat: add scatterplot svg library #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
2b98245
feat: add scatterplot svg library
divisio74 ce3c22d
[ADD]: README.md and increasing variables in ScatterPlot structure
divisio74 89a79cf
[FIX]: type.gno
divisio74 7deb617
Feat: Better description of gnov
divisio74 d672c38
Merge branch 'main' into feat/scatterplot
leohhhn b0d5413
test CI PR
divisio74 0ec5293
Fix: CI Checks fmt
divisio74 2a1700e
Fix: Coding style
divisio74 4b25d1c
Fix: CI const -> type.gno
divisio74 2bc5777
Fix: CI check type.gno
divisio74 5069504
Fix: CI check scatterplot.gno
divisio74 b0c0115
Fix: CI check scatterplot_test.gno
divisio74 d0a6c07
Add: godoc for niceStep function
divisio74 d69d20b
Fix: Godoc format corrected
divisio74 9efd5ee
Name change and Godoc for RenderAxes
divisio74 8efe040
Fix: Name and README.md
divisio74 ce78e78
Fix: Files name
divisio74 052e8b1
Fix: Name of the package
divisio74 abfd753
Merge branch 'main' into feat/scatterplot
leohhhn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
| ## 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. | ||
leohhhn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| `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` | ||
leohhhn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
leohhhn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ```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`. | ||
leohhhn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Each flag shows the `equation` of the regression in the top left of the Scatterplot. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| module = "gno.land/p/pierre115/gnov" | ||
| gno = "0.9" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| // Package gnov (Gno Visual) provides functionality to render a scatter plot as an SVG image. | ||
leohhhn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // 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" | ||
| ) | ||
|
|
||
| // Get max ticks for axis | ||
| func (sp ScatterPlot) GetMaxTicks() int { | ||
| if sp.maxTicks == 0 { | ||
| return 10 | ||
| } | ||
| return sp.maxTicks | ||
| } | ||
|
|
||
| // Get width and height of the svg | ||
| func (sp ScatterPlot) GetWidth() int { | ||
| if sp.Width == 0 { | ||
| return 750 | ||
leohhhn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| return sp.Width | ||
| } | ||
|
|
||
| func (sp ScatterPlot) GetHeight() int { | ||
| if sp.Height == 0 { | ||
| return 600 | ||
| } | ||
| 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) { | ||
|
|
||
| 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 { | ||
leohhhn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 { | ||
|
|
||
| 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 | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
leohhhn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| 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") | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
leohhhn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 regression "re" | ||
| maxTicks int // Optional max ticks for axis (default 10) | ||
leohhhn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Width, Height int // Optional width and height of the svg (default 750x600) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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 ...
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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` :)