Adding chart generation for host stats

This commit is contained in:
Matthew Stobbs 2024-03-16 11:09:58 -06:00
parent 223496075a
commit d10aea8d8c
8 changed files with 155 additions and 398 deletions

6
go.mod
View File

@ -7,3 +7,9 @@ require (
github.com/go-chi/chi/v5 v5.0.12 github.com/go-chi/chi/v5 v5.0.12
libvirt.org/go/libvirt v1.10001.0 libvirt.org/go/libvirt v1.10001.0
) )
require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/wcharczuk/go-chart/v2 v2.1.1 // indirect
golang.org/x/image v0.11.0 // indirect
)

37
go.sum
View File

@ -2,7 +2,44 @@ github.com/a-h/templ v0.2.598 h1:6jMIHv6wQZvdPxTuv87erW4RqN/FPU0wk7ZHN5wVuuo=
github.com/a-h/templ v0.2.598/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8= github.com/a-h/templ v0.2.598/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/wcharczuk/go-chart/v2 v2.1.1 h1:2u7na789qiD5WzccZsFz4MJWOJP72G+2kUuJoSNqWnE=
github.com/wcharczuk/go-chart/v2 v2.1.1/go.mod h1:CyCAUt2oqvfhCl6Q5ZvAZwItgpQKZOkCJGb+VGv6l14=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
libvirt.org/go/libvirt v1.10001.0 h1:lEVDNE7xfzmZXiDEGIS8NvJSuaz11OjRXw+ufbQEtPY= libvirt.org/go/libvirt v1.10001.0 h1:lEVDNE7xfzmZXiDEGIS8NvJSuaz11OjRXw+ufbQEtPY=
libvirt.org/go/libvirt v1.10001.0/go.mod h1:1WiFE8EjZfq+FCVog+rvr1yatKbKZ9FaFMZgEqxEJqQ= libvirt.org/go/libvirt v1.10001.0/go.mod h1:1WiFE8EjZfq+FCVog+rvr1yatKbKZ9FaFMZgEqxEJqQ=

25
lib/host/charts.go Normal file
View File

@ -0,0 +1,25 @@
package host
import (
"strings"
"github.com/wcharczuk/go-chart/v2"
)
// This file contains utilities to create charts based on the different data
// When a chart is rendered, it can return either an SVG or PNG. SVG is preferrable.
func (h *Host) ChartMemory() string {
c := chart.PieChart{
Width: 256,
Height: 256,
Values: []chart.Value{
{Value: float64(h.NodeMemory.Free / h.NodeMemory.Total), Label: "Free"},
{Value: float64(h.NodeMemory.Cached / h.NodeMemory.Total), Label: "Cached"},
{Value: float64(h.NodeMemory.Buffers / h.NodeMemory.Total), Label: "Buffers"},
},
}
sb := new(strings.Builder)
c.Render(chart.SVG, sb)
return sb.String()
}

21
main.go
View File

@ -4,6 +4,7 @@ import (
"log" "log"
"net/http" "net/http"
"git.staur.ca/stobbsm/clustvirt/lib/host"
"git.staur.ca/stobbsm/clustvirt/view/components" "git.staur.ca/stobbsm/clustvirt/view/components"
"git.staur.ca/stobbsm/clustvirt/view/layouts" "git.staur.ca/stobbsm/clustvirt/view/layouts"
"git.staur.ca/stobbsm/clustvirt/view/static" "git.staur.ca/stobbsm/clustvirt/view/static"
@ -17,11 +18,12 @@ const DEBUG bool = true
func main() { func main() {
log.Println("Starting clustvirt, the libvirt cluster manager") log.Println("Starting clustvirt, the libvirt cluster manager")
//venus, err := host.ConnectHost(host.URI_QEMU_SSH_SYSTEM, "venus.staur.ca") venus, err := host.ConnectHost(host.URI_QEMU_SSH_SYSTEM, "venus.staur.ca")
//if err != nil { if err != nil {
// log.Fatal(err) log.Fatal(err)
//} }
//defer venus.Close() log.Println("Connected to host `venus.staur.ca`")
defer venus.Close()
//lm, err := venus.GetGuestByName("logan-minecraft") //lm, err := venus.GetGuestByName("logan-minecraft")
//if err != nil { //if err != nil {
// log.Fatal(err) // log.Fatal(err)
@ -38,17 +40,16 @@ func main() {
r := chi.NewRouter() r := chi.NewRouter()
r.Use(middleware.Logger) r.Use(middleware.Logger)
if DEBUG {
r.Use(middleware.NoCache)
}
r.Get("/static/*", func(w http.ResponseWriter, r *http.Request) { r.Get("/static/*", func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close() defer r.Body.Close()
if DEBUG {
w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Add("Pragma", "no-cache")
w.Header().Add("Expire", "0")
}
fs.ServeHTTP(w, r) fs.ServeHTTP(w, r)
}) })
r.Get("/", templ.Handler(layouts.Manager("Cluster Manager", "ClustVirt", defaultNavBar)).ServeHTTP) r.Get("/", templ.Handler(layouts.Manager("Cluster Manager", "ClustVirt", defaultNavBar)).ServeHTTP)
r.Get("/about", templ.Handler(static.Home()).ServeHTTP) r.Get("/about", templ.Handler(static.Home()).ServeHTTP)
r.Get("/chart", templ.Handler(components.HostInfo(venus)).ServeHTTP)
log.Println(http.ListenAndServe(":3000", r)) log.Println(http.ListenAndServe(":3000", r))
} }

1
public/chart.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256">\n<path d="M 0 0 L 256 0 L 256 256 L 0 256 L 0 0" style="stroke-width:0;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)"/><path d="M 5 5 L 251 5 L 251 251 L 5 251 L 5 5" style="stroke-width:0;stroke:rgba(255,255,255,1.0);fill:rgba(255,255,255,1.0)"/><circle cx="128" cy="128" r="123" style="stroke-width:5;stroke:rgba(255,255,255,1.0);fill:rgba(106,195,203,1.0)"/><text x="33" y="134" style="stroke-width:0;stroke:none;fill:rgba(51,51,51,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif">Free</text></svg>

After

Width:  |  Height:  |  Size: 634 B

View File

@ -822,45 +822,12 @@ span>a:visited {
position: static; position: static;
} }
.order-3 {
order: 3;
}
.order-2 { .order-2 {
order: 2; order: 2;
} }
.float-left { .order-3 {
float: left; order: 3;
}
.m-5 {
margin: 1.25rem;
}
.mx-5 {
margin-left: 1.25rem;
margin-right: 1.25rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.my-8 {
margin-top: 2rem;
margin-bottom: 2rem;
}
.mx-2 {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.my-2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
} }
.mx-4 { .mx-4 {
@ -868,7 +835,8 @@ span>a:visited {
margin-right: 1rem; margin-right: 1rem;
} }
.mr-auto { .mx-auto {
margin-left: auto;
margin-right: auto; margin-right: auto;
} }
@ -876,250 +844,38 @@ span>a:visited {
display: block; display: block;
} }
.inline-block {
display: inline-block;
}
.flex { .flex {
display: flex; display: flex;
} }
.inline-flex {
display: inline-flex;
}
.contents { .contents {
display: contents; display: contents;
} }
.hidden { .h-28 {
display: none; height: 7rem;
}
.size-fit {
width: -moz-fit-content;
width: fit-content;
height: -moz-fit-content;
height: fit-content;
}
.size-max {
width: -moz-max-content;
width: max-content;
height: -moz-max-content;
height: max-content;
}
.size-72 {
width: 18rem;
height: 18rem;
}
.size-64 {
width: 16rem;
height: 16rem;
}
.h-4 {
height: 1rem;
}
.h-8 {
height: 2rem;
}
.h-full {
height: 100%;
} }
.h-6 { .h-6 {
height: 1.5rem; height: 1.5rem;
} }
.h-12 { .h-full {
height: 3rem; height: 100%;
}
.h-14 {
height: 3.5rem;
}
.h-10 {
height: 2.5rem;
}
.h-9 {
height: 2.25rem;
}
.h-11 {
height: 2.75rem;
}
.h-28 {
height: 7rem;
}
.h-44 {
height: 11rem;
}
.h-52 {
height: 13rem;
}
.h-36 {
height: 9rem;
}
.h-40 {
height: 10rem;
}
.h-max {
height: -moz-max-content;
height: max-content;
}
.h-auto {
height: auto;
}
.h-48 {
height: 12rem;
}
.h-56 {
height: 14rem;
}
.h-64 {
height: 16rem;
}
.h-72 {
height: 18rem;
}
.h-fit {
height: -moz-fit-content;
height: fit-content;
}
.w-full {
width: 100%;
}
.w-9\/12 {
width: 75%;
}
.w-80 {
width: 20rem;
}
.w-fit {
width: -moz-fit-content;
width: fit-content;
}
.w-10\/12 {
width: 83.333333%;
}
.w-11\/12 {
width: 91.666667%;
}
.w-6 {
width: 1.5rem;
}
.w-40 {
width: 10rem;
}
.w-44 {
width: 11rem;
}
.w-auto {
width: auto;
}
.w-48 {
width: 12rem;
}
.w-52 {
width: 13rem;
}
.w-64 {
width: 16rem;
}
.w-72 {
width: 18rem;
}
.w-max {
width: -moz-max-content;
width: max-content;
} }
.flex-auto { .flex-auto {
flex: 1 1 auto; flex: 1 1 auto;
} }
.flex-grow {
flex-grow: 1;
}
.grow {
flex-grow: 1;
}
.basis-1\/2 { .basis-1\/2 {
flex-basis: 50%; flex-basis: 50%;
} }
.basis-1\/3 {
flex-basis: 33.333333%;
}
.basis-1\/4 { .basis-1\/4 {
flex-basis: 25%; flex-basis: 25%;
} }
.basis-2\/3 {
flex-basis: 66.666667%;
}
.basis-4\/12 {
flex-basis: 33.333333%;
}
.basis-5\/12 {
flex-basis: 41.666667%;
}
.basis-7\/12 {
flex-basis: 58.333333%;
}
.basis-8\/12 {
flex-basis: 66.666667%;
}
.basis-2\/4 {
flex-basis: 50%;
}
.basis-2\/5 {
flex-basis: 40%;
}
.list-inside { .list-inside {
list-style-position: inside; list-style-position: inside;
} }
@ -1140,10 +896,6 @@ span>a:visited {
list-style-image: url("/static/icons/list-possible.svg"); list-style-image: url("/static/icons/list-possible.svg");
} }
.flex-row {
flex-direction: row;
}
.flex-col { .flex-col {
flex-direction: column; flex-direction: column;
} }
@ -1160,18 +912,10 @@ span>a:visited {
align-items: center; align-items: center;
} }
.justify-center {
justify-content: center;
}
.justify-between { .justify-between {
justify-content: space-between; justify-content: space-between;
} }
.justify-around {
justify-content: space-around;
}
.gap-2 { .gap-2 {
gap: 0.5rem; gap: 0.5rem;
} }
@ -1180,11 +924,6 @@ span>a:visited {
gap: 1rem; gap: 1rem;
} }
.gap-x-2 {
-moz-column-gap: 0.5rem;
column-gap: 0.5rem;
}
.divide-solid > :not([hidden]) ~ :not([hidden]) { .divide-solid > :not([hidden]) ~ :not([hidden]) {
border-style: solid; border-style: solid;
} }
@ -9199,22 +8938,6 @@ span>a:visited {
border-color: rgb(51 51 30 / 0.95); border-color: rgb(51 51 30 / 0.95);
} }
.self-start {
align-self: flex-start;
}
.self-center {
align-self: center;
}
.justify-self-end {
justify-self: end;
}
.justify-self-center {
justify-self: center;
}
.rounded { .rounded {
border-radius: 0.25rem; border-radius: 0.25rem;
} }
@ -93288,11 +93011,6 @@ span>a:visited {
background-color: rgb(51 51 30 / 0.95); background-color: rgb(51 51 30 / 0.95);
} }
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.from-uiblue-100 { .from-uiblue-100 {
--tw-gradient-from: #dde7f8 var(--tw-gradient-from-position); --tw-gradient-from: #dde7f8 var(--tw-gradient-from-position);
--tw-gradient-to: rgb(221 231 248 / 0) var(--tw-gradient-to-position); --tw-gradient-to: rgb(221 231 248 / 0) var(--tw-gradient-to-position);
@ -138837,28 +138555,14 @@ span>a:visited {
padding: 0.5rem; padding: 0.5rem;
} }
.px-8 {
padding-left: 2rem;
padding-right: 2rem;
}
.py-8 {
padding-top: 2rem;
padding-bottom: 2rem;
}
.align-bottom {
vertical-align: bottom;
}
.text-2xl { .text-2xl {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
} }
.text-sm { .text-lg {
font-size: 0.875rem; font-size: 1.125rem;
line-height: 1.25rem; line-height: 1.75rem;
} }
.text-xl { .text-xl {
@ -138866,21 +138570,6 @@ span>a:visited {
line-height: 1.75rem; line-height: 1.75rem;
} }
.text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.font-bold { .font-bold {
font-weight: 700; font-weight: 700;
} }
@ -138889,10 +138578,6 @@ span>a:visited {
font-weight: 600; font-weight: 600;
} }
.font-thin {
font-weight: 100;
}
.italic { .italic {
font-style: italic; font-style: italic;
} }
@ -220454,28 +220139,20 @@ span>a:visited {
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:hidden {
display: none;
}
.sm\:w-full { .sm\:w-full {
width: 100%; width: 100%;
} }
.sm\:flex-col {
flex-direction: column;
}
.sm\:justify-start {
justify-content: flex-start;
}
.sm\:gap-8 { .sm\:gap-8 {
gap: 2rem; gap: 2rem;
} }
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.md\:order-1 {
order: 1;
}
.md\:order-2 { .md\:order-2 {
order: 2; order: 2;
} }
@ -220484,34 +220161,10 @@ span>a:visited {
order: 3; order: 3;
} }
.md\:order-1 {
order: 1;
}
.md\:block {
display: block;
}
.md\:inline-block {
display: inline-block;
}
.md\:inline-flex {
display: inline-flex;
}
.md\:contents {
display: contents;
}
.md\:h-16 { .md\:h-16 {
height: 4rem; height: 4rem;
} }
.md\:w-10\/12 {
width: 83.333333%;
}
.md\:w-auto { .md\:w-auto {
width: auto; width: auto;
} }
@ -220538,29 +220191,3 @@ span>a:visited {
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
} }
@media (min-width: 1024px) {
.lg\:h-8 {
height: 2rem;
}
.lg\:flex-row {
flex-direction: row;
}
.lg\:items-end {
align-items: flex-end;
}
.lg\:items-baseline {
align-items: baseline;
}
.lg\:justify-start {
justify-content: flex-start;
}
.lg\:justify-center {
justify-content: center;
}
}

View File

@ -0,0 +1,12 @@
package components
import (
"git.staur.ca/stobbsm/clustvirt/lib/host"
)
// HostInfo is meant to be an HTMX response
templ HostInfo(h *host.Host) {
<body>
@templ.Raw(h.ChartMemory())
</body>
}

View File

@ -0,0 +1,48 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.598
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import (
"git.staur.ca/stobbsm/clustvirt/lib/host"
)
// HostInfo is meant to be an HTMX response
func HostInfo(h *host.Host) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(h.ChartMemory()).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}