Skip to content

Commit 8af1f18

Browse files
committed
feat: Implement channel-based chart data updates
Replace app rerender approach with Go channel communication for chart data updates, following the Go proverb "share memory by communicating" instead of "communicate by sharing memory". Changes: - Add ChartDataChannel property to runtime chart series - Create global data channel for chart updates in state.go - Pass data channel to BitcoinChart component - Implement channel monitoring in ChartRenderer - Send data updates through channel instead of app.Update() - Use reflection for type-safe channel monitoring Flow: 1. Chart initializes with data channel from InitChartDataChannel() 2. ChartRenderer starts goroutine to monitor the channel 3. On scroll, SendChartDataUpdate() sends new data through channel 4. Renderer receives data and updates series.Properties["data"] 5. Next render cycle uses updated data (no full rerender needed) This approach is more Go-idiomatic and efficient than forcing full app rerenders for data changes.
1 parent 3adcc74 commit 8af1f18

8 files changed

Lines changed: 131 additions & 14 deletions

File tree

examples/webgpu-chart/app.gx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ func App() (Component) {
44
// Get visible window of chart data (managed by scroll manager)
55
chartData := GetVisibleChartData()
66

7+
// Get or initialize the data channel for reactive updates
8+
dataChan := InitChartDataChannel()
9+
710
Div(
811
ID("app"),
912
Class("chart-container"),
@@ -20,7 +23,7 @@ func App() (Component) {
2023
Width(1200),
2124
Height(700)
2225
) {
23-
GPUChart(NewBitcoinChart(chartData))
26+
GPUChart(NewBitcoinChart(chartData, dataChan))
2427
}
2528
Div(Class("info")) {
2629
P() {

examples/webgpu-chart/app_gen.go

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/webgpu-chart/chart.gx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package main
22

3-
func BitcoinChart(data []OHLCV) (Chart) {
3+
func BitcoinChart(data []OHLCV, dataChan chan []OHLCV) (Chart) {
44
Chart(ChartBackground(0.08, 0.09, 0.12, 1.0)) {
55
XAxis(AxisPosition("bottom"), TimeScale(true), GridLines(true), Label("Time"))
66

77
YAxis(AxisPosition("right"), GridLines(true), Label("Price (USD)"))
88

99
CandlestickSeries(
1010
ChartData(data),
11+
ChartDataChannel(dataChan),
1112
UpColor(0.18, 0.80, 0.44, 1.0),
1213
DownColor(0.91, 0.27, 0.38, 1.0),
1314
WickColor(0.6, 0.6, 0.65, 1.0),

examples/webgpu-chart/chart_gen.go

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/webgpu-chart/scroll.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,12 +295,13 @@ func (sm *ScrollManager) attachEventListeners() {
295295
sm.app.Call("addEventListener", "keydown", sm.keyCallback)
296296
}
297297

298-
// updateChart triggers a chart rerender with updated data
298+
// updateChart triggers a chart update by sending data through the channel
299299
func (sm *ScrollManager) updateChart() {
300300
log("[Scroll] Viewport updated:", sm.chartData.visibleStart, "-", sm.chartData.visibleEnd, "of", len(sm.chartData.allData))
301301

302-
// Trigger the app to rerender with new visible data
303-
TriggerChartUpdate()
302+
// Send new visible data through the channel (non-blocking)
303+
visibleData := sm.chartData.GetVisibleData()
304+
SendChartDataUpdate(visibleData)
304305

305306
// Check if we need to fetch/generate more data
306307
if sm.chartData.visibleEnd > len(sm.chartData.allData)-sm.chartData.prefetchSize {

examples/webgpu-chart/state.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,22 @@ import "github.com/gaarutyunov/guix/pkg/runtime/chart"
88
var (
99
globalScrollManager *ScrollManager
1010
globalApp *App
11+
chartDataChannel chan []chart.OHLCV
1112
)
1213

14+
// InitChartDataChannel initializes the global chart data channel
15+
func InitChartDataChannel() chan []chart.OHLCV {
16+
if chartDataChannel == nil {
17+
chartDataChannel = make(chan []chart.OHLCV, 1) // Buffered channel
18+
}
19+
return chartDataChannel
20+
}
21+
22+
// GetChartDataChannel returns the global chart data channel
23+
func GetChartDataChannel() chan []chart.OHLCV {
24+
return chartDataChannel
25+
}
26+
1327
// GetVisibleChartData returns the currently visible chart data
1428
// This is called by the app during rendering to get only the visible window of data
1529
func GetVisibleChartData() []chart.OHLCV {
@@ -30,9 +44,20 @@ func SetGlobalApp(app *App) {
3044
globalApp = app
3145
}
3246

33-
// TriggerChartUpdate triggers the app to rerender with updated data
34-
func TriggerChartUpdate() {
35-
if globalApp != nil {
36-
globalApp.Update()
47+
// SendChartDataUpdate sends new chart data through the channel
48+
func SendChartDataUpdate(data []chart.OHLCV) {
49+
if chartDataChannel != nil {
50+
// Non-blocking send - if channel is full, drain it first
51+
select {
52+
case chartDataChannel <- data:
53+
// Successfully sent
54+
default:
55+
// Channel full, drain and send new data
56+
select {
57+
case <-chartDataChannel:
58+
default:
59+
}
60+
chartDataChannel <- data
61+
}
3762
}
3863
}

pkg/runtime/chart_renderer.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ func NewChartRenderer(canvas *GPUCanvas, chart *GPUNode) (*ChartRenderer, error)
9292
return nil, err
9393
}
9494

95+
// Start monitoring data channels for updates
96+
renderer.startDataChannelMonitors()
97+
9598
log("[ChartRenderer] Chart renderer created successfully")
9699
return renderer, nil
97100
}
@@ -917,3 +920,80 @@ func (cr *ChartRenderer) createLineBindGroup(dataBuffer *GPUBuffer) js.Value {
917920

918921
return cr.Canvas.GPUContext.Device.Call("createBindGroup", bindGroupDesc)
919922
}
923+
924+
// startDataChannelMonitors starts goroutines to monitor data channels for updates
925+
func (cr *ChartRenderer) startDataChannelMonitors() {
926+
log("[ChartRenderer] Starting data channel monitors")
927+
928+
// Monitor candlestick series data channels
929+
for _, series := range cr.CandlestickSeries {
930+
if channelValue, hasChannel := series.Properties["dataChannel"]; hasChannel {
931+
log("[ChartRenderer] Found dataChannel in candlestick series")
932+
933+
// Type assert to the correct channel type
934+
if dataChan, ok := channelValue.(chan []interface{}); ok {
935+
log("[ChartRenderer] Starting monitor for candlestick data channel (generic)")
936+
go cr.monitorCandlestickChannel(series, dataChan)
937+
} else {
938+
// Try reflection to handle the actual type
939+
log("[ChartRenderer] Data channel type:", fmt.Sprintf("%T", channelValue))
940+
go cr.monitorCandlestickChannelReflect(series, channelValue)
941+
}
942+
}
943+
}
944+
945+
log("[ChartRenderer] Data channel monitors started")
946+
}
947+
948+
// monitorCandlestickChannel monitors a candlestick data channel for updates
949+
func (cr *ChartRenderer) monitorCandlestickChannel(series *GPUNode, dataChan chan []interface{}) {
950+
log("[ChartRenderer] Candlestick channel monitor started")
951+
952+
for {
953+
select {
954+
case newData, ok := <-dataChan:
955+
if !ok {
956+
log("[ChartRenderer] Data channel closed")
957+
return
958+
}
959+
960+
log("[ChartRenderer] Received new data through channel:", len(newData), "candles")
961+
962+
// Update the series data property
963+
series.Properties["data"] = newData
964+
}
965+
}
966+
}
967+
968+
// monitorCandlestickChannelReflect monitors a channel using reflection
969+
func (cr *ChartRenderer) monitorCandlestickChannelReflect(series *GPUNode, channelValue interface{}) {
970+
log("[ChartRenderer] Starting reflect-based channel monitor")
971+
972+
// Use reflection to receive from channel
973+
channelVal := reflect.ValueOf(channelValue)
974+
if channelVal.Kind() != reflect.Chan {
975+
logError("[ChartRenderer] Value is not a channel")
976+
return
977+
}
978+
979+
log("[ChartRenderer] Channel monitor loop started")
980+
981+
for {
982+
chosen, recv, recvOK := reflect.Select([]reflect.SelectCase{
983+
{Dir: reflect.SelectRecv, Chan: channelVal},
984+
})
985+
986+
if chosen == 0 {
987+
if !recvOK {
988+
log("[ChartRenderer] Channel closed")
989+
return
990+
}
991+
992+
// Update the series data property with received value
993+
newData := recv.Interface()
994+
log("[ChartRenderer] Received new data through reflect channel:", fmt.Sprintf("%T", newData))
995+
996+
series.Properties["data"] = newData
997+
}
998+
}
999+
}

pkg/runtime/chart_vnode.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,11 @@ func ChartData(data interface{}) GPUProp {
213213
return GPUProp{Key: "data", Value: data}
214214
}
215215

216+
// ChartDataChannel sets a reactive data channel for the series
217+
func ChartDataChannel(ch interface{}) GPUProp {
218+
return GPUProp{Key: "dataChannel", Value: ch}
219+
}
220+
216221
// UpColor sets up candle color
217222
func UpColor(r, g, b, a float32) GPUProp {
218223
return GPUProp{Key: "upColor", Value: NewVec4(r, g, b, a)}

0 commit comments

Comments
 (0)