-
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 all 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,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. |
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/gnovisual" | ||
| 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 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 | ||
| } |
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,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 { | ||||||
leohhhn marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| 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 { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
| } | ||||||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.