something's working...
This commit is contained in:
parent
5eba13e51e
commit
7d298eb48b
12 changed files with 2486 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
.env
|
||||||
1969
Cargo.lock
generated
Normal file
1969
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "solarmon"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = "0.8.6"
|
||||||
|
chrono = "0.4.42"
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
tokio = { version = "1.47", features = ["full"] }
|
||||||
|
tower-http = { version = "0.6", features = ["fs"] }
|
||||||
|
tower = "0.5.0"
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-subscriber = "0.3.20"
|
||||||
|
anyhow = "1.0"
|
||||||
|
tower-livereload = "0.9.6"
|
||||||
39
src/details.json
Normal file
39
src/details.json
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"details": {
|
||||||
|
"id": 2193535,
|
||||||
|
"name": "S. Hautvast",
|
||||||
|
"accountId": 30166,
|
||||||
|
"status": "Active",
|
||||||
|
"peakPower": 3.51,
|
||||||
|
"lastUpdateTime": "2025-10-07",
|
||||||
|
"installationDate": "2021-04-13",
|
||||||
|
"ptoDate": null,
|
||||||
|
"notes": "",
|
||||||
|
"type": "Optimizers & Inverters",
|
||||||
|
"location": {
|
||||||
|
"country": "Netherlands",
|
||||||
|
"city": "Amsterdam",
|
||||||
|
"address": "A. Moenstraat, 22",
|
||||||
|
"address2": "",
|
||||||
|
"zip": "1022 KJ",
|
||||||
|
"timeZone": "Europe/Amsterdam",
|
||||||
|
"countryCode": "NL",
|
||||||
|
"latitude": "52.4069905",
|
||||||
|
"longitude": "4.932790799999999"
|
||||||
|
},
|
||||||
|
"primaryModule": {
|
||||||
|
"manufacturerName": "Hyundai Heavy Industries",
|
||||||
|
"modelName": "HiE-S390VG",
|
||||||
|
"maximumPower": 0.0,
|
||||||
|
"temperatureCoef": 0.0
|
||||||
|
},
|
||||||
|
"alertQuantity": 0,
|
||||||
|
"highestImpact": "0",
|
||||||
|
"uris": {
|
||||||
|
"DETAILS": "/site/2193535/details",
|
||||||
|
"DATA_PERIOD": "/site/2193535/dataPeriod",
|
||||||
|
"OVERVIEW": "/site/2193535/overview"
|
||||||
|
},
|
||||||
|
"publicSettings": { "isPublic": null }
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/energy.json
Normal file
104
src/energy.json
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
{
|
||||||
|
"energy": {
|
||||||
|
"timeUnit": "QUARTER_OF_AN_HOUR",
|
||||||
|
"unit": "Wh",
|
||||||
|
"values": [
|
||||||
|
{ "date": "2025-10-06 00:00:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 00:15:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 00:30:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 00:45:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 01:00:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 01:15:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 01:30:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 01:45:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 02:00:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 02:15:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 02:30:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 02:45:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 03:00:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 03:15:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 03:30:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 03:45:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 04:00:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 04:15:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 04:30:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 04:45:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 05:00:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 05:15:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 05:30:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 05:45:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 06:00:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 06:15:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 06:30:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 06:45:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 07:00:00", "value": 0.0 },
|
||||||
|
{ "date": "2025-10-06 07:15:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 07:30:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 07:45:00", "value": 0.0 },
|
||||||
|
{ "date": "2025-10-06 08:00:00", "value": 0.0 },
|
||||||
|
{ "date": "2025-10-06 08:15:00", "value": 0.0 },
|
||||||
|
{ "date": "2025-10-06 08:30:00", "value": 0.0 },
|
||||||
|
{ "date": "2025-10-06 08:45:00", "value": 0.0 },
|
||||||
|
{ "date": "2025-10-06 09:00:00", "value": 0.0 },
|
||||||
|
{ "date": "2025-10-06 09:15:00", "value": 1.0 },
|
||||||
|
{ "date": "2025-10-06 09:30:00", "value": 21.0 },
|
||||||
|
{ "date": "2025-10-06 09:45:00", "value": 14.0 },
|
||||||
|
{ "date": "2025-10-06 10:00:00", "value": 29.0 },
|
||||||
|
{ "date": "2025-10-06 10:15:00", "value": 38.0 },
|
||||||
|
{ "date": "2025-10-06 10:30:00", "value": 48.0 },
|
||||||
|
{ "date": "2025-10-06 10:45:00", "value": 44.0 },
|
||||||
|
{ "date": "2025-10-06 11:00:00", "value": 54.0 },
|
||||||
|
{ "date": "2025-10-06 11:15:00", "value": 53.0 },
|
||||||
|
{ "date": "2025-10-06 11:30:00", "value": 58.0 },
|
||||||
|
{ "date": "2025-10-06 11:45:00", "value": 35.0 },
|
||||||
|
{ "date": "2025-10-06 12:00:00", "value": 25.0 },
|
||||||
|
{ "date": "2025-10-06 12:15:00", "value": 28.0 },
|
||||||
|
{ "date": "2025-10-06 12:30:00", "value": 56.0 },
|
||||||
|
{ "date": "2025-10-06 12:45:00", "value": 56.0 },
|
||||||
|
{ "date": "2025-10-06 13:00:00", "value": 63.0 },
|
||||||
|
{ "date": "2025-10-06 13:15:00", "value": 183.0 },
|
||||||
|
{ "date": "2025-10-06 13:30:00", "value": 180.0 },
|
||||||
|
{ "date": "2025-10-06 13:45:00", "value": 104.0 },
|
||||||
|
{ "date": "2025-10-06 14:00:00", "value": 68.0 },
|
||||||
|
{ "date": "2025-10-06 14:15:00", "value": 67.0 },
|
||||||
|
{ "date": "2025-10-06 14:30:00", "value": 60.0 },
|
||||||
|
{ "date": "2025-10-06 14:45:00", "value": 57.0 },
|
||||||
|
{ "date": "2025-10-06 15:00:00", "value": 25.0 },
|
||||||
|
{ "date": "2025-10-06 15:15:00", "value": 42.0 },
|
||||||
|
{ "date": "2025-10-06 15:30:00", "value": 24.0 },
|
||||||
|
{ "date": "2025-10-06 15:45:00", "value": 11.0 },
|
||||||
|
{ "date": "2025-10-06 16:00:00", "value": 11.0 },
|
||||||
|
{ "date": "2025-10-06 16:15:00", "value": 9.0 },
|
||||||
|
{ "date": "2025-10-06 16:30:00", "value": 9.0 },
|
||||||
|
{ "date": "2025-10-06 16:45:00", "value": 3.0 },
|
||||||
|
{ "date": "2025-10-06 17:00:00", "value": 0.0 },
|
||||||
|
{ "date": "2025-10-06 17:15:00", "value": 2.0 },
|
||||||
|
{ "date": "2025-10-06 17:30:00", "value": 1.0 },
|
||||||
|
{ "date": "2025-10-06 17:45:00", "value": 1.0 },
|
||||||
|
{ "date": "2025-10-06 18:00:00", "value": 0.0 },
|
||||||
|
{ "date": "2025-10-06 18:15:00", "value": 0.0 },
|
||||||
|
{ "date": "2025-10-06 18:30:00", "value": 0.0 },
|
||||||
|
{ "date": "2025-10-06 18:45:00", "value": 0.0 },
|
||||||
|
{ "date": "2025-10-06 19:00:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 19:15:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 19:30:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 19:45:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 20:00:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 20:15:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 20:30:00", "value": 0.0 },
|
||||||
|
{ "date": "2025-10-06 20:45:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 21:00:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 21:15:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 21:30:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 21:45:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 22:00:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 22:15:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 22:30:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 22:45:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 23:00:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 23:15:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 23:30:00", "value": null },
|
||||||
|
{ "date": "2025-10-06 23:45:00", "value": null }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/main.rs
Normal file
93
src/main.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
use axum::{
|
||||||
|
Extension, Json, Router,
|
||||||
|
response::{ErrorResponse, Html},
|
||||||
|
routing::get,
|
||||||
|
};
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use dotenv::dotenv;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tower::ServiceBuilder;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
use tower_livereload::LiveReloadLayer;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
dotenv().ok();
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/api/energy", get(energy))
|
||||||
|
.route("/", get(index))
|
||||||
|
.nest_service(
|
||||||
|
"/static",
|
||||||
|
ServiceBuilder::new().service(ServeDir::new("static")),
|
||||||
|
)
|
||||||
|
.layer(LiveReloadLayer::new());
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||||
|
println!("server on {}", listener.local_addr().unwrap());
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn index() -> Html<String> {
|
||||||
|
let html = format!(
|
||||||
|
r#"html<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="refresh" content="0; url=/static/index.html">
|
||||||
|
</head>"#
|
||||||
|
);
|
||||||
|
|
||||||
|
Html(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn energy() -> axum::response::Result<Json<EnergyResponse>, ErrorResponse> {
|
||||||
|
let site_id = std::env::var("SITE_ID").unwrap();
|
||||||
|
let api_key = std::env::var("API_KEY").unwrap();
|
||||||
|
let utc_now = Utc::now().date_naive();
|
||||||
|
|
||||||
|
let url = format!(
|
||||||
|
"https://monitoringapi.solaredge.com/site/{}/energy?timeUnit=QUARTER_OF_AN_HOUR&endDate={}&startDate={}&api_key={}",
|
||||||
|
site_id, utc_now, utc_now, api_key,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut energy_response = reqwest::get(url)
|
||||||
|
.await
|
||||||
|
.map_err(|e| ErrorResponse::from(e.to_string()))?
|
||||||
|
.json::<EnergyResponse>()
|
||||||
|
.await
|
||||||
|
.map_err(|e| ErrorResponse::from(e.to_string()))?;
|
||||||
|
|
||||||
|
let values: Vec<EnergyValue> = energy_response
|
||||||
|
.energy
|
||||||
|
.values
|
||||||
|
.iter()
|
||||||
|
.map(|v| EnergyValue {
|
||||||
|
date: format!("{}+02:00", v.date.replace(' ', "T")).to_string(),
|
||||||
|
value: v.value,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
energy_response.energy.values = values;
|
||||||
|
Ok(Json(energy_response))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct EnergyResponse {
|
||||||
|
energy: Energy,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct Energy {
|
||||||
|
timeUnit: String,
|
||||||
|
unit: String,
|
||||||
|
values: Vec<EnergyValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct EnergyValue {
|
||||||
|
date: String,
|
||||||
|
value: Option<f32>,
|
||||||
|
}
|
||||||
11
src/overview.json
Normal file
11
src/overview.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"overview": {
|
||||||
|
"lastUpdateTime": "2025-10-07 11:00:14",
|
||||||
|
"lifeTimeData": { "energy": 1.511646e7, "revenue": 0.0 },
|
||||||
|
"lastYearData": { "energy": 876057.0 },
|
||||||
|
"lastMonthData": { "energy": 38313.0 },
|
||||||
|
"lastDayData": { "energy": 544.0 },
|
||||||
|
"currentPower": { "power": 431.809 },
|
||||||
|
"measuredBy": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/sites.json
Normal file
40
src/sites.json
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"sites": {
|
||||||
|
"count": 1,
|
||||||
|
"site": [
|
||||||
|
{
|
||||||
|
"id": 2193535,
|
||||||
|
"name": "S. Hautvast",
|
||||||
|
"accountId": 30166,
|
||||||
|
"status": "Active",
|
||||||
|
"peakPower": 3.51,
|
||||||
|
"lastUpdateTime": "2025-10-07",
|
||||||
|
"installationDate": "2021-04-13",
|
||||||
|
"ptoDate": null,
|
||||||
|
"notes": "",
|
||||||
|
"type": "Optimizers & Inverters",
|
||||||
|
"location": {
|
||||||
|
"country": "Netherlands",
|
||||||
|
"city": "Amsterdam",
|
||||||
|
"address": "A. Moenstraat, 22",
|
||||||
|
"address2": "",
|
||||||
|
"zip": "1022 KJ",
|
||||||
|
"timeZone": "Europe/Amsterdam",
|
||||||
|
"countryCode": "NL"
|
||||||
|
},
|
||||||
|
"primaryModule": {
|
||||||
|
"manufacturerName": "Hyundai Heavy Industries",
|
||||||
|
"modelName": "HiE-S390VG",
|
||||||
|
"maximumPower": 390.0,
|
||||||
|
"temperatureCoef": -0.34
|
||||||
|
},
|
||||||
|
"uris": {
|
||||||
|
"DETAILS": "/site/2193535/details",
|
||||||
|
"DATA_PERIOD": "/site/2193535/dataPeriod",
|
||||||
|
"OVERVIEW": "/site/2193535/overview"
|
||||||
|
},
|
||||||
|
"publicSettings": { "isPublic": false }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
184
static/chart.js
Normal file
184
static/chart.js
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
// Declare the chart dimensions and margins.
|
||||||
|
const width = 928;
|
||||||
|
const height = 500;
|
||||||
|
const marginTop = 20;
|
||||||
|
const marginRight = 30;
|
||||||
|
const marginBottom = 30;
|
||||||
|
const marginLeft = 40;
|
||||||
|
|
||||||
|
async function chart() {
|
||||||
|
const response = await fetch("/api/energy");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Response status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const energyResponse = await response.json();
|
||||||
|
const data = energyResponse.energy.values;
|
||||||
|
|
||||||
|
// Declare the x (horizontal position) scale.
|
||||||
|
const x = d3.scaleTime(
|
||||||
|
d3.extent(data, (d) => new Date(d.date)),
|
||||||
|
[marginLeft, width - marginRight],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Declare the y (vertical position) scale.
|
||||||
|
const y = d3.scaleLinear(
|
||||||
|
[0, d3.max(data, (d) => d.value)],
|
||||||
|
[height - marginBottom, marginTop],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Declare the line generator.
|
||||||
|
const line = d3
|
||||||
|
.line()
|
||||||
|
.x((d) => x(new Date(d.date)))
|
||||||
|
.y((d) => {
|
||||||
|
let v = d.value;
|
||||||
|
if (v == null) {
|
||||||
|
// no measurement from provider
|
||||||
|
v = 0;
|
||||||
|
}
|
||||||
|
return y(v);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the SVG container.
|
||||||
|
const svg = d3
|
||||||
|
.select("body")
|
||||||
|
.append("svg")
|
||||||
|
.attr("width", width)
|
||||||
|
.attr("height", height)
|
||||||
|
.attr("viewBox", [0, 0, width, height])
|
||||||
|
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
|
||||||
|
|
||||||
|
// Add the x-axis.
|
||||||
|
svg
|
||||||
|
.append("g")
|
||||||
|
.attr("transform", `translate(0,${height - marginBottom})`)
|
||||||
|
.call(
|
||||||
|
d3
|
||||||
|
.axisBottom(x)
|
||||||
|
.ticks(width / 80)
|
||||||
|
.tickSizeOuter(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add the y-axis, remove the domain line, add grid lines and a label.
|
||||||
|
svg
|
||||||
|
.append("g")
|
||||||
|
.attr("transform", `translate(${marginLeft},0)`)
|
||||||
|
.call(d3.axisLeft(y).ticks(height / 48))
|
||||||
|
.call((g) => g.select(".domain").remove())
|
||||||
|
.call((g) =>
|
||||||
|
g
|
||||||
|
.selectAll(".tick line")
|
||||||
|
.clone()
|
||||||
|
.attr("x2", width - marginLeft - marginRight)
|
||||||
|
.attr("stroke-opacity", 0.1),
|
||||||
|
)
|
||||||
|
.call((g) =>
|
||||||
|
g
|
||||||
|
.append("text")
|
||||||
|
.attr("x", -marginLeft)
|
||||||
|
.attr("y", 10)
|
||||||
|
.attr("fill", "currentColor")
|
||||||
|
.attr("text-anchor", "start")
|
||||||
|
.text(`energy (${energyResponse.energy.unit})`),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Append a path for the line.
|
||||||
|
svg
|
||||||
|
.append("path")
|
||||||
|
.attr("fill", "lightgreen")
|
||||||
|
.attr("stroke", "steelblue")
|
||||||
|
.attr("stroke-width", 2.0)
|
||||||
|
.attr("d", line(energyResponse.energy.values));
|
||||||
|
|
||||||
|
// ============ HOVER FUNCTIONALITY ============
|
||||||
|
|
||||||
|
// Create a group for hover elements
|
||||||
|
const hoverGroup = svg
|
||||||
|
.append("g")
|
||||||
|
.attr("class", "hover-group")
|
||||||
|
.style("display", "none");
|
||||||
|
|
||||||
|
// Add vertical line (crosshair)
|
||||||
|
const verticalLine = hoverGroup
|
||||||
|
.append("line")
|
||||||
|
.attr("stroke", "#999")
|
||||||
|
.attr("stroke-width", 1)
|
||||||
|
.attr("stroke-dasharray", "4,4")
|
||||||
|
.attr("y1", marginTop)
|
||||||
|
.attr("y2", height - marginBottom);
|
||||||
|
|
||||||
|
// Add circle at intersection point
|
||||||
|
const hoverCircle = hoverGroup
|
||||||
|
.append("circle")
|
||||||
|
.attr("r", 5)
|
||||||
|
.attr("fill", "steelblue")
|
||||||
|
.attr("stroke", "white")
|
||||||
|
.attr("stroke-width", 2);
|
||||||
|
|
||||||
|
// Create tooltip
|
||||||
|
const tooltip = d3.select("body").append("div").attr("class", "tooltip");
|
||||||
|
|
||||||
|
// Bisector for finding closest data point
|
||||||
|
const bisect = d3.bisector((d) => new Date(d.date)).left;
|
||||||
|
|
||||||
|
// Create invisible overlay for capturing mouse events
|
||||||
|
svg
|
||||||
|
.append("rect")
|
||||||
|
.attr("width", width - marginLeft - marginRight)
|
||||||
|
.attr("height", height - marginTop - marginBottom)
|
||||||
|
.attr("x", marginLeft)
|
||||||
|
.attr("y", marginTop)
|
||||||
|
.attr("fill", "none")
|
||||||
|
.attr("pointer-events", "all")
|
||||||
|
.on("mousemove", function (event) {
|
||||||
|
const [mouseX] = d3.pointer(event);
|
||||||
|
|
||||||
|
// Convert mouse x position to date
|
||||||
|
const xDate = x.invert(mouseX);
|
||||||
|
|
||||||
|
// Find closest data point
|
||||||
|
const index = bisect(data, xDate, 1);
|
||||||
|
const d0 = data[index - 1];
|
||||||
|
const d1 = data[index];
|
||||||
|
|
||||||
|
// Choose closer point
|
||||||
|
const d =
|
||||||
|
d1 && xDate - new Date(d0.date) > new Date(d1.date) - xDate ? d1 : d0;
|
||||||
|
|
||||||
|
if (d) {
|
||||||
|
const xPos = x(new Date(d.date));
|
||||||
|
const yPos = y(d.value || 0);
|
||||||
|
|
||||||
|
// Show hover elements
|
||||||
|
hoverGroup.style("display", null);
|
||||||
|
|
||||||
|
// Update vertical line position
|
||||||
|
verticalLine.attr("x1", xPos).attr("x2", xPos);
|
||||||
|
|
||||||
|
// Update circle position
|
||||||
|
hoverCircle.attr("cx", xPos).attr("cy", yPos);
|
||||||
|
|
||||||
|
// Format date
|
||||||
|
const formatDate = d3.timeFormat("%H:%M");
|
||||||
|
const formatValue = d3.format(",.2f");
|
||||||
|
|
||||||
|
// Update tooltip
|
||||||
|
tooltip
|
||||||
|
.style("display", "block")
|
||||||
|
.html(
|
||||||
|
`
|
||||||
|
<strong>${formatDate(new Date(d.date))}</strong><br/>
|
||||||
|
${formatValue(d.value || 0)} ${energyResponse.energy.unit}
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.style("left", event.pageX + 15 + "px")
|
||||||
|
.style("top", event.pageY - 28 + "px");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("mouseout", function () {
|
||||||
|
hoverGroup.style("display", "none");
|
||||||
|
tooltip.style("display", "none");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
chart();
|
||||||
2
static/d3.v7.min.js
vendored
Normal file
2
static/d3.v7.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
10
static/index.html
Normal file
10
static/index.html
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="/static/d3.v7.min.js"></script>
|
||||||
|
<script src="/static/chart.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
static/style.css
Normal file
14
static/style.css
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
|
||||||
|
Arial, sans-serif;
|
||||||
|
background: rgba(100, 0, 0, 0.8);
|
||||||
|
color: white;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
display: none;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue