mirror of
https://github.com/rio-labs/rio.git
synced 2025-12-31 02:09:48 -06:00
176 lines
5.2 KiB
TypeScript
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;
|
|
}
|