Files
rio/frontend/code/components/plot.ts
2024-11-13 10:43:57 +01:00

176 lines
5.2 KiB
TypeScript

import { fillToCss } from "../cssUtils";
import { AnyFill } from "../dataModels";
import { getAllocatedHeightInPx, getAllocatedWidthInPx } from "../utils";
import { ComponentBase, ComponentState } from "./componentBase";
type PlotlyType = any;
type PlotlyPlot = {
type: "plotly";
json: string;
};
type MatplotlibPlot = {
type: "matplotlib";
svg: string;
};
type PlotState = ComponentState & {
_type_: "Plot-builtin";
plot: PlotlyPlot | MatplotlibPlot;
background: AnyFill | null;
corner_radius?: [number, number, number, number];
};
export class PlotComponent extends ComponentBase {
declare state: Required<PlotState>;
// I know this abstraction looks like overkill, but plotly does so much
// stuff with a time delay (loading plotly, setTimeout, resizeObserver, ...)
// that it's just a giant mess of race conditions if it's not all
// represented as a single object that we can easily swap out.
private plotManager: PlotManager | null = null;
createElement(): HTMLElement {
let element = document.createElement("div");
element.classList.add("rio-plot");
return element;
}
updateElement(
deltaState: PlotState,
latentComponents: Set<ComponentBase>
): void {
super.updateElement(deltaState, latentComponents);
if (deltaState.plot !== undefined) {
if (this.plotManager !== null) {
this.plotManager.element.remove();
this.plotManager.destroy();
}
if (deltaState.plot.type === "plotly") {
this.plotManager = new PlotlyManager(deltaState.plot);
} else {
this.plotManager = new MatplotlibManager(deltaState.plot);
}
this.element.appendChild(this.plotManager.element);
}
if (deltaState.background === null) {
this.element.style.background = "var(--rio-local-bg-variant)";
} else if (deltaState.background !== undefined) {
Object.assign(this.element.style, fillToCss(deltaState.background));
}
if (deltaState.corner_radius !== undefined) {
let [topLeft, topRight, bottomRight, bottomLeft] =
deltaState.corner_radius;
this.element.style.borderRadius = `${topLeft}rem ${topRight}rem ${bottomRight}rem ${bottomLeft}rem`;
}
}
onDestruction(): void {
super.onDestruction();
if (this.plotManager !== null) {
this.plotManager.destroy();
}
}
}
interface PlotManager {
get element(): HTMLElement;
destroy(): void;
}
class MatplotlibManager implements PlotManager {
element: HTMLElement;
constructor(plot: MatplotlibPlot) {
this.element = document.createElement("div");
this.element.innerHTML = plot.svg;
let svgElement = this.element.querySelector("svg") as SVGElement;
svgElement.style.width = "100%";
svgElement.style.height = "100%";
}
destroy(): void {}
}
class PlotlyManager implements PlotManager {
element: HTMLElement;
private plotDiv: HTMLDivElement;
private resizeObserver: ResizeObserver | null = null;
constructor(plot: PlotlyPlot) {
this.element = document.createElement("div");
this.element.classList.add("rio-plotly-plot");
this.plotDiv = document.createElement("div");
this.element.appendChild(this.plotDiv);
this.makePlot(plot);
}
destroy(): void {
if (this.resizeObserver !== null) {
this.resizeObserver.disconnect();
}
}
private async makePlot(plot: PlotlyPlot): Promise<void> {
let plotJson = JSON.parse(plot.json);
// Make the plot transparent so the component's background
// can shine through
plotJson.layout.paper_bgcolor = "rgba(0,0,0,0)";
plotJson.layout.plot_bgcolor = "rgba(0,0,0,0)";
let Plotly = await getPlotly();
Plotly.newPlot(this.plotDiv, plotJson.data, plotJson.layout);
// Wait until all components have been created (and
// `updateElement` called), then tell plotly how much space we
// have
setTimeout(() => {
// Plotly is too stupid to layout itself. Help out.
this.resizeObserver = new ResizeObserver(() => {
// Inform plotly of the new size
Plotly.relayout(this.plotDiv, {
width: getAllocatedWidthInPx(this.element),
height: getAllocatedHeightInPx(this.element),
});
});
this.resizeObserver.observe(this.element);
}, 0);
}
}
let fetchPlotlyPromise: Promise<void> | null = null;
function getPlotly(): Promise<PlotlyType> {
if (fetchPlotlyPromise === null) {
console.debug("Fetching plotly.js");
fetchPlotlyPromise = new Promise<PlotlyType>((resolve) => {
let script = document.createElement("script");
script.onload = () => {
resolve(window["Plotly"]);
};
script.src = `${globalThis.RIO_BASE_URL}rio/assets/special/plotly.min.js`;
document.head.appendChild(script);
});
}
return fetchPlotlyPromise;
}