Update 3d chart mode, add US heatmap, move kjol-web -> kjol-website
This commit is contained in:
120
go/cmd/kjol-website/app/server_counter.go
Normal file
120
go/cmd/kjol-website/app/server_counter.go
Normal file
@@ -0,0 +1,120 @@
|
||||
//go:build !(js && wasm)
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
chart "github.com/wcharczuk/go-chart/v2"
|
||||
|
||||
. "kjol/vdom"
|
||||
ui "kjol/webui"
|
||||
)
|
||||
|
||||
// ServerCounter is a SERVER component — note it's written exactly like a client
|
||||
// component (same builders, signals, On handlers). The //gowasm:server directive
|
||||
// makes the build generate a client stub so calling ServerCounter() on the
|
||||
// frontend is identical to calling any component; the state and this render run
|
||||
// on the server (its chart is computed there with go-chart), and clicks
|
||||
// round-trip over /rsc.
|
||||
//
|
||||
// The chart plots the counter value against the wall-clock time of each click
|
||||
// (milliseconds since the first click), so spacing clicks out spreads the data
|
||||
// points along the x-axis. Because the component is stateless on the server, the
|
||||
// click points live in a signal that round-trips with the rest of its state
|
||||
// (a plain slice would reset on every request).
|
||||
//
|
||||
//gowasm:server
|
||||
func ServerCounter() func() *VNode {
|
||||
count := NewSignal(0)
|
||||
points := NewSignal([]clickPoint{})
|
||||
bump := func(delta int) {
|
||||
count.Set(count.Get() + delta)
|
||||
points.Set(append(points.Get(), clickPoint{T: time.Now().UnixMilli(), V: count.Get()}))
|
||||
}
|
||||
// No card of its own: the component draws bare content and lets the caller frame it.
|
||||
// The docs page already puts it in a demo panel, and a card inside a card gives you
|
||||
// two borders and two shadows around the same thing.
|
||||
return func() *VNode {
|
||||
return Div(
|
||||
Div(Attr("class", "flex items-center gap-2 mb-3"),
|
||||
Span(Attr("class", "text-ink-soft"), Text("Server counter: ")),
|
||||
Strong(Attr("class", "badge inline-flex items-center rounded-full bg-green-700 px-2.5 py-0.5 text-sm font-semibold text-white"), Text(strconv.Itoa(count.Get()))),
|
||||
Div(Attr("class", "ml-auto flex gap-1"),
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonSecondary, Small: true, Text: "−", OnClick: func() { bump(-1) }}),
|
||||
ui.Button(ui.ButtonProps{Color: ui.ButtonGreen, Small: true, Text: "+", OnClick: func() { bump(1) }}),
|
||||
),
|
||||
),
|
||||
Div(Attr("class", "rounded-default border border-line bg-surface p-2 overflow-auto"),
|
||||
Raw(clickChartSVG(points.Get()))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// clickPoint records one click: its wall-clock time and the resulting counter
|
||||
// value. Exported fields so the signal's JSON snapshot round-trips it.
|
||||
type clickPoint struct {
|
||||
T int64 // click time, Unix milliseconds
|
||||
V int // counter value after the click
|
||||
}
|
||||
|
||||
// clickChartSVG plots counter value vs. time-of-click (ms since the first
|
||||
// click) as a line graph. Explicit axis ranges keep it valid for the tricky
|
||||
// cases (a single click, or several clicks within the same millisecond).
|
||||
func clickChartSVG(points []clickPoint) string {
|
||||
if len(points) == 0 {
|
||||
return `<span class="text-muted">Click + / − to plot the counter over time (ms since the first click).</span>`
|
||||
}
|
||||
t0 := points[0].T
|
||||
xs := make([]float64, len(points))
|
||||
ys := make([]float64, len(points))
|
||||
minY, maxY := 0.0, 0.0 // keep the zero baseline in view for context
|
||||
for i, p := range points {
|
||||
xs[i] = float64(p.T - t0)
|
||||
ys[i] = float64(p.V)
|
||||
if ys[i] < minY {
|
||||
minY = ys[i]
|
||||
}
|
||||
if ys[i] > maxY {
|
||||
maxY = ys[i]
|
||||
}
|
||||
}
|
||||
maxX := xs[len(xs)-1]
|
||||
if maxX <= 0 {
|
||||
maxX = 1 // rapid or single clicks: avoid a zero-width x-range
|
||||
}
|
||||
if minY == maxY {
|
||||
maxY++ // avoid a zero-height y-range
|
||||
}
|
||||
graph := chart.Chart{
|
||||
Title: "Counter over time (computed on the server)",
|
||||
TitleStyle: chart.Style{FontSize: 14},
|
||||
Background: chart.Style{Padding: chart.Box{Top: 48, Left: 20, Right: 20, Bottom: 40}},
|
||||
Height: 260,
|
||||
XAxis: chart.XAxis{
|
||||
Name: "ms since first click",
|
||||
Range: &chart.ContinuousRange{Min: 0, Max: maxX},
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Name: "counter",
|
||||
Range: &chart.ContinuousRange{Min: minY, Max: maxY},
|
||||
},
|
||||
Series: []chart.Series{
|
||||
chart.ContinuousSeries{
|
||||
XValues: xs,
|
||||
YValues: ys,
|
||||
Style: chart.Style{
|
||||
StrokeColor: chart.ColorGreen, StrokeWidth: 2,
|
||||
DotColor: chart.ColorGreen, DotWidth: 4, // a dot at each click
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if graph.Render(chart.SVG, &buf) != nil {
|
||||
return `<span class="text-danger">chart error</span>`
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
Reference in New Issue
Block a user