mirror of
https://github.com/zaphar/Heracles.git
synced 2025-07-23 04:29:48 -04:00
feat: multiple subplots per graph
This commit is contained in:
parent
9a89412fc8
commit
4674a821d8
@ -2,29 +2,45 @@
|
|||||||
- title: Test Dasbboard 1
|
- title: Test Dasbboard 1
|
||||||
graphs:
|
graphs:
|
||||||
- title: Node cpu
|
- title: Node cpu
|
||||||
source: http://heimdall:9001
|
d3_tick_format: "~s"
|
||||||
query: 'sum by (instance)(irate(node_cpu_seconds_total{job="nodestats"}[5m]))'
|
plots:
|
||||||
|
- source: http://heimdall:9001
|
||||||
|
query: 'sum by (instance)(irate(node_cpu_seconds_total{job="nodestats"}[5m]))'
|
||||||
|
meta:
|
||||||
|
name_label: instance
|
||||||
query_type: Range
|
query_type: Range
|
||||||
span:
|
span:
|
||||||
end: now
|
end: now
|
||||||
duration: 1d
|
duration: 1d
|
||||||
step_duration: 10min
|
step_duration: 10min
|
||||||
name_label: instance
|
|
||||||
- title: Test Dasbboard 2
|
- title: Test Dasbboard 2
|
||||||
span:
|
span:
|
||||||
end: 2024-02-10T00:00:00.00Z
|
end: 2024-02-10T00:00:00.00Z
|
||||||
duration: 2 days
|
duration: 2 days
|
||||||
step_duration: 1 minute
|
step_duration: 1 minute
|
||||||
graphs:
|
graphs:
|
||||||
- title: Node cpu sytem percent
|
- title: Node cpu percent
|
||||||
source: http://heimdall:9001
|
|
||||||
query: |
|
|
||||||
sum by (instance)(irate(node_cpu_seconds_total{mode="system",job="nodestats"}[5m])) / sum by (instance)(irate(node_cpu_seconds_total{job="nodestats"}[5m]))
|
|
||||||
d3_tick_format: "~%"
|
d3_tick_format: "~%"
|
||||||
query_type: Range
|
query_type: Range
|
||||||
name_label: instance
|
plots:
|
||||||
|
- source: http://heimdall:9001
|
||||||
|
query: |
|
||||||
|
sum by (instance)(irate(node_cpu_seconds_total{mode="system",job="nodestats"}[5m])) / sum by (instance)(irate(node_cpu_seconds_total{job="nodestats"}[5m]))
|
||||||
|
meta:
|
||||||
|
d3_tick_format: "~s"
|
||||||
|
name_label: instance
|
||||||
|
name_prefix: "System"
|
||||||
|
- source: http://heimdall:9001
|
||||||
|
query: |
|
||||||
|
sum by (instance)(irate(node_cpu_seconds_total{mode="user",job="nodestats"}[5m])) / sum by (instance)(irate(node_cpu_seconds_total{job="nodestats"}[5m]))
|
||||||
|
meta:
|
||||||
|
#d3_tick_format: "~s"
|
||||||
|
name_label: instance
|
||||||
|
name_suffix: "User"
|
||||||
- title: Node memory
|
- title: Node memory
|
||||||
source: http://heimdall:9001
|
|
||||||
query: 'node_memory_MemFree_bytes{job="nodestats"}'
|
|
||||||
query_type: Scalar
|
query_type: Scalar
|
||||||
name_label: instance
|
plots:
|
||||||
|
- source: http://heimdall:9001
|
||||||
|
query: 'node_memory_MemFree_bytes{job="nodestats"}'
|
||||||
|
meta:
|
||||||
|
name_label: instance
|
||||||
|
@ -19,10 +19,11 @@ use serde::Deserialize;
|
|||||||
use serde_yaml;
|
use serde_yaml;
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
use crate::query::{QueryConn, QueryType};
|
use crate::query::{QueryConn, QueryType, PlotMeta};
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct GraphSpan {
|
pub struct GraphSpan {
|
||||||
|
// serialized with https://datatracker.ietf.org/doc/html/rfc3339 and special handling for 'now'
|
||||||
pub end: String,
|
pub end: String,
|
||||||
pub duration: String,
|
pub duration: String,
|
||||||
pub step_duration: String,
|
pub step_duration: String,
|
||||||
@ -32,17 +33,21 @@ pub struct GraphSpan {
|
|||||||
pub struct Dashboard {
|
pub struct Dashboard {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub graphs: Vec<Graph>,
|
pub graphs: Vec<Graph>,
|
||||||
pub span: Option<GraphSpan>
|
pub span: Option<GraphSpan>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SubPlot {
|
||||||
|
pub source: String,
|
||||||
|
pub query: String,
|
||||||
|
pub meta: PlotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Graph {
|
pub struct Graph {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub source: String,
|
pub plots: Vec<SubPlot>,
|
||||||
pub query: String,
|
|
||||||
// serialized with https://datatracker.ietf.org/doc/html/rfc3339
|
|
||||||
pub span: Option<GraphSpan>,
|
pub span: Option<GraphSpan>,
|
||||||
pub name_label: String,
|
|
||||||
pub query_type: QueryType,
|
pub query_type: QueryType,
|
||||||
pub d3_tick_format: Option<String>,
|
pub d3_tick_format: Option<String>,
|
||||||
}
|
}
|
||||||
@ -97,23 +102,31 @@ fn graph_span_to_tuple(span: &Option<GraphSpan>) -> Option<(DateTime<Utc>, Durat
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Graph {
|
impl Graph {
|
||||||
pub fn get_query_connection<'conn, 'graph: 'conn>(&'graph self, graph_span: &'graph Option<GraphSpan>, query_span: &'graph Option<GraphSpan>) -> QueryConn<'conn> {
|
pub fn get_query_connections<'conn, 'graph: 'conn>(
|
||||||
debug!(
|
&'graph self,
|
||||||
query = self.query,
|
graph_span: &'graph Option<GraphSpan>,
|
||||||
source = self.source,
|
query_span: &'graph Option<GraphSpan>,
|
||||||
"Getting query connection for graph"
|
) -> Vec<QueryConn<'conn>> {
|
||||||
);
|
let mut conns = Vec::new();
|
||||||
let mut conn = QueryConn::new(&self.source, &self.query, self.query_type.clone());
|
for plot in self.plots.iter() {
|
||||||
// Query params take precendence over all other settings. Then graph settings take
|
debug!(
|
||||||
// precedences and finally the dashboard settings take precendence
|
query = plot.query,
|
||||||
if let Some((end, duration, step_duration)) = graph_span_to_tuple(query_span) {
|
source = plot.source,
|
||||||
conn = conn.with_span(end, duration, step_duration);
|
"Getting query connection for graph"
|
||||||
} else if let Some((end, duration, step_duration)) = graph_span_to_tuple(&self.span) {
|
);
|
||||||
conn = conn.with_span(end, duration, step_duration);
|
let mut conn = QueryConn::new(&plot.source, &plot.query, self.query_type.clone(), plot.meta.clone());
|
||||||
} else if let Some((end, duration, step_duration)) = graph_span_to_tuple(graph_span) {
|
// Query params take precendence over all other settings. Then graph settings take
|
||||||
conn = conn.with_span(end, duration, step_duration);
|
// precedences and finally the dashboard settings take precendence
|
||||||
|
if let Some((end, duration, step_duration)) = graph_span_to_tuple(query_span) {
|
||||||
|
conn = conn.with_span(end, duration, step_duration);
|
||||||
|
} else if let Some((end, duration, step_duration)) = graph_span_to_tuple(&self.span) {
|
||||||
|
conn = conn.with_span(end, duration, step_duration);
|
||||||
|
} else if let Some((end, duration, step_duration)) = graph_span_to_tuple(graph_span) {
|
||||||
|
conn = conn.with_span(end, duration, step_duration);
|
||||||
|
}
|
||||||
|
conns.push(conn);
|
||||||
}
|
}
|
||||||
conn
|
conns
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
70
src/query.rs
70
src/query.rs
@ -38,20 +38,31 @@ pub struct QueryConn<'conn> {
|
|||||||
query: &'conn str,
|
query: &'conn str,
|
||||||
span: Option<TimeSpan>,
|
span: Option<TimeSpan>,
|
||||||
query_type: QueryType,
|
query_type: QueryType,
|
||||||
|
pub meta: PlotMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'conn> QueryConn<'conn> {
|
impl<'conn> QueryConn<'conn> {
|
||||||
pub fn new<'a: 'conn>(source: &'a str, query: &'a str, query_type: QueryType) -> Self {
|
pub fn new<'a: 'conn>(source: &'a str, query: &'a str, query_type: QueryType, meta: PlotMeta) -> Self {
|
||||||
Self {
|
Self {
|
||||||
source,
|
source,
|
||||||
query,
|
query,
|
||||||
query_type,
|
query_type,
|
||||||
|
meta,
|
||||||
span: None,
|
span: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_span(mut self, end: DateTime<Utc>, duration: chrono::Duration, step: chrono::Duration) -> Self {
|
pub fn with_span(
|
||||||
self.span = Some(TimeSpan { end, duration, step_seconds: step.num_seconds() , });
|
mut self,
|
||||||
|
end: DateTime<Utc>,
|
||||||
|
duration: chrono::Duration,
|
||||||
|
step: chrono::Duration,
|
||||||
|
) -> Self {
|
||||||
|
self.span = Some(TimeSpan {
|
||||||
|
end,
|
||||||
|
duration,
|
||||||
|
step_seconds: step.num_seconds(),
|
||||||
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,25 +75,35 @@ impl<'conn> QueryConn<'conn> {
|
|||||||
step_seconds,
|
step_seconds,
|
||||||
}) = self.span
|
}) = self.span
|
||||||
{
|
{
|
||||||
let start = end - du;
|
let start = end - du;
|
||||||
debug!(?start, ?end, step_seconds, "Running Query with range values");
|
debug!(
|
||||||
|
?start,
|
||||||
|
?end,
|
||||||
|
step_seconds,
|
||||||
|
"Running Query with range values"
|
||||||
|
);
|
||||||
(start.timestamp(), end.timestamp(), step_seconds as f64)
|
(start.timestamp(), end.timestamp(), step_seconds as f64)
|
||||||
} else {
|
} else {
|
||||||
let end = Utc::now();
|
let end = Utc::now();
|
||||||
let start = end - chrono::Duration::minutes(10);
|
let start = end - chrono::Duration::minutes(10);
|
||||||
debug!(?start, ?end, step_seconds=30, "Running Query with range values");
|
debug!(
|
||||||
|
?start,
|
||||||
|
?end,
|
||||||
|
step_seconds = 30,
|
||||||
|
"Running Query with range values"
|
||||||
|
);
|
||||||
(start.timestamp(), end.timestamp(), 30 as f64)
|
(start.timestamp(), end.timestamp(), 30 as f64)
|
||||||
};
|
};
|
||||||
//debug!(start, end, step_resolution, "Running Query with range values");
|
//debug!(start, end, step_resolution, "Running Query with range values");
|
||||||
match self.query_type {
|
match self.query_type {
|
||||||
QueryType::Range => {
|
QueryType::Range => {
|
||||||
let results = client
|
let results = client
|
||||||
.query_range(self.query, start, end, step_resolution)
|
.query_range(self.query, start, end, step_resolution)
|
||||||
.get()
|
.get()
|
||||||
.await?;
|
.await?;
|
||||||
//debug!(?results, "range results");
|
//debug!(?results, "range results");
|
||||||
Ok(results)
|
Ok(results)
|
||||||
},
|
}
|
||||||
QueryType::Scalar => Ok(client.query(self.query).get().await?),
|
QueryType::Scalar => Ok(client.query(self.query).get().await?),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,19 +115,33 @@ pub struct DataPoint {
|
|||||||
value: f64,
|
value: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct PlotMeta {
|
||||||
|
name_prefix: Option<String>,
|
||||||
|
name_suffix: Option<String>,
|
||||||
|
name_label: Option<String>,
|
||||||
|
d3_tick_format: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub enum QueryResult {
|
pub enum QueryResult {
|
||||||
Series(Vec<(HashMap<String, String>, Vec<DataPoint>)>),
|
Series(Vec<(HashMap<String, String>, PlotMeta, Vec<DataPoint>)>),
|
||||||
Scalar(Vec<(HashMap<String, String>, DataPoint)>),
|
Scalar(Vec<(HashMap<String, String>, PlotMeta, DataPoint)>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for QueryResult {
|
impl std::fmt::Debug for QueryResult {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
QueryResult::Series(v) => {
|
QueryResult::Series(v) => {
|
||||||
f.write_fmt(format_args!("Series trace count = {}", v.len()))?;
|
f.write_fmt(format_args!("Series trace count = {}", v.len()))?;
|
||||||
for (idx, (tags, trace)) in v.iter().enumerate() {
|
for (idx, (tags, meta, trace)) in v.iter().enumerate() {
|
||||||
f.write_fmt(format_args!("; {}: meta {:?} datapoint count = {};", idx, tags, trace.len()))?;
|
f.write_fmt(format_args!(
|
||||||
|
"; {}: tags {:?} meta: {:?} datapoint count = {};",
|
||||||
|
idx,
|
||||||
|
tags,
|
||||||
|
meta,
|
||||||
|
trace.len()
|
||||||
|
))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QueryResult::Scalar(v) => {
|
QueryResult::Scalar(v) => {
|
||||||
@ -117,7 +152,7 @@ impl std::fmt::Debug for QueryResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_samples(data: Data) -> QueryResult {
|
pub fn to_samples(data: Data, meta: PlotMeta) -> QueryResult {
|
||||||
match data {
|
match data {
|
||||||
Data::Matrix(mut range) => QueryResult::Series(
|
Data::Matrix(mut range) => QueryResult::Series(
|
||||||
range
|
range
|
||||||
@ -126,6 +161,7 @@ pub fn to_samples(data: Data) -> QueryResult {
|
|||||||
let (metric, mut samples) = rv.into_inner();
|
let (metric, mut samples) = rv.into_inner();
|
||||||
(
|
(
|
||||||
metric,
|
metric,
|
||||||
|
meta.clone(),
|
||||||
samples
|
samples
|
||||||
.drain(0..)
|
.drain(0..)
|
||||||
.map(|s| DataPoint {
|
.map(|s| DataPoint {
|
||||||
@ -144,6 +180,7 @@ pub fn to_samples(data: Data) -> QueryResult {
|
|||||||
let (metric, sample) = iv.into_inner();
|
let (metric, sample) = iv.into_inner();
|
||||||
(
|
(
|
||||||
metric,
|
metric,
|
||||||
|
meta.clone(),
|
||||||
DataPoint {
|
DataPoint {
|
||||||
timestamp: sample.timestamp(),
|
timestamp: sample.timestamp(),
|
||||||
value: sample.value(),
|
value: sample.value(),
|
||||||
@ -154,6 +191,7 @@ pub fn to_samples(data: Data) -> QueryResult {
|
|||||||
),
|
),
|
||||||
Data::Scalar(sample) => QueryResult::Scalar(vec![(
|
Data::Scalar(sample) => QueryResult::Scalar(vec![(
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
|
meta.clone(),
|
||||||
DataPoint {
|
DataPoint {
|
||||||
timestamp: sample.timestamp(),
|
timestamp: sample.timestamp(),
|
||||||
value: sample.value(),
|
value: sample.value(),
|
||||||
|
@ -33,7 +33,7 @@ pub async fn graph_query(
|
|||||||
State(config): Config,
|
State(config): Config,
|
||||||
Path((dash_idx, graph_idx)): Path<(usize, usize)>,
|
Path((dash_idx, graph_idx)): Path<(usize, usize)>,
|
||||||
Query(query): Query<HashMap<String, String>>,
|
Query(query): Query<HashMap<String, String>>,
|
||||||
) -> Json<QueryResult> {
|
) -> Json<Vec<QueryResult>> {
|
||||||
debug!("Getting data for query");
|
debug!("Getting data for query");
|
||||||
let dash = config.get(dash_idx).expect("No such dashboard index");
|
let dash = config.get(dash_idx).expect("No such dashboard index");
|
||||||
let graph = dash
|
let graph = dash
|
||||||
@ -45,7 +45,6 @@ pub async fn graph_query(
|
|||||||
&& query.contains_key("duration")
|
&& query.contains_key("duration")
|
||||||
&& query.contains_key("step_duration")
|
&& query.contains_key("step_duration")
|
||||||
{
|
{
|
||||||
// TODO(jwall): handle the now case.
|
|
||||||
Some(GraphSpan {
|
Some(GraphSpan {
|
||||||
end: query["end"].clone(),
|
end: query["end"].clone(),
|
||||||
duration: query["duration"].clone(),
|
duration: query["duration"].clone(),
|
||||||
@ -55,21 +54,23 @@ pub async fn graph_query(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let data = to_samples(
|
let connections = graph.get_query_connections(&dash.span, &query_span);
|
||||||
graph
|
let mut data = Vec::new();
|
||||||
.get_query_connection(&dash.span, &query_span)
|
for conn in connections {
|
||||||
.get_results()
|
data.push(to_samples(
|
||||||
.await
|
conn.get_results()
|
||||||
.expect("Unable to get query results")
|
.await
|
||||||
.data()
|
.expect("Unable to get query results")
|
||||||
.clone(),
|
.data()
|
||||||
);
|
.clone(),
|
||||||
|
conn.meta,
|
||||||
|
));
|
||||||
|
}
|
||||||
Json(data)
|
Json(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mk_api_routes(config: Arc<Vec<Dashboard>>) -> Router<Config> {
|
pub fn mk_api_routes(config: Arc<Vec<Dashboard>>) -> Router<Config> {
|
||||||
// Query routes
|
// Query routes
|
||||||
// TODO(zaphar): Allow passing the timespan in via query
|
|
||||||
Router::new().route(
|
Router::new().route(
|
||||||
"/dash/:dash_idx/graph/:graph_idx",
|
"/dash/:dash_idx/graph/:graph_idx",
|
||||||
get(graph_query).with_state(config),
|
get(graph_query).with_state(config),
|
||||||
@ -83,9 +84,9 @@ pub fn graph_component(dash_idx: usize, graph_idx: usize, graph: &Graph) -> Mark
|
|||||||
div {
|
div {
|
||||||
h2 { (graph.title) }
|
h2 { (graph.title) }
|
||||||
@if graph.d3_tick_format.is_some() {
|
@if graph.d3_tick_format.is_some() {
|
||||||
timeseries-graph uri=(graph_data_uri) id=(graph_id) label=(graph.name_label) d3-tick-format=(graph.d3_tick_format.as_ref().unwrap()) { }
|
timeseries-graph uri=(graph_data_uri) id=(graph_id) d3-tick-format=(graph.d3_tick_format.as_ref().unwrap()) { }
|
||||||
} @else {
|
} @else {
|
||||||
timeseries-graph uri=(graph_data_uri) id=(graph_id) label=(graph.name_label) { }
|
timeseries-graph uri=(graph_data_uri) id=(graph_id) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
138
static/lib.js
138
static/lib.js
@ -18,7 +18,6 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
#height;
|
#height;
|
||||||
#intervalId;
|
#intervalId;
|
||||||
#pollSeconds;
|
#pollSeconds;
|
||||||
#label;
|
|
||||||
#end;
|
#end;
|
||||||
#duration;
|
#duration;
|
||||||
#step_duration;
|
#step_duration;
|
||||||
@ -32,7 +31,7 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
this.#targetNode = this.appendChild(document.createElement("div"));
|
this.#targetNode = this.appendChild(document.createElement("div"));
|
||||||
}
|
}
|
||||||
|
|
||||||
static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration', 'd3-tick-format'];
|
static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration'];
|
||||||
|
|
||||||
attributeChangedCallback(name, _oldValue, newValue) {
|
attributeChangedCallback(name, _oldValue, newValue) {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
@ -48,9 +47,6 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
case 'poll-seconds':
|
case 'poll-seconds':
|
||||||
this.#pollSeconds = newValue;
|
this.#pollSeconds = newValue;
|
||||||
break;
|
break;
|
||||||
case 'label':
|
|
||||||
this.#label = newValue;
|
|
||||||
break;
|
|
||||||
case 'end':
|
case 'end':
|
||||||
this.#end = newValue;
|
this.#end = newValue;
|
||||||
break;
|
break;
|
||||||
@ -74,7 +70,6 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
this.#width = this.getAttribute('width') || this.#width;
|
this.#width = this.getAttribute('width') || this.#width;
|
||||||
this.#height = this.getAttribute('height') || this.#height;
|
this.#height = this.getAttribute('height') || this.#height;
|
||||||
this.#pollSeconds = this.getAttribute('poll-seconds') || this.#pollSeconds;
|
this.#pollSeconds = this.getAttribute('poll-seconds') || this.#pollSeconds;
|
||||||
this.#label = this.getAttribute('label') || null;
|
|
||||||
this.#end = this.getAttribute('end') || null;
|
this.#end = this.getAttribute('end') || null;
|
||||||
this.#duration = this.getAttribute('duration') || null;
|
this.#duration = this.getAttribute('duration') || null;
|
||||||
this.#step_duration = this.getAttribute('step-duration') || null;
|
this.#step_duration = this.getAttribute('step-duration') || null;
|
||||||
@ -134,62 +129,81 @@ class TimeseriesGraph extends HTMLElement {
|
|||||||
orientation: 'h'
|
orientation: 'h'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const layout = {
|
var layout = {
|
||||||
displayModeBar: false,
|
displayModeBar: false,
|
||||||
responsive: true,
|
responsive: true,
|
||||||
yaxis: {
|
|
||||||
tickformat: this.#d3TickFormat,
|
|
||||||
//showticksuffix: 'all',
|
|
||||||
//ticksuffix: '%',
|
|
||||||
//exponentFormat: 'SI'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
console.debug("layout", layout);
|
var traces = [];
|
||||||
if (data.Series) {
|
for (var subplot_idx in data) {
|
||||||
// https://plotly.com/javascript/reference/scatter/
|
const subplot = data[subplot_idx];
|
||||||
var traces = [];
|
const subplotCount = Number(subplot_idx) + 1;
|
||||||
for (const pair of data.Series) {
|
const yaxis = "y" + subplotCount
|
||||||
const series = pair[1];
|
if (subplot.Series) {
|
||||||
const labels = pair[0];
|
// https://plotly.com/javascript/reference/scatter/
|
||||||
var trace = {
|
for (const triple of subplot.Series) {
|
||||||
type: "scatter",
|
const labels = triple[0];
|
||||||
mode: "lines+text",
|
const meta = triple[1];
|
||||||
x: [],
|
layout["yaxis" + subplotCount] = {
|
||||||
y: []
|
anchor: yaxis,
|
||||||
};
|
tickformat: meta["d3_tick_format"] || this.#d3TickFormat
|
||||||
if (labels[this.#label]) {
|
};
|
||||||
trace.name = labels[this.#label];
|
const series = triple[2];
|
||||||
};
|
var trace = {
|
||||||
for (const point of series) {
|
type: "scatter",
|
||||||
trace.x.push(new Date(point.timestamp * 1000));
|
mode: "lines+text",
|
||||||
trace.y.push(point.value);
|
x: [],
|
||||||
|
y: [],
|
||||||
|
yaxis: yaxis,
|
||||||
|
yhoverformat: meta["d3_tick_format"],
|
||||||
|
};
|
||||||
|
const namePrefix = meta["name_prefix"];
|
||||||
|
const nameSuffix = meta["name_suffix"];
|
||||||
|
const nameLabel = meta["name_label"];
|
||||||
|
var name = "";
|
||||||
|
if (namePrefix) {
|
||||||
|
name = namePrefix + "-";
|
||||||
|
};
|
||||||
|
if (nameLabel && labels[nameLabel]) {
|
||||||
|
name = name + labels[nameLabel];
|
||||||
|
};
|
||||||
|
if (nameSuffix) {
|
||||||
|
name = name + " - " + nameSuffix;
|
||||||
|
};
|
||||||
|
if (name) { trace.name = name; }
|
||||||
|
for (const point of series) {
|
||||||
|
trace.x.push(new Date(point.timestamp * 1000));
|
||||||
|
trace.y.push(point.value);
|
||||||
|
}
|
||||||
|
traces.push(trace);
|
||||||
|
}
|
||||||
|
} else if (subplot.Scalar) {
|
||||||
|
// https://plotly.com/javascript/reference/bar/
|
||||||
|
for (const triple of subplot.Scalar) {
|
||||||
|
const labels = triple[0];
|
||||||
|
const meta = triple[1];
|
||||||
|
const series = triple[2];
|
||||||
|
var trace = {
|
||||||
|
type: "bar",
|
||||||
|
x: [],
|
||||||
|
y: [],
|
||||||
|
yaxis: yaxis,
|
||||||
|
yhoverformat: meta["d3_tick_format"],
|
||||||
|
};
|
||||||
|
let nameLabel = meta["name_label"];
|
||||||
|
if (nameLabel && labels[nameLabel]) {
|
||||||
|
trace.name = labels[nameLabel];
|
||||||
|
};
|
||||||
|
if (nameLabel && labels[nameLabel]) {
|
||||||
|
trace.x.push(labels[nameLabel]);
|
||||||
|
};
|
||||||
|
trace.y.push(series.value);
|
||||||
|
traces.push(trace);
|
||||||
}
|
}
|
||||||
traces.push(trace);
|
|
||||||
}
|
}
|
||||||
// https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
|
|
||||||
Plotly.react(this.getTargetNode(), traces, layout, config);
|
|
||||||
} else if (data.Scalar) {
|
|
||||||
// https://plotly.com/javascript/reference/bar/
|
|
||||||
var traces = [];
|
|
||||||
for (const pair of data.Scalar) {
|
|
||||||
const series = pair[1];
|
|
||||||
const labels = pair[0];
|
|
||||||
var trace = {
|
|
||||||
type: "bar",
|
|
||||||
x: [],
|
|
||||||
y: []
|
|
||||||
};
|
|
||||||
if (labels[this.#label]) {
|
|
||||||
trace.name = labels[this.#label];
|
|
||||||
};
|
|
||||||
if (labels[this.#label]) {
|
|
||||||
trace.x.push(labels[this.#label]);
|
|
||||||
};
|
|
||||||
trace.y.push(series.value);
|
|
||||||
traces.push(trace);
|
|
||||||
}
|
|
||||||
Plotly.react(this.getTargetNode(), traces, layout, config);
|
|
||||||
}
|
}
|
||||||
|
console.debug("traces: ", traces);
|
||||||
|
// https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
|
||||||
|
Plotly.react(this.getTargetNode(), traces, layout, config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,20 +215,20 @@ class SpanSelector extends HTMLElement {
|
|||||||
#durationInput = null;
|
#durationInput = null;
|
||||||
#stepDurationInput = null;
|
#stepDurationInput = null;
|
||||||
#updateInput = null
|
#updateInput = null
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.#targetNode = this.appendChild(document.createElement('div'));
|
this.#targetNode = this.appendChild(document.createElement('div'));
|
||||||
|
|
||||||
this.#targetNode.appendChild(document.createElement('span')).innerText = "end: ";
|
this.#targetNode.appendChild(document.createElement('span')).innerText = "end: ";
|
||||||
this.#endInput = this.#targetNode.appendChild(document.createElement('input'));
|
this.#endInput = this.#targetNode.appendChild(document.createElement('input'));
|
||||||
|
|
||||||
this.#targetNode.appendChild(document.createElement('span')).innerText = "duration: ";
|
this.#targetNode.appendChild(document.createElement('span')).innerText = "duration: ";
|
||||||
this.#durationInput = this.#targetNode.appendChild(document.createElement('input'));
|
this.#durationInput = this.#targetNode.appendChild(document.createElement('input'));
|
||||||
|
|
||||||
this.#targetNode.appendChild(document.createElement('span')).innerText = "step duration: ";
|
this.#targetNode.appendChild(document.createElement('span')).innerText = "step duration: ";
|
||||||
this.#stepDurationInput = this.#targetNode.appendChild(document.createElement('input'));
|
this.#stepDurationInput = this.#targetNode.appendChild(document.createElement('input'));
|
||||||
|
|
||||||
this.#updateInput = this.#targetNode.appendChild(document.createElement('button'));
|
this.#updateInput = this.#targetNode.appendChild(document.createElement('button'));
|
||||||
this.#updateInput.innerText = "Update";
|
this.#updateInput.innerText = "Update";
|
||||||
}
|
}
|
||||||
@ -225,7 +239,7 @@ class SpanSelector extends HTMLElement {
|
|||||||
self.updateGraphs()
|
self.updateGraphs()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this.#updateInput.onclick = undefined;
|
this.#updateInput.onclick = undefined;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user