[Feature] Use Plotly V6 For Charting Extension + (#7132)

* update charting extension for plotly v6 and do some cleanup

* pylint

* formatting

* plotly.html
This commit is contained in:
Danglewood
2025-05-24 12:48:37 -07:00
committed by GitHub
parent a4319a6dd5
commit d91c9b9c35
27 changed files with 2154 additions and 2467 deletions

View File

@@ -16,7 +16,13 @@ type PopupData = {
bordercolor: string;
color: string;
size: number;
bgcolor?: string;
arrowcolor?: string;
arrowsize?: number;
arrowwidth?: number;
annotation?: any;
high?: number;
low?: number;
};
export function add_annotation({
@@ -58,14 +64,15 @@ export function add_annotation({
text: popup_data.text,
showarrow: true,
arrowhead: 2,
arrowsize: 1,
arrowwidth: 2,
arrowsize: popup_data.arrowsize || 1,
arrowwidth: popup_data.arrowwidth || 2,
ax: x,
ay: y + popup_data.yshift,
ayref: yref,
axref: "x",
bordercolor: popup_data.bordercolor,
bgcolor: "#000000",
bgcolor: popup_data.bgcolor || "#000000",
arrowcolor: popup_data.arrowcolor || popup_data.bordercolor,
borderwidth: 2,
borderpad: 4,
opacity: 0.8,
@@ -86,6 +93,10 @@ export function add_annotation({
annotations[index].font.size = popup_data.size;
annotations[index].ay = y + popup_data.yshift;
annotations[index].bordercolor = popup_data.bordercolor;
annotations[index].bgcolor = popup_data.bgcolor || "#000000";
annotations[index].arrowcolor = popup_data.arrowcolor || popup_data.bordercolor;
annotations[index].arrowsize = popup_data.arrowsize || 1;
annotations[index].arrowwidth = popup_data.arrowwidth || 2;
annotations[index].high = popup_data.high || undefined;
annotations[index].low = popup_data.low || undefined;
}
@@ -204,6 +215,11 @@ export function init_annotation({
return;
}
// First remove any existing click handlers to avoid duplicates
plotDiv.removeAllListeners("plotly_clickannotation");
plotDiv.removeAllListeners("plotly_click");
// Add handler for clicking on existing annotations
plotDiv.on("plotly_clickannotation", (eventData) => {
console.log("plotly_clickannotation", eventData);
const annotation = eventData.annotation;
@@ -235,6 +251,7 @@ export function init_annotation({
setOnAnnotationClick({});
});
// Add handler for adding a new annotation on click
function clickHandler(eventData: PlotMouseEvent) {
console.log("plotly_click", eventData);
const x = eventData.points[0].x;
@@ -282,13 +299,23 @@ export function init_annotation({
setAnnotations(
[...annotations, to_update.annotation].filter((a) => a !== undefined),
);
// Important: update plotData with the new annotations to make them visible
plotData.layout.dragmode = "pan";
setPlotData({ ...plotData, ...to_update.update });
// Force a relayout to ensure annotations appear
Plotly.relayout(plotDiv, {'annotations': to_update.update.annotations, dragmode: "pan"});
// Remove click handler after creating the annotation
plotDiv.removeAllListeners("plotly_click");
}
// Set up dragmode and add the click handler
plotData.layout.dragmode = "select";
setPlotData({ ...plotData });
// Ensure we add the click handler
plotDiv.on("plotly_click", clickHandler);
}
}

View File

@@ -1,239 +1,4 @@
// @ts-nocheck
import domtoimage from "dom-to-image";
import * as Plotly from "plotly.js-dist-min";
import { Figure } from "react-plotly.js";
const exportNativeFileSystem = async ({
fileHandle,
blob,
}: {
fileHandle?: FileSystemFileHandle | null;
blob: Blob;
}) => {
if (!fileHandle) {
return;
}
return writeFileHandler({ fileHandle, blob });
};
const writeFileHandler = async ({
fileHandle,
blob,
}: {
fileHandle: FileSystemFileHandle;
blob: Blob;
}) => {
const writer = await fileHandle.createWritable();
await writer.write(blob);
await writer.close();
};
const IMAGE_TYPE: FilePickerAcceptType[] = [
{
description: "PNG Image",
accept: {
"image/png": [".png"],
},
},
{
description: "JPEG Image",
accept: {
"image/jpeg": [".jpeg"],
},
},
{
description: "SVG Image",
accept: {
"image/svg+xml": [".svg"],
},
},
];
const getNewFileHandle = ({
filename,
is_image,
}: {
filename: string;
is_image?: boolean;
}): Promise<FileSystemFileHandle | null> => {
try {
if ("showSaveFilePicker" in window) {
const opts: SaveFilePickerOptions = {
suggestedName: filename,
types: is_image
? IMAGE_TYPE
: [
{
description: "CSV File",
accept: {
"image/csv": [".csv"],
},
},
],
excludeAcceptAllOption: true,
};
return showSaveFilePicker(opts);
}
} catch (error) {
console.error(error);
}
return new Promise((resolve) => {
resolve(null);
});
};
export const saveToFile = (
blob: Blob,
fileName: string,
fileHandle?: FileSystemFileHandle | null,
) => {
try {
if (fileHandle === null) {
throw new Error("Cannot access filesystem");
}
return exportNativeFileSystem({ fileHandle, blob });
} catch (error) {
console.error("oops, something went wrong!", error);
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", fileName);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
return new Promise((resolve) => {
resolve(true);
});
};
export async function downloadCSV(
gd: Figure,
downloadFinished: (change: boolean) => void,
) {
const data = gd.data;
let columns: string[] = [];
const rows: any[] = [];
const xaxis =
"title" in gd.layout["xaxis"] &&
gd.layout["xaxis"]["title"]["text"] !== undefined
? gd.layout["xaxis"]["title"]["text"]
: "x";
const yaxis =
"title" in gd.layout["yaxis"] &&
gd.layout["yaxis"]["title"]["text"] !== undefined
? gd.layout["yaxis"]["title"]["text"]
: "y";
data.forEach(function (trace) {
if (trace.type === "candlestick") {
if (columns.length === 0) {
columns = ["Date", "Open", "High", "Low", "Close"];
}
trace.x.forEach(function (x, i) {
rows.push([
x,
trace.open[i],
trace.high[i],
trace.low[i],
trace.close[i],
]);
});
}
if (["scatter", "bar"].includes(trace.type)) {
if (columns.length === 0) {
columns.push(xaxis);
}
columns.push(trace.name !== undefined ? trace.name : yaxis);
trace.x.forEach(function (x, i) {
if (rows[i] === undefined) {
rows[i] = [x];
}
rows[i].push(trace.y[i]);
});
}
});
return await downloadData(columns, rows, downloadFinished);
}
export async function downloadData(
columns: any,
data: any,
downloadFinished: (change: boolean) => void,
) {
const headers = columns;
const rows = data.map((row: any) =>
row.map((cell: any) => {
if (cell == null) {
return "";
} else if (typeof cell === "object") {
return JSON.stringify(cell);
} else {
return cell.toString().replace(/"/g, '""');
}
}),
);
const csvData = [headers, ...rows];
const csvContent = csvData.map((e) => e.join(",")).join("\n");
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const filename = `${window.title}.csv`;
try {
const fileHandle = await getNewFileHandle({
filename: filename,
});
await loadingOverlay("Saving CSV");
non_blocking(async function () {
// @ts-ignore
saveToFile(blob, filename, fileHandle).then(async function () {
await new Promise((resolve) => setTimeout(resolve, 1500));
if (!fileHandle) {
downloadFinished(true);
}
await loadingOverlay("", true);
});
}, 2)();
} catch (error) {
console.error(error);
}
}
function loadingOverlay(message?: string, is_close?: boolean) {
const loading = window.document.getElementById("loading") as HTMLElement;
const loading_text = window.document.getElementById(
"loading_text",
) as HTMLElement;
return new Promise((resolve) => {
if (is_close) {
loading.classList.remove("show");
} else {
// @ts-ignore
loading_text.innerHTML = message;
loading.classList.add("show");
}
const is_loaded = setInterval(function () {
if (
is_close
? !loading.classList.contains("show")
: loading.classList.contains("show")
) {
clearInterval(is_loaded);
resolve(true);
}
}, 0.01);
});
}
export const non_blocking = (func: Function, delay: number) => {
let timeout: number;
@@ -246,139 +11,3 @@ export const non_blocking = (func: Function, delay: number) => {
};
};
const openbb_watermark = {
yref: "paper",
xref: "paper",
x: 1,
y: 0,
text: "OpenBB",
font_size: 17,
font_color: "gray",
opacity: 0.5,
xanchor: "right",
yanchor: "bottom",
yshift: -80,
xshift: 40,
};
async function setWatermarks(margin, old_index, init = false) {
const chart = document.getElementById("plotlyChart") as HTMLElement;
if (init) {
chart.layout.annotations.push(openbb_watermark);
if (
chart.globals.cmd_idx !== undefined &&
chart.globals.cmd_src !== undefined
) {
chart.layout.annotations[chart.globals.cmd_idx].text =
chart.globals.cmd_src;
}
Plotly.relayout(chart, {
"title.text": chart.globals.title,
margin: chart.globals.old_margin,
});
}
if (!init) {
if (
chart.globals.cmd_idx !== undefined &&
chart.globals.cmd_src !== undefined
) {
chart.layout.annotations[chart.globals.cmd_idx].text = "";
}
chart.layout.annotations.splice(old_index, 1);
Plotly.relayout(chart, {
"title.text": "",
margin: margin,
});
}
}
export async function saveImage(
id: string,
filename: string,
extension: string,
) {
const chart = document.getElementById(id) as HTMLElement;
if (["svg"].includes(extension)) {
const chart = document.getElementById("plotlyChart") as HTMLElement;
const margin = chart.layout.margin;
const old_index = chart.layout.annotations.length;
await setWatermarks(margin, old_index, true);
Plotly.downloadImage(chart, {
format: "svg",
height: chart.clientHeight,
width: chart.clientWidth,
filename: window.title,
});
await setWatermarks(margin, old_index, false);
await loadingOverlay("", true);
return;
}
non_blocking(async function () {
domtoimage.toBlob(chart).then(function (blob: Blob) {
saveToFile(blob, filename, null);
});
}, 2)();
}
export async function downloadImage(
id: string,
hidemodebar: () => void,
loading: (bool: boolean) => void,
downloadFinished: (bool: boolean) => void,
) {
const chart = document.getElementById(id) as HTMLElement;
const filename = `${window.title}.png`;
try {
loading(true);
const fileHandle = await getNewFileHandle({
filename: filename,
is_image: true,
});
let extension = "png";
if (fileHandle !== null) {
// @ts-ignore
extension = fileHandle.name.split(".").pop();
}
await loadingOverlay(`Saving ${extension.toUpperCase()}`).then(
setTimeout(async function () {
if (["svg"].includes(extension)) {
await saveImage(id, filename, extension);
hidemodebar(false);
loading(false);
if (!fileHandle) {
downloadFinished(true);
}
return;
}
non_blocking(async function () {
domtoimage.toBlob(chart).then(function (blob: Blob) {
saveToFile(blob, filename, fileHandle).then(async function () {
await loadingOverlay("", true);
hidemodebar(false);
loading(false);
if (!fileHandle) {
downloadFinished(true);
}
});
});
}, 2)();
}, 100),
);
} catch (error) {
console.error(error);
hidemodebar(false);
loading(false);
}
loading(false);
}