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() {
	/* ... */
	~(doors.Sub(d.settings, elem(s Settings) {
		<section>
			~(d.menu(s))
		</section>
		~// display charts only when the city is valid
		~(if city.IsValid() {
			<section>
				~(d.charts(city, s))
			</section>
		})
	}))
}

// component with all charts
elem (d dashboard) charts(c driver.City, s Settings) {
	<div class="grid">
		<div>
			~(d.temperature(c, s))
		</div>
		<div></div>
	</div>
}

elem (d dashboard) temperature(c driver.City, s Settings) {
}

Next, generate and serve the temperature chart:

elem (d dashboard) temperature(c driver.City, s Settings) {
	~{
		values, _ := driver.Weather.Temperature(ctx, c, s.Units, s.Days)
		// generate []byte
		svg, _ := driver.ChartLine(values.Values, values.Labels, s.Units.Temperature())
	}
	<article>
		~// pass []byte directly into src
		<img height="auto" width="100%" src=(svg) type="image/svg+xml"/>
	</article>
}

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:

elem (d dashboard) charts(c driver.City, s Settings) {
	<div class="grid">
		<div>
			~chart{
				title: "Temperature",
				svg: func() []byte {
					values, _ := driver.Weather.Temperature(ctx, c, s.Units, s.Days)
					svg, _ := driver.ChartLine(values.Values, values.Labels, s.Units.Temperature())
					return svg
				},
			}
			~chart{
				title: "Humidity",
				svg: func() []byte {
					values, _ := driver.Weather.Humidity(ctx, c, s.Days)
					svg, _ := driver.ChartLine(values.Values, values.Labels, "%")
					return svg
				},
			}
		</div>
		<div>
			~chart{
				title: "Weather",
				svg: func() []byte {
					values, _ := driver.Weather.Code(ctx, c, s.Days)
					svg, _ := driver.ChartPie(values.Values)
					return svg
				},
			}
			~chart{
				title: "Wind Speed",
				svg: func() []byte {
					values, _ := driver.Weather.WindSpeed(ctx, c, s.Units, s.Days)
					svg, _ := driver.ChartLine(values.Values, values.Labels, s.Units.WindSpeed())
					return svg
				},
			}
		</div>
	</div>
}

The result:

Dashboard with all charts rendered

Free weather API is pretty slow, some feedback to user is required.

UX

Navigation

Add a simple loader to the chart header:

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

And now trigger the loader on all dashboard nav links:

This uses the selector-based helpers from Indication.

<a
	class="secondary"
	(doors.ALink{
		// set aria-busy on all .chart-loader elements
		Indicator: doors.IndicatorOnlyAttrQueryAll(".chart-loader", "aria-busy", "true"),
		Active: doors.Active{
			Indicator: doors.IndicatorOnlyAttr("aria-current", "true"),
		},
		Model: Path{
			Dashboard: true,
			CityID: d.city,
			Days: s.DaysQuery(),
			Units: s.UnitsQuery(),
		},
	})>
	~/* ... */
</a>

Preloader

At this point we also need a bit of CSS.

Put this file in the project root:

style.css

.img-wrapper {
	position: relative;
	aspect-ratio: 3 / 2;
	width: 100%;
}

.img-loader {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
	z-index: 0;
}

.img-wrapper img {
	position: relative;
	z-index: 1;
}

Next, embed and serve it from the app:

//go:embed style.css
var styles []byte

elem (a App) Main() {
	~/* ... */
	<head>
		/* ... */
		<link href=(styles) rel="stylesheet" cache>
	</head>
	~/* ... */
}

Here we added the cache attribute to the stylesheet link. That makes it behave like a regular public static link with a hash-based URL.

For stylesheet resources and cache, see Styles.

Cached resources are stored in RAM. Do not use cache for generated or heavy content.

Add a preloader for SVGs:

elem (c chart) Main() {
	<article>
		<header>
			~(c.title, " ")
			<span class="chart-loader"></span>
		</header>
		<div class="img-wrapper">
			<img height="auto" width="100%" src=(c.svg()) type="image/svg+xml"/>
			<div class="img-loader" aria-busy="true"></div>
		</div>
	</article>
}

Charts with preloaders:

Chart preloaders during navigation


Next: Optimization

Code

./dashboard.gox

package main

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

type dashboard struct {
	city int
	settings doors.Beam[Settings]
}

elem (d dashboard) Main() {
	~{
		city, _ := driver.Locations.CitiesGet(d.city)
	}
	~(d.header(city))
	~(doors.Sub(d.settings, elem(s Settings) {
		<section>
			~(d.menu(s))
		</section>
		~(if city.IsValid() {
			<section>
				~(d.charts(city, s))
			</section>
		})
	}))
}

elem (d dashboard) header(city driver.City) {
	~(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>
	})
}

elem (d dashboard) menu(s Settings) {
	<a
		class="secondary"
		(doors.ALink{
			Model: Path{
				Home: true,
				Days: s.DaysQuery(),
				Units: s.UnitsQuery(),
			},
		})
		role="button">
		Change
	</a>
	<nav>
		~(d.navDays(s))
		~(d.navUnits(s))
	</nav>
}

elem (d dashboard) navDays(s Settings) {
	<ul>
		~(for i := range 7 {
			~{
				s.Days = i + 1
			}
			<li>
				<a
					class="secondary"
					(doors.ALink{
						Indicator: doors.IndicatorOnlyAttrQueryAll(".chart-loader", "aria-busy", "true"),
						Active: doors.Active{
							Indicator: doors.IndicatorOnlyAttr("aria-current", "true"),
						},
						Model: Path{
							Dashboard: true,
							CityID: d.city,
							Days: s.DaysQuery(),
							Units: s.UnitsQuery(),
						},
					})>
					~(s.Days)
					~(if s.Days == 1 {
						 day
					} else {
						 days
					})
				</a>
			</li>
		})
	</ul>
}

elem (d dashboard) navUnits(s Settings) {
	<ul>
		~(for _, unit := range []driver.Units{driver.Metric, driver.Imperial} {
			<li>
				~{
					s.Units = unit
				}
				<a
					class="secondary"
					(doors.ALink{
						Indicator: doors.IndicatorOnlyAttrQueryAll(".chart-loader", "aria-busy", "true"),
						Active: doors.Active{
							Indicator: doors.IndicatorOnlyAttr("aria-current", "true"),
						},
						Model: Path{
							Dashboard: true,
							CityID: d.city,
							Days: s.DaysQuery(),
							Units: s.UnitsQuery(),
						},
					})>
					~(unit.String())
				</a>
			</li>
		})
	</ul>
}

elem (d dashboard) charts(c driver.City, s Settings) {
	<div class="grid">
		<div>
			~chart{
				title: "Temperature",
				svg: func() []byte {
					values, _ := driver.Weather.Temperature(ctx, c, s.Units, s.Days)
					svg, _ := driver.ChartLine(values.Values, values.Labels, s.Units.Temperature())
					return svg
				},
			}
			~chart{
				title: "Humidity",
				svg: func() []byte {
					values, _ := driver.Weather.Humidity(ctx, c, s.Days)
					svg, _ := driver.ChartLine(values.Values, values.Labels, "%")
					return svg
				},
			}
		</div>
		<div>
			~chart{
				title: "Weather",
				svg: func() []byte {
					values, _ := driver.Weather.Code(ctx, c, s.Days)
					svg, _ := driver.ChartPie(values.Values)
					return svg
				},
			}
			~chart{
				title: "Wind Speed",
				svg: func() []byte {
					values, _ := driver.Weather.WindSpeed(ctx, c, s.Units, s.Days)
					svg, _ := driver.ChartLine(values.Values, values.Labels, s.Units.WindSpeed())
					return svg
				},
			}
		</div>
	</div>
}

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

elem (c chart) Main() {
	<article>
		<header>
			~(c.title, " ")
			<span class="chart-loader"></span>
		</header>
		<div class="img-wrapper">
			<img height="auto" width="100%" src=(c.svg()) type="image/svg+xml"/>
			<div class="img-loader" aria-busy="true"></div>
		</div>
	</article>
}