Charts
We will generate charts dynamically and serve them through the Resources API.
For resource-backed
srcvalues 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.

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:

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
cachefor 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:

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>
}