refactor: Split the log and graph elements

This commit is contained in:
Jeremy Wall 2024-05-26 19:34:13 -04:00
parent ae669767c8
commit 9c904a3c62
2 changed files with 383 additions and 283 deletions

View File

@ -154,7 +154,7 @@ pub fn log_component(dash_idx: usize, log_idx: usize, log: &LogStream) -> Markup
html! { html! {
div { div {
h2 { (log.title) " - " a href=(log_embed_uri) { "embed url" } } h2 { (log.title) " - " a href=(log_embed_uri) { "embed url" } }
graph-plot uri=(log_data_uri) id=(log_id) { } log-plot uri=(log_data_uri) id=(log_id) { }
} }
} }
} }

View File

@ -12,6 +12,19 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
function yaxisNameGenerator() {
var counter = 1;
return function() {
var name = "yaxis";
if (counter != 1) {
name = "yaxis" + counter;
}
counter++;
return name;
};
}
/** /**
* Map ansi terminal codes to html color codes. * Map ansi terminal codes to html color codes.
* @param {string} line * @param {string} line
@ -33,7 +46,7 @@ function ansiToHtml(line) {
// NOTE(zaphar): Yes this is gross and I should really do a better parser but I'm lazy. // NOTE(zaphar): Yes this is gross and I should really do a better parser but I'm lazy.
// Replace ANSI codes with HTML span elements styled with the corresponding color // Replace ANSI codes with HTML span elements styled with the corresponding color
return line.replace(/\x1b\[([0-9;]*)m/g, (match, p1) => { return line.replace(/\x1b\[([0-9;]*)m/g, (_match, p1) => {
const parts = p1.split(';'); // ANSI codes can be compounded, e.g., "1;31" for bold red const parts = p1.split(';'); // ANSI codes can be compounded, e.g., "1;31" for bold red
let styles = ''; let styles = '';
for (let part of parts) { for (let part of parts) {
@ -47,6 +60,27 @@ function ansiToHtml(line) {
}) + '</span>'; }) + '</span>';
} }
/**
* Formats the name for the plot trace.
* @param {PlotConfig} config
* @param {Map<string, string>} labels
* @return string
*/
function formatName(config, labels) {
var name = "";
const formatter = config.name_format
if (formatter) {
name = eval(formatter);
} else {
var names = [];
for (const value of labels) {
names.push(value);
}
name = names.join(" ");
}
return name;
}
/** /**
* Get's a css variable's value from the document. * Get's a css variable's value from the document.
* @param {string} variableName - Name of the variable to get `--var-name` * @param {string} variableName - Name of the variable to get `--var-name`
@ -56,159 +90,77 @@ function getCssVariableValue(variableName) {
return getComputedStyle(document.documentElement).getPropertyValue(variableName); return getComputedStyle(document.documentElement).getPropertyValue(variableName);
} }
/** class ElementConfig {
* Custom element for showing a plotly graph. uri;
*
* @extends HTMLElement
*/
export class GraphPlot extends HTMLElement {
/** @type {?string} */
#uri;
/** @type {?boolean} */ /** @type {?boolean} */
#allowUriFilters; allowUriFilters;
/** @type {?number} */ /** @type {?number} */
#width; width;
/** @type {?number} */ /** @type {?number} */
#height; height;
/** @type {?number} */ /** @type {?number} */
#intervalId; intervalId;
/** @type {?number} */ /** @type {?number} */
#pollSeconds; pollSeconds;
/** @type {?string} */ /** @type {?string} */
#end; end;
/** @type {?number} */ /** @type {?number} */
#duration; duration;
/** @type {?string} */ /** @type {?string} */
#step_duration; step_duration;
/** @type {?string} */ /** @type {?string} */
#d3TickFormat = "~s"; d3TickFormat = "~s";
/** @type {?HTMLDivElement} */ /** @type {?HTMLDivElement} */
#targetNode = null; targetNode = null;
/** @type {?HTMLElement} */ /** @type {?HTMLElement} */
#menuContainer = null; menuContainer = null;
/** @type {Object<string, HTMLSelectElement>} */ /** @type {Object<string, HTMLSelectElement>} */
#filterSelectElements = {}; filterSelectElements = {};
/** @type {Object<string, Array<string>>} */ /** @type {Object<string, Array<string>>} */
#filterLabels = {}; filterLabels = {};
/** @type {Object<string, Array<string>>} */ /** @type {Object<string, Array<string>>} */
#filteredLabelSets = {}; filteredLabelSets = {};
/** @type {?HTMLElement} */
#container = null;
constructor() { constructor(/** @type {?HTMLElement} */ container) {
super(); this.#container = container;
this.#width = 800; this.width = 800;
this.#height = 600; this.height = 600;
this.#pollSeconds = 30; this.pollSeconds = 30;
this.#menuContainer = this.appendChild(document.createElement('div')); this.menuContainer = this.#container.appendChild(document.createElement('div'));
// TODO(jwall): These should probably be done as template clones so we have less places // TODO(jwall): These should probably be done as template clones so we have less places
// to look for class attributes. // to look for class attributes.
this.#menuContainer.setAttribute("class", "row-flex"); this.menuContainer.setAttribute("class", "row-flex");
this.#targetNode = this.appendChild(document.createElement("div")); this.targetNode = this.#container.appendChild(document.createElement("div"));
} }
static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration', 'd3-tick-format', 'allow-uri-filter']; connectedHandler(/** @type {HtmlElement} */ element) {
this.uri = element.getAttribute('uri') || this.uri;
/** this.width = Number(element.getAttribute('width') || this.width);
* Callback for attributes changes. this.height = Number(element.getAttribute('height') || this.height);
* this.pollSeconds = Number(element.getAttribute('poll-seconds') || this.pollSeconds);
* @param {string} name - The name of the attribute. this.end = element.getAttribute('end') || null;
* @param {?string} _oldValue - The old value for the attribute this.duration = Number(element.getAttribute('duration')) || null;
* @param {?string} newValue - The new value for the attribute this.step_duration = element.getAttribute('step-duration') || null;
*/ this.d3TickFormat = element.getAttribute('d3-tick-format') || this.d3TickFormat;
attributeChangedCallback(name, _oldValue, newValue) { this.allowUriFilters = Boolean(element.getAttribute('allow-uri-filters'));
switch (name) {
case 'uri':
this.#uri = newValue;
break;
case 'width':
this.#width = Number(newValue);
break;
case 'height':
this.#height = Number(newValue);
break;
case 'poll-seconds':
this.#pollSeconds = Number(newValue);
break;
case 'end':
this.#end = newValue;
break;
case 'duration':
this.#duration = Number(newValue);
break;
case 'step-duration':
this.#step_duration = newValue;
break;
case 'd3-tick-format':
this.#d3TickFormat = newValue;
break;
case 'allow-uri-filters':
this.#allowUriFilters = Boolean(newValue);
break;
default: // do nothing;
break;
}
this.reset();
} }
connectedCallback() { stopInterval() {
this.#uri = this.getAttribute('uri') || this.#uri; if (this.intervalId) {
this.#width = Number(this.getAttribute('width') || this.#width); clearInterval(this.intervalId);
this.#height = Number(this.getAttribute('height') || this.#height); this.intervalId = null;
this.#pollSeconds = Number(this.getAttribute('poll-seconds') || this.#pollSeconds);
this.#end = this.getAttribute('end') || null;
this.#duration = Number(this.getAttribute('duration')) || null;
this.#step_duration = this.getAttribute('step-duration') || null;
this.#d3TickFormat = this.getAttribute('d3-tick-format') || this.#d3TickFormat;
this.#allowUriFilters = Boolean(this.getAttribute('allow-uri-filters'));
this.reset(true);
} }
disconnectedCallback() {
this.stopInterval()
} }
static elementName = "graph-plot";
/* /*
* Get's the target node for placing the plotly graph. * Get's the target node for placing the plotly graph.
* *
* @returns {?HTMLDivElement} * @returns {?HTMLDivElement}
*/ */
getTargetNode() { getTargetNode() {
return this.#targetNode; return this.targetNode;
}
/**
*/
stopInterval() {
if (this.#intervalId) {
clearInterval(this.#intervalId);
this.#intervalId = null;
}
}
/**
* Resets the entire graph and then restarts polling.
* @param {boolean=} updateOnly
*/
reset(updateOnly) {
var self = this;
self.stopInterval()
self.fetchData().then((data) => {
if (!updateOnly) {
self.getLabelsForData(data.Metrics || data.Logs.lines);
self.buildFilterMenu();
}
self.updateGraph(data).then(() => {
self.#intervalId = setInterval(() => self.updateGraph(), 1000 * self.#pollSeconds);
});
});
}
/** Registers the custom element if it doesn't already exist */
static registerElement() {
if (!customElements.get(GraphPlot.elementName)) {
customElements.define(GraphPlot.elementName, GraphPlot);
}
} }
/** /**
@ -219,21 +171,21 @@ export class GraphPlot extends HTMLElement {
getUri() { getUri() {
//var uriParts = [this.#uri]; //var uriParts = [this.#uri];
var uriParts = []; var uriParts = [];
if (this.#end && this.#duration && this.#step_duration) { if (this.end && this.duration && this.step_duration) {
uriParts.push("end=" + this.#end); uriParts.push("end=" + this.end);
uriParts.push("duration=" + this.#duration); uriParts.push("duration=" + this.duration);
uriParts.push("step_duration=" + this.#step_duration); uriParts.push("step_duration=" + this.step_duration);
} }
if (this.#allowUriFilters) { if (this.allowUriFilters) {
for (const filterName in this.#filteredLabelSets) { for (const filterName in this.filteredLabelSets) {
const filterVals = this.#filteredLabelSets[filterName].join("|"); const filterVals = this.filteredLabelSets[filterName].join("|");
uriParts.push(`filter-${filterName}=${filterVals}`) uriParts.push(`filter-${filterName}=${filterVals}`)
} }
} }
if (uriParts) { if (uriParts) {
return this.#uri + "?" + uriParts.join('&'); return this.uri + "?" + uriParts.join('&');
} else { } else {
return this.#uri; return this.uri;
} }
} }
@ -250,29 +202,8 @@ export class GraphPlot extends HTMLElement {
return data; return data;
} }
/**
* Formats the name for the plot trace.
* @param {PlotConfig} config
* @param {Map<string, string>} labels
* @return string
*/
formatName(config, labels) {
var name = "";
const formatter = config.name_format
if (formatter) {
name = eval(formatter);
} else {
var names = [];
for (const value of labels) {
names.push(value);
}
name = names.join(" ");
}
return name;
}
getFilterLabels() { getFilterLabels() {
return this.#filterLabels; return this.filterLabels;
} }
/** /**
@ -280,13 +211,13 @@ export class GraphPlot extends HTMLElement {
*/ */
populateFilterData(labels) { populateFilterData(labels) {
for (var key in labels) { for (var key in labels) {
const label = this.#filterLabels[key]; const label = this.filterLabels[key];
if (label) { if (label) {
if (!label.includes(labels[key])) { if (!label.includes(labels[key])) {
this.#filterLabels[key].push(labels[key]); this.filterLabels[key].push(labels[key]);
} }
} else { } else {
this.#filterLabels[key] = [labels[key]]; this.filterLabels[key] = [labels[key]];
} }
} }
} }
@ -295,7 +226,7 @@ export class GraphPlot extends HTMLElement {
* @param {string} key * @param {string} key
* @returns {HTMLDivElement} * @returns {HTMLDivElement}
*/ */
buildSelectElement(key) { buildSelectElement(key, me) {
var id = key + "-select" + Math.random(); var id = key + "-select" + Math.random();
const element = document.createElement("div"); const element = document.createElement("div");
const select = document.createElement("select"); const select = document.createElement("select");
@ -307,7 +238,7 @@ export class GraphPlot extends HTMLElement {
const optValue = "Select All: " + key; const optValue = "Select All: " + key;
optElement.innerText = optValue; optElement.innerText = optValue;
select.appendChild(optElement); select.appendChild(optElement);
for (var opt of this.#filterLabels[key]) { for (var opt of this.filterLabels[key]) {
const optElement = document.createElement("option"); const optElement = document.createElement("option");
optElement.setAttribute("value", opt); optElement.setAttribute("value", opt);
optElement.setAttribute("selected", "selected"); optElement.setAttribute("selected", "selected");
@ -344,36 +275,126 @@ export class GraphPlot extends HTMLElement {
filteredValues.push(o.value); filteredValues.push(o.value);
} }
} }
self.#filteredLabelSets[key] = filteredValues; self.filteredLabelSets[key] = filteredValues;
self.reset(true); me.reset(true);
}; };
element.appendChild(select); element.appendChild(select);
return element; return element;
} }
buildFilterMenu() { // FIXME(jwall): We pass the element down but that couples a little too tightly. We should do this differently.
buildFilterMenu(me) {
// We need to maintain a stable order for these // We need to maintain a stable order for these
var children = []; var children = [];
for (var key of Object.keys(this.#filterLabels).sort()) { for (var key of Object.keys(this.filterLabels).sort()) {
// If there are multiple items to filter by then show the selectElement. // If there are multiple items to filter by then show the selectElement.
// otherwise there is no point. // otherwise there is no point.
if (this.#filterLabels[key].length > 1) { if (this.filterLabels[key].length > 1) {
const element = this.#filterSelectElements[key] || this.buildSelectElement(key); const element = this.filterSelectElements[key] || this.buildSelectElement(key, me);
children.push(element); children.push(element);
} }
} }
this.#menuContainer.replaceChildren(...children); this.menuContainer.replaceChildren(...children);
}
attributeChangedHandler(name, newValue) {
switch (name) {
case 'uri':
this.uri = newValue;
break;
case 'width':
this.width = Number(newValue);
break;
case 'height':
this.height = Number(newValue);
break;
case 'poll-seconds':
this.pollSeconds = Number(newValue);
break;
case 'end':
this.end = newValue;
break;
case 'duration':
this.config.duration = Number(newValue);
break;
case 'step-duration':
this.step_duration = newValue;
break;
case 'd3-tick-format':
this.config.d3TickFormat = newValue;
break;
case 'allow-uri-filters':
this.allowUriFilters = Boolean(newValue);
break;
default: // do nothing;
break;
}
}
}
/**
* Custom element for showing Log Output.
*
* @extends HTMLElement
*/
export class LogPlot extends HTMLElement {
/** @type {?ElementConfig} */
#config;
constructor() {
super();
this.#config = new ElementConfig(this);
}
static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration', 'd3-tick-format', 'allow-uri-filter'];
/**
* Callback for attributes changes.
*
* @param {string} name - The name of the attribute.
* @param {?string} _oldValue - The old value for the attribute
* @param {?string} newValue - The new value for the attribute
*/
attributeChangedCallback(name, _oldValue, newValue) {
this.#config.attributeChangedHandler(name, newValue);
this.reset();
}
connectedCallback() {
this.#config.connectedHandler(this);
this.reset(true);
}
disconnectedCallback() {
this.#config.stopInterval()
}
static elementName = "log-plot";
/** Registers the custom element if it doesn't already exist */
static registerElement() {
if (!customElements.get(LogPlot.elementName)) {
customElements.define(LogPlot.elementName, LogPlot);
}
} }
/** /**
* @param {QueryData|LogLineList} graph * Resets the entire graph and then restarts polling.
* @param {boolean=} updateOnly
*/ */
getLabelsForData(graph) { reset(updateOnly) {
if (/** @type {QueryData} */(graph).plots) { var self = this;
this.getLabelsForQueryData(/** @type {QueryData} */(graph)); self.#config.stopInterval()
} else { self.#config.fetchData().then((data) => {
this.getLabelsForLogLines(/** @type {LogLineList} */(graph)); if (!updateOnly) {
self.getLabelsForLogLines(data.Metrics || data.Logs.lines);
self.#config.buildFilterMenu(this);
} }
self.updateGraph(data).then(() => {
self.#config.intervalId = setInterval(() => self.updateGraph(), 1000 * self.#config.pollSeconds);
});
});
} }
/** /**
@ -383,7 +404,7 @@ export class GraphPlot extends HTMLElement {
if (graph.Stream) { if (graph.Stream) {
for (const pair of graph.Stream) { for (const pair of graph.Stream) {
const labels = pair[0]; const labels = pair[0];
this.populateFilterData(labels); this.#config.populateFilterData(labels);
} }
} }
if (graph.StreamInstant) { if (graph.StreamInstant) {
@ -391,102 +412,6 @@ export class GraphPlot extends HTMLElement {
} }
} }
/**
* @param {QueryData} graph
*/
getLabelsForQueryData(graph) {
const data = graph.plots;
for (var subplot of data) {
if (subplot.Series) {
for (const triple of subplot.Series) {
const labels = triple[0];
this.populateFilterData(labels);
}
}
if (subplot.Scalar) {
for (const triple of subplot.Scalar) {
const labels = triple[0];
this.populateFilterData(labels);
}
}
}
}
yaxisNameGenerator() {
var counter = 1;
return function() {
var name = "yaxis";
if (counter != 1) {
name = "yaxis" + counter;
}
counter++;
return name;
};
}
/**
* @param {any} triple
*/
buildSeriesPlot(triple) {
const labels = /** @type {Map<String, String>} */(triple[0]);
for (var label in labels) {
var show = this.#filteredLabelSets[label];
if (show && !show.includes(labels[label])) {
return null;
}
}
const config = /** @type {PlotConfig} */(triple[1]);
var yaxis = config.yaxis || "y";
// https://plotly.com/javascript/reference/layout/yaxis/
const series = triple[2];
const trace = /** @type GraphTrace */({
type: "scatter",
mode: "lines+text",
x: [],
y: [],
// We always share the x axis for timeseries graphs.
xaxis: "x",
yaxis: yaxis,
//yhoverformat: yaxis.tickformat,
});
if (config.fill) {
trace.fill = config.fill;
}
var name = this.formatName(config, labels);
if (name) { trace.name = name; }
for (const point of series) {
trace.x.push(new Date(point.timestamp * 1000));
trace.y.push(point.value);
}
return trace;
}
/**
* @param {any} triple
*/
buildScalarPlot(triple) {
const labels = /** @type {Map<String,String>} */(triple[0]);
for (var label in labels) {
var show = this.#filteredLabelSets[label];
if (show && !show.includes(labels[label])) {
return null;
}
}
const config = /** @type {PlotConfig} */(triple[1]);
const series = triple[2];
const trace = /** @type GraphTrace */({
type: "bar",
x: [],
y: [],
yhoverformat: config["d3_tick_format"],
});
var name = this.formatName(config, labels);
if (name) { trace.name = name; }
trace.y.push(series.value);
trace.x.push(trace.name);
return trace;
}
/** /**
* @param {Array} stream * @param {Array} stream
* *
@ -501,7 +426,7 @@ export class GraphPlot extends HTMLElement {
const labels = pair[0]; const labels = pair[0];
var labelList = []; var labelList = [];
for (var label in labels) { for (var label in labels) {
var show = this.#filteredLabelSets[label]; var show = this.#config.filteredLabelSets[label];
if (show && !show.includes(labels[label])) { if (show && !show.includes(labels[label])) {
continue loopStream; continue loopStream;
} }
@ -528,10 +453,10 @@ export class GraphPlot extends HTMLElement {
async updateGraph(maybeGraph) { async updateGraph(maybeGraph) {
var graph = maybeGraph; var graph = maybeGraph;
if (!graph) { if (!graph) {
graph = await this.fetchData(); graph = await this.#config.fetchData();
} }
if (graph.Metrics) { if (graph.Metrics) {
this.updateMetricsGraph(graph.Metrics); // FIXME(zaphar): Log an Error;
} else if (graph.Logs) { } else if (graph.Logs) {
this.updateLogsView(graph.Logs.lines); this.updateLogsView(graph.Logs.lines);
} else { } else {
@ -588,7 +513,177 @@ export class GraphPlot extends HTMLElement {
} }
// https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact // https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
// @ts-ignore // @ts-ignore
Plotly.react(this.getTargetNode(), traces, layout, null); Plotly.react(this.#config.getTargetNode(), traces, layout, null);
}
}
LogPlot.registerElement();
/**
* Custom element for showing a plotly graph.
*
* @extends HTMLElement
*/
export class GraphPlot extends HTMLElement {
/** @type {?ElementConfig} */
#config;
constructor() {
super();
this.#config = new ElementConfig(this);
}
static observedAttributes = ['uri', 'width', 'height', 'poll-seconds', 'end', 'duration', 'step-duration', 'd3-tick-format', 'allow-uri-filter'];
/**
* Callback for attributes changes.
*
* @param {string} name - The name of the attribute.
* @param {?string} _oldValue - The old value for the attribute
* @param {?string} newValue - The new value for the attribute
*/
attributeChangedCallback(name, _oldValue, newValue) {
this.#config.attributeChangedHandler(name, newValue);
this.reset();
}
connectedCallback() {
this.#config.connectedHandler(this);
this.reset(true);
}
disconnectedCallback() {
this.#config.stopInterval()
}
static elementName = "graph-plot";
/**
* Resets the entire graph and then restarts polling.
* @param {boolean=} updateOnly
*/
reset(updateOnly) {
var self = this;
self.#config.stopInterval()
self.#config.fetchData().then((data) => {
if (!updateOnly) {
self.getLabelsForQueryData(data.Metrics || data.Logs.lines);
self.#config.buildFilterMenu(this);
}
self.updateGraph(data).then(() => {
self.#config.intervalId = setInterval(() => self.updateGraph(), 1000 * self.#config.pollSeconds);
});
});
}
/** Registers the custom element if it doesn't already exist */
static registerElement() {
if (!customElements.get(GraphPlot.elementName)) {
customElements.define(GraphPlot.elementName, GraphPlot);
}
}
/**
* @param {QueryData} graph
*/
getLabelsForQueryData(graph) {
const data = graph.plots;
for (var subplot of data) {
if (subplot.Series) {
for (const triple of subplot.Series) {
const labels = triple[0];
this.#config.populateFilterData(labels);
}
}
if (subplot.Scalar) {
for (const triple of subplot.Scalar) {
const labels = triple[0];
this.#config.populateFilterData(labels);
}
}
}
}
/**
* @param {any} triple
*/
buildSeriesPlot(triple) {
const labels = /** @type {Map<String, String>} */(triple[0]);
for (var label in labels) {
var show = this.#config.filteredLabelSets[label];
if (show && !show.includes(labels[label])) {
return null;
}
}
const config = /** @type {PlotConfig} */(triple[1]);
var yaxis = config.yaxis || "y";
// https://plotly.com/javascript/reference/layout/yaxis/
const series = triple[2];
const trace = /** @type GraphTrace */({
type: "scatter",
mode: "lines+text",
x: [],
y: [],
// We always share the x axis for timeseries graphs.
xaxis: "x",
yaxis: yaxis,
//yhoverformat: yaxis.tickformat,
});
if (config.fill) {
trace.fill = config.fill;
}
var name = formatName(config, labels);
if (name) { trace.name = name; }
for (const point of series) {
trace.x.push(new Date(point.timestamp * 1000));
trace.y.push(point.value);
}
return trace;
}
/**
* @param {any} triple
*/
buildScalarPlot(triple) {
const labels = /** @type {Map<String,String>} */(triple[0]);
for (var label in labels) {
var show = this.#config.filteredLabelSets[label];
if (show && !show.includes(labels[label])) {
return null;
}
}
const config = /** @type {PlotConfig} */(triple[1]);
const series = triple[2];
const trace = /** @type GraphTrace */({
type: "bar",
x: [],
y: [],
yhoverformat: config["d3_tick_format"],
});
var name = formatName(config, labels);
if (name) { trace.name = name; }
trace.y.push(series.value);
trace.x.push(trace.name);
return trace;
}
/**
* Update the graph with new data.
*
* @param {?QueryPayload=} maybeGraph
*/
async updateGraph(maybeGraph) {
var graph = maybeGraph;
if (!graph) {
graph = await this.#config.fetchData();
}
if (graph.Metrics) {
this.updateMetricsGraph(graph.Metrics);
} else if (graph.Logs) {
// FIXME(zaphar): Log an Error;
} else {
}
} }
/** /**
@ -617,16 +712,16 @@ export class GraphPlot extends HTMLElement {
if (graph.legend_orientation) { if (graph.legend_orientation) {
layout.legend.orientation = graph.legend_orientation; layout.legend.orientation = graph.legend_orientation;
} }
var nextYaxis = this.yaxisNameGenerator(); var nextYaxis = yaxisNameGenerator();
for (const yaxis of yaxes) { for (const yaxis of yaxes) {
yaxis.tickformat = yaxis.tickformat || this.#d3TickFormat; yaxis.tickformat = yaxis.tickformat || this.#config.d3TickFormat;
yaxis.gridColor = getCssVariableValue("--grid-line-color"); yaxis.gridColor = getCssVariableValue("--grid-line-color");
layout[nextYaxis()] = yaxis; layout[nextYaxis()] = yaxis;
} }
var traces = /** @type {Array<PlotTrace>} */ ([]); var traces = /** @type {Array<PlotTrace>} */ ([]);
for (var subplot_idx in data) { for (var subplot_idx in data) {
const subplot = data[subplot_idx]; const subplot = data[subplot_idx];
var nextYaxis = this.yaxisNameGenerator(); var nextYaxis = yaxisNameGenerator();
if (subplot.Series) { if (subplot.Series) {
// https://plotly.com/javascript/reference/scatter/ // https://plotly.com/javascript/reference/scatter/
for (const triple of subplot.Series) { for (const triple of subplot.Series) {
@ -647,7 +742,7 @@ export class GraphPlot extends HTMLElement {
} }
// https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact // https://plotly.com/javascript/plotlyjs-function-reference/#plotlyreact
// @ts-ignore // @ts-ignore
Plotly.react(this.getTargetNode(), traces, layout, null); Plotly.react(this.#config.getTargetNode(), traces, layout, null);
} }
} }
@ -702,6 +797,11 @@ export class SpanSelector extends HTMLElement {
node.setAttribute('duration', this.#durationInput.value); node.setAttribute('duration', this.#durationInput.value);
node.setAttribute('step-duration', this.#stepDurationInput.value); node.setAttribute('step-duration', this.#stepDurationInput.value);
} }
for (var node of document.getElementsByTagName(LogPlot.elementName)) {
node.setAttribute('end', this.#endInput.value);
node.setAttribute('duration', this.#durationInput.value);
node.setAttribute('step-duration', this.#stepDurationInput.value);
}
} }
static elementName = "span-selector"; static elementName = "span-selector";