Charts

We will generate charts dynamically and serve them through the Resources API.

For resource-backed src values and cached assets, see Resources.

./dashboard.gox

Charts

Temperature

Start with the temperature chart. First, prepare the content layout:

elem (d dashboard) Main() {
	~/* ... */
	<section>
		~(d.change())
		~(d.menu())
	</section>
	~(if city.IsValid() {
		<section>
			~(d.charts(city))
		</section>
	})
}

// component with all charts
elem (d dashboard) charts(city driver.City) {
	<div class="grid">
		<div></div>
		<div></div>
	</div>
}

Next, generate and serve the temperature chart while learning a new pattern: a React-style functional component.

A proxy, such as ~>(new(doors.Door)), captures whatever follows. We can even proxy a GoX ~func literal.

A GoX func literal (~func) is evaluated immediately, and its return value is rendered in place.

elem (d dashboard) charts(city driver.City) {
	<div class="grid">
		<div>
			~// Make a dynamic container with the content returned by ~func.
			~>(new(doors.Door)) ~func {
				days, _ := d.days.Effect(ctx)
				units, ok := d.units.Effect(ctx)
				if !ok {
					return nil
				}

				values, _ := driver.Weather.Temperature(ctx, city, units, days)
				// Generate []byte.
				svg, _ := driver.ChartLine(values.Values, values.Labels, units.Temperature())

				return <article>
					~// Pass []byte directly into src.
					<img height="auto" width="100%" src=(svg) type="image/svg+xml"/>
				</article>
			}
		</div>
		<div></div>
	</div>
}

Doors detects that the value of src is []byte and creates a private temporary endpoint for it. The type attribute is used for the Content-Type header in the response.

Temperature chart rendered from an SVG resource

Rest

Now prepare a separate chart component:

type chart struct {
	title string
	svg func() []byte
}

elem (c chart) Main() {
	<article>
		<header>
			~(c.title)
		</header>
		<img height="auto" width="100%" src=(c.svg()) type="image/svg+xml"/>
	</article>
}

And finally render all charts (some depend only on days, some on both days and units):

elem (d dashboard) charts(city driver.City) {
	<div class="grid">
		<div>
			~>(new(doors.Door)) ~func {
				days, _ := d.days.Effect(ctx)
				units, ok := d.units.Effect(ctx)
				if !ok {
					return nil
				}
				return chart{
					title: "Temperature",
					svg: func() []byte {
						values, _ := driver.Weather.Temperature(ctx, city, units, days)
						svg, _ := driver.ChartLine(values.Values, values.Labels, units.Temperature())
						return svg
					},
				}
			}
			~>(new(doors.Door)) ~func {
				days, ok := d.days.Effect(ctx)
				if !ok {
					return nil
				}
				return chart{
					title: "Humidity",
					svg: func() []byte {
						values, _ := driver.Weather.Humidity(ctx, city, days)
						svg, _ := driver.ChartLine(values.Values, values.Labels, "%")
						return svg
					},
				}
			}
		</div>
		<div>
			~>(new(doors.Door)) ~func {
				days, ok := d.days.Effect(ctx)
				if !ok {
					return nil
				}
				return chart{
					title: "Weather",
					svg: func() []byte {
						values, _ := driver.Weather.Code(ctx, city, days)
						svg, _ := driver.ChartPie(values.Values)
						return svg
					},
				}
			}
			~>(new(doors.Door)) ~func {
				days, _ := d.days.Effect(ctx)
				units, ok := d.units.Effect(ctx)
				if !ok {
					return nil
				}
				return chart{
					title: "Wind Speed",
					svg: func() []byte {
						values, _ := driver.Weather.WindSpeed(ctx, city, units, days)
						svg, _ := driver.ChartLine(values.Values, values.Labels, units.WindSpeed())
						return svg
					},
				}
			}
		</div>
	</div>
}

The result:

Dashboard with all charts rendered


Next: UX

Code

./dashboard.gox

package main

import (
	"github.com/doors-dev/doors"
	"github.com/doors-dev/gox"
	"github.com/doors-dev/tutorial/driver"
)

func WeatherDashboard(path doors.Source[Path]) gox.Comp {
	city := doors.DeriveBeam(path, func(p Path) int {
		return p.CityID
	})
	days := doors.DeriveBeam(path, func(p Path) int {
		return p.days()
	})
	units := doors.DeriveBeam(path, func(p Path) driver.Units {
		return p.units()
	})
	return city.Bind(elem(cityID int) {
		~dashboard{
			cityID: cityID,
			days: days,
			units: units,
		}
	})
}

type dashboard struct {
	cityID int
	days doors.Beam[int]
	units doors.Beam[driver.Units]
}

elem (d dashboard) Main() {
	~{
		city, _ := driver.Locations.CitiesGet(d.cityID)
	}
	~(if !city.IsValid() {
		~(doors.Status(404))
		<title>Not Found</title>
		<h1>City Not Found</h1>
	} else {
		<title>~(city.Name) Weather</title>
		<h1>Weather in ~(city.Name, ", ", city.Country.Name)</h1>
	})
	<section>
		~(d.change())
		~(d.menu())
	</section>
	~(if city.IsValid() {
		<section>
			~(d.charts(city))
		</section>
	})
}

elem (d dashboard) change() {
	~>(new(doors.Door)) <>
		~{
			days, _ := d.days.Effect(ctx)
			units, ok := d.units.Effect(ctx)
		}
		~(if ok {
			<a
				class="secondary"
				(doors.ALink{
					Model: Path{
						Route: Selector,
						Days: daysQuery(days),
						Units: unitsQuery(units),
					},
				})
				role="button">
				Change
			</a>
		})
	</>
}

elem (d dashboard) menu() {
	<nav>
		~(d.units.Bind(d.daysNav))
		~(d.days.Bind(d.unitNav))
	</nav>
}

elem (d dashboard) daysNav(units driver.Units) {
	<ul>
		~(for i := range 7 {
			~{
				days := i + 1
			}
			<li>
				~navLink{
					city: d.cityID,
					days: days,
					units: units,
					text: <>
						~(days)
						~(if days == 1 {
							 day
						} else {
							 days
						})
					</>,
				}
			</li>
		})
	</ul>
}

elem (d dashboard) unitNav(days int) {
	<ul>
		~(for _, units := range []driver.Units{driver.Metric, driver.Imperial} {
			<li>
				~navLink{
					city: d.cityID,
					days: days,
					units: units,
					text: units,
				}
			</li>
		})
	</ul>
}

type navLink struct {
	city int
	days int
	units driver.Units
	text any
}

elem (l navLink) Main() {
	<a
		class="secondary"
		(doors.ALink{
			Model: Path{
				Route: Dashboard,
				CityID: l.city,
				Days: daysQuery(l.days),
				Units: unitsQuery(l.units),
			},
		})>
		~(l.text)
	</a>
}

elem (d dashboard) charts(city driver.City) {
	<div class="grid">
		<div>
			~>(new(doors.Door)) ~func {
				days, _ := d.days.Effect(ctx)
				units, ok := d.units.Effect(ctx)
				if !ok {
					return nil
				}
				return chart{
					title: "Temperature",
					svg: func() []byte {
						values, _ := driver.Weather.Temperature(ctx, city, units, days)
						svg, _ := driver.ChartLine(values.Values, values.Labels, units.Temperature())
						return svg
					},
				}
			}
			~>(new(doors.Door)) ~func {
				days, ok := d.days.Effect(ctx)
				if !ok {
					return nil
				}
				return chart{
					title: "Humidity",
					svg: func() []byte {
						values, _ := driver.Weather.Humidity(ctx, city, days)
						svg, _ := driver.ChartLine(values.Values, values.Labels, "%")
						return svg
					},
				}
			}
		</div>
		<div>
			~>(new(doors.Door)) ~func {
				days, ok := d.days.Effect(ctx)
				if !ok {
					return nil
				}
				return chart{
					title: "Weather",
					svg: func() []byte {
						values, _ := driver.Weather.Code(ctx, city, days)
						svg, _ := driver.ChartPie(values.Values)
						return svg
					},
				}
			}
			~>(new(doors.Door)) ~func {
				days, _ := d.days.Effect(ctx)
				units, ok := d.units.Effect(ctx)
				if !ok {
					return nil
				}
				return chart{
					title: "Wind Speed",
					svg: func() []byte {
						values, _ := driver.Weather.WindSpeed(ctx, city, units, days)
						svg, _ := driver.ChartLine(values.Values, values.Labels, units.WindSpeed())
						return svg
					},
				}
			}
		</div>
	</div>
}

type chart struct {
	title string
	svg func() []byte
}

elem (c chart) Main() {
	<article>
		<header>
			~(c.title)
		</header>
		<img height="auto" width="100%" src=(c.svg()) type="image/svg+xml"/>
	</article>
}