basic scatter/line plots

This commit is contained in:
Anton Nesterov 2024-09-30 19:12:28 +02:00
parent 46fa140d01
commit 60b929db2b
No known key found for this signature in database
GPG key ID: 59121E8AE2851FB5
7 changed files with 273 additions and 11 deletions

View file

File diff suppressed because one or more lines are too long

View file

@ -12,6 +12,7 @@ import (
func InitPlotExports(this js.Value, args []js.Value) interface{} { func InitPlotExports(this js.Value, args []js.Value) interface{} {
exports := args[0] exports := args[0]
exports.Set("Hist", js.FuncOf(src.HistPlot)) exports.Set("Hist", js.FuncOf(src.HistPlot))
exports.Set("Plot", js.FuncOf(src.Plot))
return nil return nil
} }

Binary file not shown.

208
plot/src/Plot.go Normal file
View file

@ -0,0 +1,208 @@
//go:build js && wasm
// +build js,wasm
package src
import (
"image/color"
"syscall/js"
"github.com/gonum/stat"
"gonum.org/v1/plot"
"gonum.org/v1/plot/plotter"
"gonum.org/v1/plot/vg"
"gonum.org/v1/plot/vg/draw"
)
func Plot(this js.Value, args []js.Value) interface{} {
plt := plot.New()
opts := args[0]
var (
title = opts.Get("title").String()
XLabel = opts.Get("XLabel").String()
YLabel = opts.Get("YLabel").String()
width = 7.5
height = 4.25
)
if !opts.Get("width").IsUndefined() {
width = opts.Get("width").Float()
}
if !opts.Get("height").IsUndefined() {
height = opts.Get("height").Float()
}
if opts.Get("title").IsUndefined() {
title = "Plot"
}
if opts.Get("XLabel").IsUndefined() {
XLabel = "X"
}
if opts.Get("YLabel").IsUndefined() {
YLabel = "Y"
}
plt.Title.Text = title
plt.X.Label.Text = XLabel
plt.Y.Label.Text = YLabel
plt.Add(plotter.NewGrid())
lines := make([]plot.Plotter, 0)
for i, arg := range args {
if i == 0 {
continue
}
PlotterLinesFromJSObject(arg, plt, &lines)
}
plt.Add(lines...)
plt.Legend.Top = true
writer, err := plt.WriterTo(vg.Length(width)*vg.Inch, vg.Length(height)*vg.Inch, "png")
if err != nil {
panic(err)
}
b64string := WriterToBase64String(writer)
return b64string
}
func PlotterLinesFromJSObject(object js.Value, plt *plot.Plot, plotters *[]plot.Plotter) {
var (
typ = object.Get("type").String()
data = object.Get("data")
legend = ""
glyphColor = object.Get("glyphColor")
glyphRadius = object.Get("glypRadius")
glyphShape = object.Get("glyphShape")
lineWidth = object.Get("lineWidth")
lineColor = object.Get("lineColor")
lineDashes = object.Get("lineDashes")
)
shapeGlyph := func(g *draw.GlyphStyle) {
if !glyphShape.IsUndefined() {
shape := glyphShape.String()
switch shape {
case "cross":
g.Shape = draw.CrossGlyph{}
case "plus":
g.Shape = draw.PlusGlyph{}
case "ring":
g.Shape = draw.RingGlyph{}
case "square":
g.Shape = draw.SquareGlyph{}
case "triangle":
g.Shape = draw.TriangleGlyph{}
case "pyramid":
g.Shape = draw.PyramidGlyph{}
}
}
}
radiusGlyph := func(r *vg.Length) {
if !glyphRadius.IsUndefined() {
*r = vg.Points(glyphRadius.Float())
}
}
dashLine := func(l *[]vg.Length) {
if !lineDashes.IsUndefined() {
w := lineDashes.Index(0).Float()
h := lineDashes.Index(1).Float()
*l = []vg.Length{vg.Points(w), vg.Points(h)}
}
}
applyColor := func(p *color.Color, hex js.Value) {
if !hex.IsUndefined() {
*p = HexToRGBA(hex.String())
}
}
if !object.Get("legend").IsUndefined() {
legend = object.Get("legend").String()
}
if data.IsUndefined() {
panic("data is undefined")
}
XYs := XYFromJSObject(data)
if XYs == nil {
XYs = XYFromJSValues(data.Index(0), data.Index(1))
}
switch typ {
case "scatter":
s, err := plotter.NewScatter(XYs)
if err != nil {
panic(err)
}
applyColor(&s.GlyphStyle.Color, glyphColor)
shapeGlyph(&s.GlyphStyle)
radiusGlyph(&s.GlyphStyle.Radius)
*plotters = append(*plotters, s)
if legend != "" {
plt.Legend.Add(legend, s)
}
case "line":
l, err := plotter.NewLine(XYs)
if err != nil {
panic(err)
}
applyColor(&l.LineStyle.Color, lineColor)
dashLine(&l.LineStyle.Dashes)
if !lineWidth.IsUndefined() {
l.LineStyle.Width = vg.Points(lineWidth.Float())
}
*plotters = append(*plotters, l)
if legend != "" {
plt.Legend.Add(legend, l)
}
case "linePoints":
l, lp, err := plotter.NewLinePoints(XYs)
if err != nil {
panic(err)
}
if !lineWidth.IsUndefined() {
l.LineStyle.Width = vg.Points(lineWidth.Float())
}
applyColor(&l.LineStyle.Color, lineColor)
dashLine(&l.LineStyle.Dashes)
shapeGlyph(&lp.GlyphStyle)
applyColor(&lp.GlyphStyle.Color, glyphColor)
radiusGlyph(&lp.GlyphStyle.Radius)
*plotters = append(*plotters, l, lp)
if legend != "" {
plt.Legend.Add(legend, l, lp)
}
case "fitLinear":
X := make([]float64, len(XYs))
Y := make([]float64, len(XYs))
min := XYs[0].Y
max := XYs[0].Y
for i, xy := range XYs {
X[i] = xy.X
Y[i] = xy.Y
if xy.Y < min {
min = xy.Y
}
if xy.Y > max {
max = xy.Y
}
}
a, b := stat.LinearRegression(X, Y, nil, false)
l, err := plotter.NewLine(plotter.XYs{
{X: X[0], Y: a + b*X[0]},
{X: X[len(X)-1], Y: a + b*X[len(X)-1]},
})
if err != nil {
panic(err)
}
applyColor(&l.LineStyle.Color, lineColor)
dashLine(&l.LineStyle.Dashes)
if !lineWidth.IsUndefined() {
l.LineStyle.Width = vg.Points(lineWidth.Float())
}
*plotters = append(*plotters, l)
if legend != "" {
plt.Legend.Add(legend, l)
}
}
}

View file

@ -9,6 +9,9 @@ import (
"fmt" "fmt"
"image/color" "image/color"
"io" "io"
"syscall/js"
"gonum.org/v1/plot/plotter"
) )
func WriterToBase64String(writer io.WriterTo) string { func WriterToBase64String(writer io.WriterTo) string {
@ -36,3 +39,23 @@ func HexToRGBA(hex string) color.RGBA {
} }
return c return c
} }
func XYFromJSObject(object js.Value) plotter.XYs {
var (
x = object.Get("x")
y = object.Get("y")
)
if x.IsUndefined() || y.IsUndefined() {
return nil
}
return XYFromJSValues(x, y)
}
func XYFromJSValues(x, y js.Value) plotter.XYs {
xy := make(plotter.XYs, x.Length())
for i := range xy {
xy[i].X = x.Index(i).Float()
xy[i].Y = y.Index(i).Float()
}
return xy
}

View file

@ -1,4 +1,4 @@
import "../lib/wasm.js"; import "../lib/wasm_tinygo.js";
import type { Stat } from "./types.ts"; import type { Stat } from "./types.ts";
// @ts-expect-error: no types // @ts-expect-error: no types