|
1 | 1 | <script>
|
2 | 2 | import { onMount } from 'svelte';
|
| 3 | + import Chip, {Set, Icon, Text} from '@smui/chips'; |
3 | 4 | import LinearProgress from '@smui/linear-progress';
|
4 | 5 |
|
5 | 6 | export let data = [];
|
6 | 7 | export let progress = false;
|
7 | 8 | export let error = null;
|
8 | 9 |
|
| 10 | + let time_choice = "Today"; |
| 11 | +
|
9 | 12 | let canvas;
|
10 | 13 | let chart;
|
11 | 14 |
|
12 | 15 | function processData(data, bucketize){
|
13 |
| - var buckets = {}; |
14 |
| - var in_energy = []; |
15 |
| - var out_energy = []; |
| 16 | + let buckets = {}; |
| 17 | + let panel_in = []; |
| 18 | + let panel_usage = []; |
| 19 | + let surplus = []; |
| 20 | + let in_bat = []; |
| 21 | + let out_bat = []; |
16 | 22 |
|
17 | 23 | for (const values of data){
|
18 | 24 | if (values[0] && values[1] && values[2]) {
|
19 |
| - var time = new Date(values[0]); |
| 25 | + let time = new Date(values[0]); |
20 | 26 |
|
21 |
| - var bucket = bucketize(time); |
22 |
| - var bucket_value = buckets[bucket]; |
| 27 | + let bucket = bucketize(time); |
| 28 | + let bucket_value = buckets[bucket]; |
23 | 29 | if (typeof(bucket_value) == "undefined"){
|
24 | 30 | // console.log("Initializing bucket " + new Date(bucket));
|
25 |
| - bucket_value = {"in": 0, "out": 0}; |
| 31 | + bucket_value = {"in": 0, "out": 0, "avail": 0}; |
26 | 32 | buckets[bucket] = bucket_value;
|
27 | 33 | }
|
28 | 34 |
|
29 | 35 | // Converting Watt-Seconds to Watt-Hours (Wh) as we accumulate:
|
30 | 36 | bucket_value["in"] += values[1] / 60;
|
31 | 37 | bucket_value["out"] += values[2] / 60;
|
| 38 | + bucket_value["avail"] += values[3] / 60; |
32 | 39 | }
|
33 | 40 | }
|
34 | 41 |
|
35 | 42 | for (const key of Object.keys(buckets).sort()){
|
36 |
| - var time = new Date(Number(key)); |
37 |
| - in_energy.push({x: time, y: Math.round(buckets[key]["in"])}); |
38 |
| - out_energy.push({x: time, y: Math.round(-buckets[key]["out"])}); |
| 43 | + let time = new Date(Number(key)); |
| 44 | + surplus.push({x: time, y: Math.round(buckets[key]["avail"] - buckets[key]["in"])}); // Stacked bar chart so only want to show the extra available. |
| 45 | + let net_bat_val = Math.round(buckets[key]["in"] - buckets[key]["out"]); |
| 46 | + let in_bat_val = 0; |
| 47 | + let out_bat_val = 0; |
| 48 | + if (net_bat_val >= 0){ |
| 49 | + in_bat_val = net_bat_val; |
| 50 | + } else { |
| 51 | + out_bat_val = net_bat_val; |
| 52 | + } |
| 53 | + in_bat.push({x: time, y: in_bat_val}); |
| 54 | + out_bat.push({x: time, y: out_bat_val}); |
| 55 | + panel_in.push({x: time, y: Math.round(buckets[key]["in"]) - in_bat_val}); |
| 56 | + panel_usage.push({x: time, y: Math.round(-buckets[key]["out"]) - out_bat_val}); |
39 | 57 | }
|
40 | 58 |
|
41 |
| - return [in_energy, out_energy]; |
| 59 | + return [in_bat, out_bat, panel_in, panel_usage, surplus]; |
42 | 60 | }
|
43 | 61 |
|
44 | 62 | function bucketize_15_min(value){
|
45 |
| - var rounded_minutes = Math.floor(value.getMinutes() / 15) * 15; |
46 |
| - var bucket_date = new Date(value) |
| 63 | + let rounded_minutes = Math.floor(value.getMinutes() / 15) * 15; |
| 64 | + let bucket_date = new Date(value) |
47 | 65 | bucket_date.setMinutes(rounded_minutes);
|
48 | 66 | bucket_date.setSeconds(0);
|
49 | 67 | bucket_date.setMilliseconds(0);
|
50 | 68 | return bucket_date.getTime();
|
51 | 69 | }
|
52 | 70 |
|
53 |
| - function createChart(ctx, in_energy, out_energy){ |
54 |
| - var min_time = new Date(); |
55 |
| - min_time.setHours(0); |
56 |
| - min_time.setMinutes(0); |
57 |
| - min_time.setSeconds(0); |
58 |
| - min_time.setMilliseconds(0); |
59 |
| -
|
60 |
| - var max_time = new Date(min_time); |
61 |
| - max_time.setDate(min_time.getDate() + 1); |
| 71 | + function createChart(ctx){ |
| 72 | + const [min_time, max_time] = calculateDateRange(time_choice); |
62 | 73 |
|
63 |
| -
|
64 |
| - var chart = new Chart(ctx, { |
| 74 | + let chart = new Chart(ctx, { |
65 | 75 | type: 'bar',
|
66 | 76 | data: {
|
67 |
| - datasets: [{ |
68 |
| - label: "In", |
69 |
| - data: [], |
70 |
| - fill: false, |
71 |
| - borderColor: "blue", |
72 |
| - backgroundColor: "blue", |
73 |
| - }, { |
74 |
| - label: "Out", |
75 |
| - data: [], |
76 |
| - fill: false, |
77 |
| - borderColor: "orange", |
78 |
| - backgroundColor: "orange", |
79 |
| - }] |
| 77 | + datasets: [ |
| 78 | + { |
| 79 | + label: "Battery Charging", |
| 80 | + data: [], |
| 81 | + fill: false, |
| 82 | + borderColor: "lawngreen", |
| 83 | + backgroundColor: "lawngreen", |
| 84 | + }, |
| 85 | + { |
| 86 | + label: "Battery Usage", |
| 87 | + data: [], |
| 88 | + fill: false, |
| 89 | + borderColor: "orangered", |
| 90 | + backgroundColor: "orangered", |
| 91 | + }, |
| 92 | + { |
| 93 | + label: "Panel In", |
| 94 | + data: [], |
| 95 | + fill: false, |
| 96 | + borderColor: "blue", |
| 97 | + backgroundColor: "blue", |
| 98 | + }, |
| 99 | + { |
| 100 | + label: "Panel Usage", |
| 101 | + data: [], |
| 102 | + fill: false, |
| 103 | + borderColor: "orange", |
| 104 | + backgroundColor: "orange", |
| 105 | + }, |
| 106 | + { |
| 107 | + label: "Panel Surplus", |
| 108 | + data: [], |
| 109 | + fill: false, |
| 110 | + borderColor: "deepskyblue", |
| 111 | + backgroundColor: "deepskyblue", |
| 112 | + }, |
| 113 | + ] |
80 | 114 | },
|
81 | 115 | options: {
|
82 | 116 | scales: {
|
|
115 | 149 |
|
116 | 150 | function updateDataInPlace(existing_data, new_data){
|
117 | 151 | for (const new_value of new_data){
|
118 |
| - var found = false; |
119 |
| - for (var existing_value of existing_data){ |
| 152 | + let found = false; |
| 153 | + for (let existing_value of existing_data){ |
120 | 154 | if (existing_value.x.getTime() == new_value.x.getTime()){
|
121 | 155 | found = true;
|
122 | 156 | existing_value.y = new_value.y;
|
|
129 | 163 | }
|
130 | 164 | }
|
131 | 165 |
|
132 |
| - function updateChart(data){ |
| 166 | + function updateChartData(data){ |
133 | 167 | if (chart !== undefined && data.length > 0){
|
134 |
| - let [in_energy, out_energy] = processData(data, bucketize_15_min); |
135 |
| - updateDataInPlace(chart.data.datasets[0].data, in_energy); |
136 |
| - updateDataInPlace(chart.data.datasets[1].data, out_energy); |
| 168 | + let datasets_data = processData(data, bucketize_15_min); |
| 169 | + for (const [i, dataset_data] of datasets_data.entries()){ |
| 170 | + updateDataInPlace(chart.data.datasets[i].data, dataset_data); |
| 171 | + } |
| 172 | + chart.update(); |
| 173 | + } |
| 174 | + } |
| 175 | +
|
| 176 | +
|
| 177 | + function calculateDateRange(time_choice){ |
| 178 | + let min_time; |
| 179 | + let max_time; |
| 180 | +
|
| 181 | + switch (time_choice){ |
| 182 | + case "Today": |
| 183 | + min_time = new Date(); |
| 184 | + min_time.setHours(0); |
| 185 | + min_time.setMinutes(0); |
| 186 | + min_time.setSeconds(0); |
| 187 | + min_time.setMilliseconds(0); |
| 188 | +
|
| 189 | + max_time = new Date(min_time); |
| 190 | + max_time.setDate(min_time.getDate() + 1); |
| 191 | + return [min_time, max_time]; |
| 192 | +
|
| 193 | + case "Yesterday": |
| 194 | + min_time = new Date(); |
| 195 | + min_time.setHours(0); |
| 196 | + min_time.setMinutes(0); |
| 197 | + min_time.setSeconds(0); |
| 198 | + min_time.setMilliseconds(0); |
| 199 | + min_time.setDate(min_time.getDate() - 1); |
| 200 | +
|
| 201 | + max_time = new Date(min_time); |
| 202 | + max_time.setDate(min_time.getDate() + 1); |
| 203 | + return [min_time, max_time]; |
| 204 | +
|
| 205 | + case "Last 3 days": |
| 206 | + min_time = new Date(); |
| 207 | + min_time.setHours(0); |
| 208 | + min_time.setMinutes(0); |
| 209 | + min_time.setSeconds(0); |
| 210 | + min_time.setMilliseconds(0); |
| 211 | + min_time.setDate(min_time.getDate() - 2); |
| 212 | +
|
| 213 | + max_time = new Date(min_time); |
| 214 | + max_time.setDate(min_time.getDate() + 3); |
| 215 | + return [min_time, max_time]; |
| 216 | +
|
| 217 | + case "Last 7 days": |
| 218 | + min_time = new Date(); |
| 219 | + min_time.setHours(0); |
| 220 | + min_time.setMinutes(0); |
| 221 | + min_time.setSeconds(0); |
| 222 | + min_time.setMilliseconds(0); |
| 223 | + min_time.setDate(min_time.getDate() - 6); |
| 224 | +
|
| 225 | + max_time = new Date(min_time); |
| 226 | + max_time.setDate(min_time.getDate() + 7); |
| 227 | + return [min_time, max_time]; |
| 228 | +
|
| 229 | + } |
| 230 | + } |
| 231 | +
|
| 232 | + function updateChartDateRange(time_choice){ |
| 233 | + if (chart !== undefined){ |
| 234 | + let ticks = chart.options.scales.xAxes[0].ticks; |
| 235 | + [ticks.min, ticks.max] = calculateDateRange(time_choice); |
| 236 | +
|
137 | 237 | chart.update();
|
138 | 238 | }
|
139 | 239 | }
|
140 | 240 |
|
141 |
| - $: updateChart(data); // Update the chart anytime data changes. |
| 241 | + $: updateChartData(data); // Update the chart anytime data changes. |
| 242 | + $: updateChartDateRange(time_choice) // Update the axis date range anytime the selection changes. |
142 | 243 |
|
143 | 244 | async function main(){
|
144 | 245 | chart = createChart(canvas.getContext('2d'));
|
|
147 | 248 | onMount(main);
|
148 | 249 |
|
149 | 250 | </script>
|
150 |
| - |
151 |
| -<h2>Today's Power Usage</h2> |
| 251 | +<h2>{time_choice}{time_choice.endsWith("s") ? "'" : "'s"} Power Usage</h2> |
| 252 | +<Set chips={['Last 7 days', 'Last 3 days', 'Yesterday', 'Today']} let:chip choice bind:selected={time_choice}> |
| 253 | + <Chip tabindex="0">{chip}</Chip> |
| 254 | +</Set> |
152 | 255 | <canvas bind:this={canvas}/>
|
153 | 256 |
|
154 | 257 | <LinearProgress
|
|
0 commit comments