github.com/serge-v/zero@v1.0.2-0.20220911142406-af4b6a19e68a/examples/chart/main.go (about) 1 package main 2 3 import ( 4 "flag" 5 "fmt" 6 "io" 7 "log" 8 "net/http" 9 "os" 10 "text/template" 11 "time" 12 13 "github.com/serge-v/zero" 14 "github.com/wcharczuk/go-chart/v2" 15 16 _ "embed" 17 _ "time/tzdata" 18 ) 19 20 var deploy = flag.Bool("deploy", false, "") 21 var standalone = flag.Bool("standalone", false, "") 22 23 func main() { 24 flag.Parse() 25 log.SetFlags(log.LstdFlags | log.Lshortfile) 26 27 if *standalone { 28 if err := saveChart("sensor1", "1.png"); err != nil { 29 log.Fatal(err) 30 } 31 return 32 } 33 34 if *deploy { 35 if err := zero.Deploy(8096); err != nil { 36 log.Fatal(err) 37 } 38 return 39 } 40 41 http.HandleFunc("/", handleRoot) 42 http.HandleFunc("/chart.png", handleChart) 43 http.HandleFunc("/chart.svg", handleSVGChart) 44 http.HandleFunc("/data.csv", handleCsv) 45 http.HandleFunc("/events.csv", handleEvents) 46 47 log.Println("starting on http://127.0.0.1:8096") 48 49 if err := http.ListenAndServe("127.0.0.1:8096", nil); err != nil { 50 log.Fatal(err) 51 } 52 } 53 54 //go:embed main.html 55 var mainPage string 56 57 //go:embed chart.svg 58 var chartTemplate string 59 60 func handleRoot(w http.ResponseWriter, r *http.Request) { 61 fmt.Fprintln(w, mainPage) 62 } 63 64 func handleChart(w http.ResponseWriter, r *http.Request) { 65 fname := r.URL.Query().Get("f") 66 w.Header().Set("Content-Type", "image/png") 67 generateChart(w, fname) 68 } 69 70 func handleSVGChart(w http.ResponseWriter, r *http.Request) { 71 fname := r.URL.Query().Get("f") 72 log.Println("chart svg") 73 w.Header().Set("Content-Type", "image/svg+xml") 74 if err := generateSVGChart(w, fname); err != nil { 75 log.Println(err) 76 } 77 } 78 79 func handleEvents(w http.ResponseWriter, r *http.Request) { 80 now := time.Now().Truncate(time.Hour * 24).Add(time.Hour * 4) 81 fmt.Fprint(w, "time,event\n") 82 for i := -3; i < 1; i++ { 83 ts := now.Add(time.Hour * 24 * time.Duration(i)) 84 fmt.Fprint(w, ts.Format(time.RFC3339), ",midnight", "\n") 85 ts = now.Add(time.Hour*24*time.Duration(i) + time.Hour*12) 86 fmt.Fprint(w, ts.Format(time.RFC3339), ",noon", "\n") 87 } 88 } 89 90 func handleCsv(w http.ResponseWriter, r *http.Request) { 91 fname := r.URL.Query().Get("f") 92 lines, err := fetchSensorLog(fname) 93 if err != nil { 94 http.Error(w, err.Error(), http.StatusInternalServerError) 95 return 96 } 97 98 records, err := parseLogLines(lines) 99 if err != nil { 100 http.Error(w, err.Error(), http.StatusInternalServerError) 101 return 102 } 103 104 w.Header().Set("Access-Control-Allow-Origin", "*") 105 fmt.Fprint(w, "time,moisture,battery,hall\n") 106 for _, r := range records { 107 fmt.Fprint(w, r.ts.Add(time.Hour*4).Format(time.RFC3339), ",", r.moisture, ",", fmt.Sprintf("%.2f", r.battery), ",", r.hall, "\n") 108 } 109 } 110 111 func saveChart(fname, outfname string) error { 112 f, err := os.Create(outfname) 113 if err != nil { 114 return fmt.Errorf("save: %w", err) 115 } 116 defer f.Close() 117 if err := generateChart(f, fname); err != nil { 118 return fmt.Errorf("generate: %w", err) 119 } 120 return nil 121 } 122 123 func generateChart(w io.Writer, fname string) error { 124 lines, err := fetchSensorLog(fname) 125 if err != nil { 126 return fmt.Errorf("fetch: %w", err) 127 } 128 129 records, err := parseLogLines(lines) 130 if err != nil { 131 return fmt.Errorf("parse: %w", err) 132 } 133 134 var xvalues []time.Time 135 var yvalues []float64 136 137 for _, r := range records { 138 xvalues = append(xvalues, r.ts) 139 yvalues = append(yvalues, float64(r.moisture)) 140 } 141 142 var ygrid []chart.GridLine 143 144 st := chart.Style{DotWidth: 1, DotColor: chart.ColorBlue, StrokeWidth: chart.Disabled} 145 lowHumidity := chart.Style{StrokeWidth: 3, StrokeColor: chart.ColorRed} 146 147 for i := 300; i < 800; i += 50 { 148 ygrid = append(ygrid, chart.GridLine{Value: float64(i)}) 149 ygrid = append(ygrid, chart.GridLine{Value: float64(i + 25), Style: st}) 150 } 151 ygrid = append(ygrid, chart.GridLine{Value: 525, Style: lowHumidity}) 152 153 var yticks []chart.Tick 154 for i := 300; i < 800; i += 50 { 155 // yticks = append(yticks, chart.Tick{Value: float64(i), Label: fmt.Sprintf("%d", i)}) 156 } 157 158 yticks = append(yticks, chart.Tick{Value: 625, Label: "high"}) 159 yticks = append(yticks, chart.Tick{Value: 575, Label: "norm"}) 160 yticks = append(yticks, chart.Tick{Value: 525, Label: "low"}) 161 162 graph := chart.Chart{ 163 DPI: 144, 164 Series: []chart.Series{ 165 chart.TimeSeries{ 166 XValues: xvalues, 167 YValues: yvalues, 168 }, 169 }, 170 YAxis: chart.YAxis{ 171 GridMajorStyle: chart.Style{ 172 StrokeColor: chart.ColorAlternateGray, 173 StrokeWidth: 0.5, 174 }, 175 GridLines: ygrid, 176 Ticks: yticks, 177 Range: &chart.ContinuousRange{ 178 Min: 200, 179 Max: 800, 180 }, 181 }, 182 XAxis: chart.XAxis{ 183 ValueFormatter: chart.TimeHourValueFormatter, 184 GridMajorStyle: chart.Style{ 185 StrokeColor: chart.ColorAlternateGray, 186 StrokeWidth: 1.0, 187 }, 188 GridLines: dayEventsLines(), 189 Ticks: xTicks(), 190 }, 191 } 192 if err := graph.Render(chart.PNG, w); err != nil { 193 return fmt.Errorf("render: %w", err) 194 } 195 return nil 196 } 197 198 func generateSVGChart(w io.Writer, fname string) error { 199 lines, err := fetchSensorLog(fname) 200 if err != nil { 201 return fmt.Errorf("fetch: %w", err) 202 } 203 204 /* 205 buf, err := ioutil.ReadFile("sensor.log") 206 if err != nil { 207 return fmt.Errorf("read sensor log: %w", err) 208 } 209 */ 210 // lines = strings.Split(string(buf), "\n") 211 212 records, err := parseLogLines(lines) 213 if err != nil { 214 return fmt.Errorf("parse: %w", err) 215 } 216 217 type line struct { 218 X1, Y1, X2, Y2 int 219 Class string 220 } 221 222 type data struct { 223 Lines []line 224 DayLabels []string 225 Polyline string 226 } 227 228 d := data{} 229 year, month, day := time.Now().Add(time.Hour * 24).Date() 230 loc, err := time.LoadLocation("America/New_York") 231 if err != nil { 232 return err 233 } 234 235 start := time.Date(year, month, day, 0, 0, 0, 0, loc) 236 237 for i := 0; i <= 5; i++ { 238 ts := start.Add(time.Duration(-i) * time.Hour * 24).Format("1/2") 239 d.DayLabels = append(d.DayLabels, ts) 240 } 241 242 if len(records) > 110 { 243 records = records[len(records)-110:] 244 } 245 246 var prev line 247 248 for i, r := range records { 249 var ln line 250 251 if r.moisture < 410 { 252 r.moisture = 410 253 } 254 255 ln.X1 = int(start.Sub(r.ts).Minutes()) - 240 256 ln.X2 = ln.X1 + 60 257 ln.Y1 = r.moisture 258 ln.Y2 = r.moisture 259 ln.Class = "mhor" 260 261 println("=== ", start.String(), r.ts.UTC().String(), ln.X1) 262 263 if i > 0 { 264 ln2 := line{X1: prev.X1, X2: prev.X1, Y1: prev.Y1, Y2: ln.Y1, Class: "mver"} 265 d.Lines = append(d.Lines, ln2) 266 } 267 268 d.Lines = append(d.Lines, ln) 269 prev = ln 270 } 271 272 var t *template.Template 273 274 _, err = os.Stat("chart.svg") 275 if err != nil { 276 t, err = template.New("").Parse(chartTemplate) 277 } else { 278 t, err = template.ParseFiles("chart.svg") 279 } 280 if err != nil { 281 return fmt.Errorf("parse template: %w", err) 282 } 283 284 if err := t.Execute(w, d); err != nil { 285 return fmt.Errorf("execute: %w", err) 286 } 287 288 return nil 289 }