This commit is contained in:
teh_coderer
2023-05-01 10:46:48 -04:00
parent c603a360ec
commit 4100d393de
41 changed files with 28293 additions and 373 deletions

175
frontend-components/plotly/src/App.tsx vendored Normal file
View File

@@ -0,0 +1,175 @@
//@ts-nocheck
import { PostHogProvider } from "posthog-js/react";
import { useEffect, useState } from "react";
import Chart from "./components/Chart";
import { plotlyMockup, candlestickMockup } from "./data/mockup";
declare global {
interface Window {
plotly_figure: any;
export_image: string;
save_image: boolean;
title: string;
Plotly: any;
MODEBAR: HTMLElement;
}
}
function App() {
const [data, setData] = useState(
process.env.NODE_ENV === "production" ? null : candlestickMockup
);
const [options, setOptions] = useState({});
useEffect(() => {
if (process.env.NODE_ENV === "production") {
const interval = setInterval(() => {
if (window.plotly_figure) {
const data = window.plotly_figure;
console.log(data);
setData(data);
clearInterval(interval);
}
}, 100);
return () => clearInterval(interval);
}
}, []);
const transformData = (data: any) => {
if (!data) return null;
let filename = data.layout?.title?.text
.replace(/<b>|<\/b>/g, "")
.replace(/ /g, "_");
let date = new Date().toISOString().slice(0, 10).replace(/-/g, "");
let time = new Date().toISOString().slice(11, 19).replace(/:/g, "");
window.title = `openbb_${filename}_${date}_${time}`;
let extension = "";
if (window.export_image != undefined) {
filename = window.export_image.split("/").pop();
extension = filename.split(".").pop().replace("jpg", "jpeg");
}
if (data.layout.annotations != undefined) {
data.layout.annotations.forEach(function (annotation) {
if (annotation.text != undefined && !["svg", "pdf"].includes(extension))
if (annotation.text[0] == "/") {
annotation.text = "";
let margin = data.layout.margin;
if (margin.t != undefined && margin.t > 40) margin.t = 40;
margin.l = 70;
if (data.cmd == "/stocks/candle") margin.r -= 40;
}
});
}
let title = data.layout?.title?.text;
return {
data: data,
date: new Date(),
cmd: data.command_location,
posthog: data.posthog,
python_version: data.python_version,
pywry_version: data.pywry_version,
terminal_version: data.terminal_version,
title,
};
};
const transformedData = transformData(data);
if (transformedData) {
if (transformedData.posthog.collect_logs && !options) {
const opts = {
api_host: "https://app.posthog.com",
autocapture: {
css_selector_allowlist: [".ph-capture"],
},
capture_pageview: false,
loaded: function (posthog: any) {
const log_id = transformedData?.log_id || "";
if (log_id != "" && log_id != "REPLACE_ME") posthog.identify(log_id);
posthog.onFeatureFlags(function () {
if (
!posthog.isFeatureEnabled("record-pywry", { send_event: false })
)
posthog.stopSessionRecording();
if (
!posthog.isFeatureEnabled("collect-logs-pywry", {
send_event: false,
})
)
posthog.opt_out_capturing();
else if (posthog.has_opted_out_capturing())
posthog.opt_in_capturing();
});
},
};
setOptions(opts);
}
const info = {
INFO: {
command: transformedData.cmd,
title: transformedData.title,
date: transformedData.date,
python_version: transformedData.python_version,
pywry_version: transformedData.pywry_version,
terminal_version: transformedData.terminal_version,
},
};
const chartDiv = (
<Chart
json={transformedData.data}
date={transformedData.date}
cmd={transformedData.cmd}
title={transformedData.title}
info={info}
/>
);
if (transformedData.posthog.collect_logs && options) {
return (
<PostHogProvider
apiKey="phc_vhssDAMod5qIplznQ75Kdgz4aB1qPFmeVmfEOZ4hkRw"
options={options}
>
{chartDiv}
</PostHogProvider>
);
}
return chartDiv;
} else
return (
<div className="absolute inset-0 flex items-center justify-center z-[100]">
<svg
className="animate-spin h-20 w-20 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v8z"
></path>
</svg>
</div>
);
}
export default App;

View File

@@ -0,0 +1,141 @@
//@ts-nocheck
import { Figure } from "react-plotly.js";
export default async function autoScaling(
eventdata: Readonly<Plotly.PlotRelayoutEvent>,
graphs: Figure
) {
try {
if (eventdata["xaxis.range[0]"] != undefined) {
const x_min = eventdata["xaxis.range[0]"];
const x_max = eventdata["xaxis.range[1]"];
let to_update = {};
let y_min, y_max;
const YaxisData = graphs.data.filter((trace) => trace.yaxis != undefined);
const yaxis_unique = [
...new Set(
YaxisData.map(
(trace) =>
trace.yaxis || trace.y != undefined || trace.type == "candlestick"
)
),
];
const get_all_yaxis_traces = (yaxis) => {
return graphs.data.filter(
(trace) =>
trace.yaxis == yaxis && (trace.y || trace.type == "candlestick")
);
};
yaxis_unique.forEach((unique) => {
if (typeof unique != "string") {
return;
}
let yaxis = "yaxis" + unique.replace("y", "");
let y_candle = [];
let y_values = [];
let log_scale = graphs.layout[yaxis].type == "log";
get_all_yaxis_traces(unique).forEach((trace2) => {
let x = trace2.x;
log_scale = graphs.layout[yaxis].type == "log";
let y = trace2.y != undefined ? trace2.y : [];
let y_low = trace2.type == "candlestick" ? trace2.low : [];
let y_high = trace2.type == "candlestick" ? trace2.high : [];
if (log_scale) {
y = y.map(Math.log10);
if (trace2.type == "candlestick") {
y_low = trace2.low.map(Math.log10);
y_high = trace2.high.map(Math.log10);
}
}
let yx_values = x.map((x, i) => {
let out = null;
if (x >= x_min && x <= x_max) {
if (trace2.y != undefined) {
console.log(trace2.name, trace2.type, trace2.yaxis);
out = y[i];
}
if (trace2.type == "candlestick") {
y_candle.push(y_low[i]);
y_candle.push(y_high[i]);
}
}
return out;
});
y_values = y_values.concat(yx_values);
});
y_values = y_values.filter((y2) => y2 != undefined && y2 != null);
y_min = Math.min(...y_values);
y_max = Math.max(...y_values);
if (y_candle.length > 0) {
y_candle = y_candle.filter((y2) => y2 != undefined && y2 != null);
y_min = Math.min(...y_candle);
y_max = Math.max(...y_candle);
}
let org_y_max = y_max;
let is_volume =
graphs.layout[yaxis].fixedrange != undefined &&
graphs.layout[yaxis].fixedrange == true;
if (y_min != undefined && y_max != undefined) {
let y_range = y_max - y_min;
let y_mult = 0.15;
if (y_candle.length > 0) {
y_mult = 0.3;
}
y_min -= y_range * y_mult;
y_max += y_range * y_mult;
if (is_volume) {
if (graphs.layout[yaxis].tickvals != undefined) {
const range_x = 7;
let volume_ticks = org_y_max;
let round_digits = -3;
let first_val = Math.round(volume_ticks * 0.2, round_digits);
let x_zipped = [2, 5, 6, 7, 8, 9, 10];
let y_zipped = [1, 4, 5, 6, 7, 8, 9];
for (let i = 0; i < x_zipped.length; i++) {
if (String(volume_ticks).length > x_zipped[i]) {
round_digits = -y_zipped[i];
first_val = Math.round(volume_ticks * 0.2, round_digits);
}
}
let tickvals = [
Math.floor(first_val),
Math.floor(first_val * 2),
Math.floor(first_val * 3),
Math.floor(first_val * 4),
];
let volume_range = [0, Math.floor(volume_ticks * range_x)];
to_update[yaxis + ".tickvals"] = tickvals;
to_update[yaxis + ".range"] = volume_range;
to_update[yaxis + ".tickformat"] = ".2s";
return;
}
y_min = 0;
y_max = graphs.layout[yaxis].range[1];
}
to_update[yaxis + ".range"] = [y_min, y_max];
}
});
return to_update;
}
} catch (e) {
console.log(`Error in AutoScaling: ${e}`);
}
return {};
}

View File

@@ -0,0 +1,95 @@
//@ts-nocheck
import { useEffect, useState } from "react";
export default function ChangeColor({
open,
onColorChange,
}: {
open: boolean;
onColorChange: (color: string) => void;
}) {
const [active, setActive] = useState(false);
function onChangeColor(color) {
onColorChange(color);
}
if (open && !active) {
setActive(true);
}
if (!open && active) {
setActive(false);
}
useEffect(() => {
if (active) {
let color_picker = document.getElementById("changecolor");
color_picker.style.display = "block";
color_picker.style.width = null;
dragElement(color_picker);
function dragElement(elmnt) {
let pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
if (document.getElementById(elmnt.id + "_header")) {
// if present, the header is where you move the DIV from:
document.getElementById(elmnt.id + "_header").onmousedown =
dragMouseDown;
} else {
// otherwise, move the DIV from anywhere inside the DIV:
elmnt.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
elmnt.style.top = elmnt.offsetTop - pos2 + "px";
elmnt.style.left = elmnt.offsetLeft - pos1 + "px";
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
}
}
} else {
document.getElementById("changecolor").style.display = "none";
}
}, [active]);
return (
<div id="changecolor">
<div id="changecolor_header">
<input
type="color"
id="picked_color"
value="#00ACFF"
onChange={(e) => {
let color = e.target.value;
onChangeColor(color);
}}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,469 @@
// @ts-nocheck
import * as Plotly from "plotly.js-dist-min";
import { usePostHog } from "posthog-js/react";
import createPlotlyComponent from "react-plotly.js/factory";
import { useState, useEffect } from "react";
import { ICONS, DARK_CHARTS_TEMPLATE, LIGHT_CHARTS_TEMPLATE } from "./Config";
import OverlayChartDialog from "./Dialogs/OverlayChartDialog";
import TitleChartDialog from "./Dialogs/TitleChartDialog";
import TextChartDialog from "./Dialogs/TextChartDialog";
import { Icons as PlotlyIcons } from "plotly.js-dist-min";
import { init_annotation } from "../utils/addAnnotation";
import { PlotConfig, hideModebar } from "./PlotlyConfig";
import autoScaling from "./AutoScaling";
import { non_blocking, saveImage } from "../utils/utils";
import ResizeHandler from "./ResizeHandler";
import ChangeColor from "./ChangeColor";
const Plot = createPlotlyComponent(Plotly);
export default function Chart({
json,
date,
cmd,
title,
info,
}: {
// @ts-ignore
json: Figure;
date: Date;
cmd: string;
title: string;
info?: any;
}) {
const posthog = usePostHog();
if (info)
useEffect(() => {
if (posthog) posthog.capture("chart", info);
}, [posthog]);
delete json.layout.width;
delete json.layout.height;
json.layout.title.text = "";
const [barButtons, setModeBarButtons] = useState({});
const [LogYaxis, setLogYaxis] = useState(false);
const [chartTitle, setChartTitle] = useState(title);
const [axesTitles, setAxesTitles] = useState({});
const [plotLoaded, setPlotLoaded] = useState(false);
const [modal, setModal] = useState({ modal: "" });
const [loading, setLoading] = useState(false);
const [plotDiv, setPlotDiv] = useState(null);
const [Volume, setVolume] = useState({ old_nticks: {} });
const [plotData, setPlotData] = useState(json);
const [annotations, setAnnotations] = useState([]);
const [changeTheme, setChangeTheme] = useState(false);
const [darkMode, setDarkMode] = useState(true);
const [autoScale, setAutoScaling] = useState(false);
const [changeColor, setChangeColor] = useState(false);
const [colorActive, setColorActive] = useState(false);
const [onAnnotationClick, setOnAnnotationClick] = useState({});
const onClose = () => setModal({ modal: "" });
// @ts-ignore
function onDeleteAnnotation(annotation) {
console.log("onDeleteAnnotation", annotation);
const index = plotData?.layout?.annotations?.findIndex(
(a: any) => a.text === annotation.text
);
console.log("index", index);
if (index > -1) {
plotData?.layout?.annotations?.splice(index, 1);
setPlotData({ ...plotData });
setAnnotations(plotData?.layout?.annotations);
}
}
// @ts-ignore
function onAddAnnotation(data) {
init_annotation({
plotData,
popupData: data,
setPlotData,
setModal,
setOnAnnotationClick,
setAnnotations,
onAnnotationClick,
annotations,
plotDiv,
});
}
if (axesTitles && Object.keys(axesTitles).length > 0) {
Object.keys(axesTitles).forEach((k) => {
plotData.layout[k].title = {
...(plotData.layout[k].title || {}),
text: axesTitles[k],
};
plotData.layout[k].showticklabels = true;
});
setAxesTitles({});
}
function onChangeColor(color) {
// updates the color of the last added shape
// this function is called when the color picker is used
// if there are no shapes, we remove the color picker
let shapes = plotDiv.layout.shapes;
if (!shapes || shapes.length == 0) {
return;
}
// we change last added shape color
let last_shape = shapes[shapes.length - 1];
last_shape.line.color = color;
Plotly.update(plotDiv, {}, { shapes: shapes });
}
function button_pressed(title, active = false) {
// changes the style of the button when it is pressed
// title is the title of the button
// active is true if the button is active, false otherwise
let button =
barButtons[title] || document.querySelector(`[data-title="${title}"]`);
if (!active) {
button.style.border = "1px solid rgba(0, 151, 222, 1.0)";
button.style.borderRadius = "5px";
button.style.borderpadding = "5px";
button.style.boxShadow = "0 0 5px rgba(0, 151, 222, 1.0)";
} else {
button.style.border = "transparent";
button.style.boxShadow = "none";
}
setModeBarButtons({ ...barButtons, [title]: button });
}
function autoscaleButton() {
// We need to check if the button is active or not
let title = "Auto Scale (Ctrl+Shift+A)";
let button =
barButtons[title] || document.querySelector(`[data-title="${title}"]`);
let active = true;
if (button.style.border == "transparent") {
active = false;
plotDiv.on(
"plotly_relayout",
non_blocking(async function (eventdata) {
if (eventdata["xaxis.range[0]"] == undefined) return;
let to_update = await autoScaling(eventdata, plotDiv);
Plotly.update(plotDiv, {}, to_update);
}, 100)
);
}
// If the button isn't active, we remove the listener so
// the graphs don't autoscale anymore
else plotDiv.removeAllListeners("plotly_relayout");
button_pressed(title, active);
}
function changecolorButton() {
// We need to check if the button is active or not
let title = "Edit Color (Ctrl+E)";
let button =
barButtons[title] || document.querySelector(`[data-title="${title}"]`);
let active = true;
if (button.style.border == "transparent") {
active = false;
}
setColorActive(!active);
button_pressed(title, active);
}
useEffect(() => {
if (autoScale) {
let scale = !autoScale;
console.log("activateAutoScale", scale);
autoscaleButton();
setAutoScaling(false);
}
}, [autoScale]);
useEffect(() => {
if (changeColor) {
changecolorButton();
setChangeColor(false);
}
}, [changeColor]);
useEffect(() => {
if (changeTheme) {
try {
console.log("changeTheme", changeTheme);
const TRACES = plotData?.data.filter((trace) =>
trace?.name?.startsWith("Volume")
);
let darkmode = !darkMode;
window.document.body.style.backgroundColor = darkmode ? "#000" : "#fff";
plotData.layout.font = {
...(plotData.layout.font || {}),
color: darkmode ? "#fff" : "#000",
};
const changeIcon = darkmode ? ICONS.sunIcon : ICONS.moonIcon;
document
.querySelector('[data-title="Change Theme"]')
.getElementsByTagName("path")[0]
.setAttribute("d", changeIcon.path);
document
.querySelector('[data-title="Change Theme"]')
.getElementsByTagName("svg")[0]
.setAttribute("viewBox", changeIcon.viewBox);
const volumeColors = {
"#e4003a": "#c80000",
"#00ACFF": "#009600",
"#009600": "#00ACFF",
"#c80000": "#e4003a",
};
TRACES.forEach((trace) => {
if (trace.type == "bar")
trace.marker.color = trace.marker.color.map((color) => {
return volumeColors[color] || color;
});
});
plotData.layout.template = darkmode
? DARK_CHARTS_TEMPLATE
: LIGHT_CHARTS_TEMPLATE;
setPlotData({ ...plotData });
Plotly.react(plotDiv, plotData.data, plotData.layout);
setDarkMode(darkmode);
setChangeTheme(false);
} catch (e) {
console.log("error", e);
}
}
}, [changeTheme]);
useEffect(() => {
if (plotLoaded) {
setDarkMode(true);
setAutoScaling(false);
const captureButtons = [
"Download CSV",
"Download Chart as Image",
"Overlay chart from CSV",
"Add Text",
"Change Titles",
"Auto Scale (Ctrl+Shift+A)",
"Reset Axes",
];
const autoscale = document.querySelector('[data-title="Autoscale"]');
if (autoscale) {
autoscale
.getElementsByTagName("path")[0]
.setAttribute("d", PlotlyIcons.home.path);
autoscale.setAttribute("data-title", "Reset Axes");
}
window.MODEBAR = document.getElementsByClassName(
"modebar-container"
)[0] as HTMLElement;
const modeBarButtons = window.MODEBAR.getElementsByClassName(
"modebar-btn"
) as HTMLCollectionOf<HTMLElement>;
window.MODEBAR.style.cssText = `${window.MODEBAR.style.cssText}; display:flex;`;
if (modeBarButtons) {
let barbuttons: any = {};
for (let i = 0; i < modeBarButtons.length; i++) {
let btn = modeBarButtons[i];
if (captureButtons.includes(btn.getAttribute("data-title"))) {
btn.classList.add("ph-capture");
}
btn.style.border = "transparent";
barbuttons[btn.getAttribute("data-title")] = btn;
}
setModeBarButtons(barbuttons);
}
if (plotData.layout.yaxis.type != undefined) {
if (plotData.layout.yaxis.type == "log" && !LogYaxis) {
console.log("yaxis.type changed to log");
setLogYaxis(true);
// We update the yaxis exponent format to SI,
// set the tickformat to '.0s' and the exponentbase to 100
let layout_update = {
"yaxis.exponentformat": "SI",
"yaxis.tickformat": ".0s",
"yaxis.exponentbase": 100,
};
Plotly.update(plotDiv, layout_update);
}
if (plotData.layout.yaxis.type == "linear" && LogYaxis) {
console.log("yaxis.type changed to linear");
setLogYaxis(false);
// We update the yaxis exponent format to none,
// set the tickformat to null and the exponentbase to 10
let layout_update = {
"yaxis.exponentformat": "none",
"yaxis.tickformat": null,
"yaxis.exponentbase": 10,
};
Plotly.update(plotDiv, layout_update);
}
}
// We check to see if window.save_png is defined and true
if (window.save_image != undefined && window.export_image) {
// We get the extension of the file and check if it is valid
let filename = window.export_image.split("/").pop();
const extension = filename.split(".").pop().replace("jpg", "jpeg");
if (["jpeg", "png", "svg", "pdf"].includes(extension))
non_blocking(async function () {
await hideModebar();
await saveImage("MainChart", filename.split(".")[0], extension);
}, 2)();
}
window.addEventListener("resize", async function () {
let update = await ResizeHandler({ plotData, Volume });
let layout_update = update.layout_update;
let newPlotData = update.plotData;
let volume_update = update.volume_update;
if (Object.keys(layout_update).length > 0) {
setPlotData(newPlotData);
setVolume(volume_update);
Plotly.relayout(plotDiv, layout_update);
}
});
}
}, [plotLoaded]);
return (
<div className="relative h-full">
{loading && (
<div className="absolute inset-0 flex items-center justify-center z-[100]">
<svg
className="animate-spin h-20 w-20 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v8z"
></path>
</svg>
</div>
)}
<OverlayChartDialog
addOverlay={(overlay) => {
console.log(overlay);
plotData.layout.showlegend = true;
setPlotData(overlay);
setPlotLoaded(false);
}}
plotlyData={plotData}
setLoading={setLoading}
open={modal.modal === "overlayChart"}
close={onClose}
/>
<TitleChartDialog
updateTitle={(title) => setChartTitle(title)}
updateAxesTitles={(axesTitles) => setAxesTitles(axesTitles)}
defaultTitle={chartTitle}
plotlyData={plotData}
open={modal.modal === "titleDialog"}
close={onClose}
/>
<TextChartDialog
popupData={modal.modal === "textDialog" ? modal?.data : null}
open={modal.modal === "textDialog"}
close={onClose}
addAnnotation={(data) => onAddAnnotation(data)}
deleteAnnotation={(data) => onDeleteAnnotation(data)}
/>
<ChangeColor open={colorActive} onColorChange={onChangeColor} />
<div className="relative h-full" id="MainChart">
<div className="_header relative gap-4 py-2 text-center text-xs flex items-center justify-between px-4 text-white">
<div className="w-1/3">
<svg
xmlns="http://www.w3.org/2000/svg"
width="64"
height="40"
fill="none"
viewBox="0 0 64 40"
>
<path
fill="#fff"
d="M61.283 3.965H33.608v27.757h25.699V19.826H37.561v-3.965H63.26V3.965h-1.977zM39.538 23.792h15.815v3.965H37.561v-3.965h1.977zM59.306 9.913v1.983H37.561V7.931h21.745v1.982zM33.606 0h-3.954v3.965H33.606V0zM25.7 3.966H0V15.86h25.7v3.965H3.953v11.896h25.7V3.966h-3.955zm0 21.808v1.983H7.907v-3.965h17.791v1.982zm0-15.86v1.982H3.953V7.931h21.745v1.982zM37.039 35.693v2.952l-.246-.246-.245-.245-.245-.247-.245-.246-.246-.246-.245-.245-.245-.247-.247-.246-.245-.246-.245-.246-.245-.246-.246-.246h-.49v3.936h.49v-3.198l.246.246.245.246.245.246.245.246.246.246.246.246.245.247.246.245.245.246.245.247.245.246.246.245.245.246h.245v-3.936h-.49zM44.938 37.17h-.491v-1.477h-2.944v3.937h3.93v-2.46h-.495zm-2.944-.246v-.739h1.962v.984h-1.962v-.245zm2.944.984v1.23h-2.944V37.66h2.944v.247zM52.835 37.17h-.49v-1.477h-2.946v3.937h3.925v-2.46h-.489zm-2.944-.246v-.739h1.963v.984h-1.965l.002-.245zm2.944.984v1.23H49.89V37.66h2.946v.247zM29.174 35.693H25.739v3.936H29.663v-.491H26.229v-.984h2.943v-.493H26.229v-1.476h3.434v-.492h-.489zM13.37 35.693H9.934v3.937h3.925v-3.937h-.49zm0 .738v2.709h-2.945v-2.955h2.943l.001.246zM21.276 35.693h-3.435v3.937h.491v-1.476h3.434v-2.461h-.49zm0 .738v1.23h-2.944v-1.476h2.944v.246z"
></path>
</svg>
</div>
<p className="font-bold w-1/3 flex flex-col gap-0.5 items-center">
{chartTitle}
{/* {source && (
<span className="font-normal text-[10px]">{`[${source}]`}</span>
)} */}
</p>
<p className="w-1/3 text-right text-xs">
{new Intl.DateTimeFormat("en-GB", {
dateStyle: "full",
timeStyle: "long",
})
.format(date)
.replace(/:\d\d /, " ")}
<br />
<span className="text-grey-400">{cmd}</span>
</p>
{/* {source && typeof source === "string" && source.includes("*") && (
<p className="text-[8px] absolute bottom-0 right-4">
*not affiliated
</p>
)} */}
</div>
<div className="w-full h-[calc(100%-50px)] sm:pb-12">
<Plot
onInitialized={() => {
if (!plotDiv) {
const plot = document.getElementById("plotlyChart");
console.log("plot", plot);
if (plot) setPlotDiv(plot);
}
if (!plotLoaded) setPlotLoaded(true);
}}
className="w-full h-full"
divId="plotlyChart"
data={plotData.data}
layout={plotData.layout}
config={PlotConfig({
setModal: setModal,
changeTheme: setChangeTheme,
autoScaling: setAutoScaling,
Loading: setLoading,
changeColor: setChangeColor,
})}
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,756 @@
export const ICONS = {
sunIcon: {
viewBox: "0 0 16 16",
width: 16,
height: 16,
path: "M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z",
},
moonIcon: {
viewBox: "0 0 25 25",
width: 25,
height: 25,
path: "M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z",
},
plotCsv: {
width: 900,
height: 900,
path: "M170.666667 106.666667l0.192 736H906.666667v64H149.546667c-23.552 0-42.666667-19.093333-42.666667-42.666667L106.666667 106.666667h64z m686.506666 454.144l13.653334 16.362666a21.333333 21.333333 0 0 1-2.666667 30.058667l-171.157333 143.146667a21.333333 21.333333 0 0 1-21.546667 3.477333l-229.973333-91.285333-113.834667 94.997333a21.333333 21.333333 0 0 1-30.037333-2.709333l-13.653334-16.362667a21.333333 21.333333 0 0 1 2.688-30.058667l133.312-111.274666a21.333333 21.333333 0 0 1 21.546667-3.456l229.930667 91.264 151.68-126.826667a21.333333 21.333333 0 0 1 30.037333 2.666667z m-1.621333-417.962667l16.896 13.013333a21.333333 21.333333 0 0 1 3.925333 29.888L685.802667 433.706667a21.333333 21.333333 0 0 1-20.202667 8.085333l-226.794667-35.413333-150.186666 222.357333a21.333333 21.333333 0 0 1-27.477334 7.018667l-2.133333-1.28-17.685333-11.946667a21.333333 21.333333 0 0 1-5.738667-29.610667l165.354667-244.821333a21.333333 21.333333 0 0 1 20.992-9.130667L650.453333 374.613333l175.146667-227.882666a21.333333 21.333333 0 0 1 29.930667-3.904z",
},
addText: {
path: "M896 928H128a32 32 0 0 1-32-32V128a32 32 0 0 1 32-32h768a32 32 0 0 1 32 32v768a32 32 0 0 1-32 32z m-736-64h704v-704h-704z M704 352H320a32 32 0 0 1 0-64h384a32 32 0 0 1 0 64z M512 736a32 32 0 0 1-32-32V320a32 32 0 0 1 64 0v384a32 32 0 0 1-32 32z",
width: 950,
height: 950,
},
changeTitle: {
path: "M122.368 165.888h778.24c-9.216 0-16.384-7.168-16.384-16.384v713.728c0-9.216 7.168-16.384 16.384-16.384h-778.24c9.216 0 16.384 7.168 16.384 16.384V150.016c0 8.192-6.656 15.872-16.384 15.872z m-32.768 684.544c0 26.112 20.992 47.104 47.104 47.104h750.08c26.112 0 47.104-20.992 47.104-47.104V162.304c0-26.112-20.992-47.104-47.104-47.104H136.704c-26.112 0-47.104 20.992-47.104 47.104v688.128z M244.736 656.896h534.016v62.464H244.736z M373.76 358.4H307.2v219.136h-45.568V358.4H192v-41.472H373.76V358.4zM403.968 316.928h44.032v50.176h-44.032v-50.176z m0 67.072h44.032v194.048h-44.032V384zM576.512 541.184l8.704 31.744c-13.312 5.12-26.624 8.192-38.912 8.704-32.768 1.024-48.64-15.36-48.128-48.128V422.912h-26.624V384h26.624v-46.592l44.032-21.504V384h36.352v38.912h-36.352V532.48c-1.024 10.24 3.072 14.848 11.264 13.824 5.12 0 12.8-1.536 23.04-5.12zM619.008 316.928h44.032v260.608h-44.032V316.928zM813.056 509.952l41.472 12.8c-11.776 40.96-37.888 61.44-78.336 60.416-52.736-1.536-80.384-34.304-81.92-98.304 2.56-67.072 29.696-102.4 81.92-105.984 52.224 1.536 78.336 36.864 79.36 105.984v13.824h-117.248c3.584 30.208 15.872 45.568 37.888 46.592 19.968 0.512 32.256-11.264 36.864-35.328z m-72.704-51.712h70.656c-1.024-25.088-12.288-38.4-33.792-38.912-21.504 0.512-33.792 13.824-36.864 38.912z",
width: 920,
height: 900,
},
changeColor: {
path: "M8 3C5.79 3 4 4.79 4 7V14C4 15.1 4.9 16 6 16H9V20C9 21.1 9.9 22 11 22H13C14.1 22 15 21.1 15 20V16H18C19.1 16 20 15.1 20 14V3H8M8 5H12V7H14V5H15V9H17V5H18V10H6V7C6 5.9 6.9 5 8 5M6 14V12H18V14H6Z",
width: 22,
height: 22,
},
uploadImage: {
path: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5",
width: 1024,
height: 1024,
},
downloadCsv: {
path: `M486.2,196.121h-13.164V132.59c0-0.399-0.064-0.795-0.116-1.2c-0.021-2.52-0.824-5-2.551-6.96L364.656,3.677
c-0.031-0.034-0.064-0.044-0.085-0.075c-0.629-0.707-1.364-1.292-2.141-1.796c-0.231-0.157-0.462-0.286-0.704-0.419
c-0.672-0.365-1.386-0.672-2.121-0.893c-0.199-0.052-0.377-0.134-0.576-0.188C358.229,0.118,357.4,0,356.562,0H96.757
C84.893,0,75.256,9.649,75.256,21.502v174.613H62.093c-16.972,0-30.733,13.756-30.733,30.73v159.81
c0,16.966,13.761,30.736,30.733,30.736h13.163V526.79c0,11.854,9.637,21.501,21.501,21.501h354.777
c11.853,0,21.502-9.647,21.502-21.501V417.392H486.2c16.966,0,30.729-13.764,30.729-30.731v-159.81
C516.93,209.872,503.166,196.121,486.2,196.121z M96.757,21.502h249.053v110.006c0,5.94,4.818,10.751,10.751,10.751h94.973v53.861
H96.757V21.502z M258.618,313.18c-26.68-9.291-44.063-24.053-44.063-47.389c0-27.404,22.861-48.368,60.733-48.368
c18.107,0,31.447,3.811,40.968,8.107l-8.09,29.3c-6.43-3.107-17.862-7.632-33.59-7.632c-15.717,0-23.339,7.149-23.339,15.485
c0,10.247,9.047,14.769,29.78,22.632c28.341,10.479,41.681,25.239,41.681,47.874c0,26.909-20.721,49.786-64.792,49.786
c-18.338,0-36.449-4.776-45.497-9.77l7.38-30.016c9.772,5.014,24.775,10.006,40.264,10.006c16.671,0,25.488-6.908,25.488-17.396
C285.536,325.789,277.909,320.078,258.618,313.18z M69.474,302.692c0-54.781,39.074-85.269,87.654-85.269
c18.822,0,33.113,3.811,39.549,7.149l-7.392,28.816c-7.38-3.084-17.632-5.939-30.491-5.939c-28.822,0-51.206,17.375-51.206,53.099
c0,32.158,19.051,52.4,51.456,52.4c10.947,0,23.097-2.378,30.241-5.238l5.483,28.346c-6.672,3.34-21.674,6.919-41.208,6.919
C98.06,382.976,69.474,348.424,69.474,302.692z M451.534,520.962H96.757v-103.57h354.777V520.962z M427.518,380.583h-42.399
l-51.45-160.536h39.787l19.526,67.894c5.479,19.046,10.479,37.386,14.299,57.397h0.709c4.048-19.298,9.045-38.352,14.526-56.693
l20.487-68.598h38.599L427.518,380.583z`,
width: 550,
height: 550,
transform: "translate(4, 0)",
},
downloadImage: {
path: "M22.71,6.29a1,1,0,0,0-1.42,0L20,7.59V2a1,1,0,0,0-2,0V7.59l-1.29-1.3a1,1,0,0,0-1.42,1.42l3,3a1,1,0,0,0,.33.21.94.94,0,0,0,.76,0,1,1,0,0,0,.33-.21l3-3A1,1,0,0,0,22.71,6.29ZM19,13a1,1,0,0,0-1,1v.38L16.52,12.9a2.79,2.79,0,0,0-3.93,0l-.7.7L9.41,11.12a2.85,2.85,0,0,0-3.93,0L4,12.6V7A1,1,0,0,1,5,6h8a1,1,0,0,0,0-2H5A3,3,0,0,0,2,7V19a3,3,0,0,0,3,3H17a3,3,0,0,0,3-3V14A1,1,0,0,0,19,13ZM5,20a1,1,0,0,1-1-1V15.43l2.9-2.9a.79.79,0,0,1,1.09,0l3.17,3.17,0,0L15.46,20Zm13-1a.89.89,0,0,1-.18.53L13.31,15l.7-.7a.77.77,0,0,1,1.1,0L18,17.21Z",
width: 21,
height: 21,
transform: "translate(-2, -2)",
},
};
export const DARK_CHARTS_TEMPLATE = {
data: {
candlestick: [
{
decreasing: {
fillcolor: "#e4003a",
line: {
color: "#e4003a",
},
},
increasing: {
fillcolor: "#00ACFF",
line: {
color: "#00ACFF",
},
},
type: "candlestick",
},
],
},
layout: {
annotationdefaults: {
showarrow: false,
},
autotypenumbers: "strict",
colorway: [
"#ffed00",
"#ef7d00",
"#e4003a",
"#c13246",
"#822661",
"#48277c",
"#005ca9",
"#00aaff",
"#9b30d9",
"#af005f",
"#5f00af",
"#af87ff",
],
dragmode: "pan",
font: {
family: "Fira Code",
size: 18,
},
hoverlabel: {
align: "left",
},
mapbox: {
style: "dark",
},
hovermode: "x",
legend: {
bgcolor: "rgba(0, 0, 0, 0)",
x: 0.01,
xanchor: "left",
y: 0.99,
yanchor: "top",
font: {
size: 15,
},
},
paper_bgcolor: "#000000",
plot_bgcolor: "#000000",
xaxis: {
automargin: true,
autorange: true,
rangeslider: {
visible: false,
},
showgrid: true,
showline: true,
tickfont: {
size: 14,
},
zeroline: false,
tick0: 1,
title: {
standoff: 20,
},
linecolor: "#F5EFF3",
mirror: true,
ticks: "outside",
},
yaxis: {
anchor: "x",
automargin: true,
fixedrange: false,
zeroline: false,
showgrid: true,
showline: true,
side: "right",
tick0: 0.5,
title: {
standoff: 20,
},
gridcolor: "#283442",
linecolor: "#F5EFF3",
mirror: true,
ticks: "outside",
},
},
};
export const LIGHT_CHARTS_TEMPLATE = {
data: {
barpolar: [
{
marker: {
line: {
color: "white",
width: 0.5,
},
pattern: {
fillmode: "overlay",
size: 10,
solidity: 0.2,
},
},
type: "barpolar",
},
],
bar: [
{
error_x: {
color: "#2a3f5f",
},
error_y: {
color: "#2a3f5f",
},
marker: {
line: {
color: "white",
width: 0.5,
},
pattern: {
fillmode: "overlay",
size: 10,
solidity: 0.2,
},
},
type: "bar",
},
],
carpet: [
{
aaxis: {
endlinecolor: "#2a3f5f",
gridcolor: "#C8D4E3",
linecolor: "#C8D4E3",
minorgridcolor: "#C8D4E3",
startlinecolor: "#2a3f5f",
},
baxis: {
endlinecolor: "#2a3f5f",
gridcolor: "#C8D4E3",
linecolor: "#C8D4E3",
minorgridcolor: "#C8D4E3",
startlinecolor: "#2a3f5f",
},
type: "carpet",
},
],
choropleth: [
{
colorbar: {
outlinewidth: 0,
ticks: "",
},
type: "choropleth",
},
],
contourcarpet: [
{
colorbar: {
outlinewidth: 0,
ticks: "",
},
type: "contourcarpet",
},
],
contour: [
{
colorbar: {
outlinewidth: 0,
ticks: "",
},
colorscale: [
[0.0, "#0d0887"],
[0.1111111111111111, "#46039f"],
[0.2222222222222222, "#7201a8"],
[0.3333333333333333, "#9c179e"],
[0.4444444444444444, "#bd3786"],
[0.5555555555555556, "#d8576b"],
[0.6666666666666666, "#ed7953"],
[0.7777777777777778, "#fb9f3a"],
[0.8888888888888888, "#fdca26"],
[1.0, "#f0f921"],
],
type: "contour",
},
],
heatmapgl: [
{
colorbar: {
outlinewidth: 0,
ticks: "",
},
colorscale: [
[0.0, "#0d0887"],
[0.1111111111111111, "#46039f"],
[0.2222222222222222, "#7201a8"],
[0.3333333333333333, "#9c179e"],
[0.4444444444444444, "#bd3786"],
[0.5555555555555556, "#d8576b"],
[0.6666666666666666, "#ed7953"],
[0.7777777777777778, "#fb9f3a"],
[0.8888888888888888, "#fdca26"],
[1.0, "#f0f921"],
],
type: "heatmapgl",
},
],
heatmap: [
{
colorbar: {
outlinewidth: 0,
ticks: "",
},
colorscale: [
[0.0, "#0d0887"],
[0.1111111111111111, "#46039f"],
[0.2222222222222222, "#7201a8"],
[0.3333333333333333, "#9c179e"],
[0.4444444444444444, "#bd3786"],
[0.5555555555555556, "#d8576b"],
[0.6666666666666666, "#ed7953"],
[0.7777777777777778, "#fb9f3a"],
[0.8888888888888888, "#fdca26"],
[1.0, "#f0f921"],
],
type: "heatmap",
},
],
histogram2dcontour: [
{
colorbar: {
outlinewidth: 0,
ticks: "",
},
colorscale: [
[0.0, "#0d0887"],
[0.1111111111111111, "#46039f"],
[0.2222222222222222, "#7201a8"],
[0.3333333333333333, "#9c179e"],
[0.4444444444444444, "#bd3786"],
[0.5555555555555556, "#d8576b"],
[0.6666666666666666, "#ed7953"],
[0.7777777777777778, "#fb9f3a"],
[0.8888888888888888, "#fdca26"],
[1.0, "#f0f921"],
],
type: "histogram2dcontour",
},
],
histogram2d: [
{
colorbar: {
outlinewidth: 0,
ticks: "",
},
colorscale: [
[0.0, "#0d0887"],
[0.1111111111111111, "#46039f"],
[0.2222222222222222, "#7201a8"],
[0.3333333333333333, "#9c179e"],
[0.4444444444444444, "#bd3786"],
[0.5555555555555556, "#d8576b"],
[0.6666666666666666, "#ed7953"],
[0.7777777777777778, "#fb9f3a"],
[0.8888888888888888, "#fdca26"],
[1.0, "#f0f921"],
],
type: "histogram2d",
},
],
histogram: [
{
marker: {
pattern: {
fillmode: "overlay",
size: 10,
solidity: 0.2,
},
},
type: "histogram",
},
],
mesh3d: [
{
colorbar: {
outlinewidth: 0,
ticks: "",
},
type: "mesh3d",
},
],
parcoords: [
{
line: {
colorbar: {
outlinewidth: 0,
ticks: "",
},
},
type: "parcoords",
},
],
pie: [
{
automargin: true,
type: "pie",
},
],
scatter3d: [
{
line: {
colorbar: {
outlinewidth: 0,
ticks: "",
},
},
marker: {
colorbar: {
outlinewidth: 0,
ticks: "",
},
},
type: "scatter3d",
},
],
scattercarpet: [
{
marker: {
colorbar: {
outlinewidth: 0,
ticks: "",
},
},
type: "scattercarpet",
},
],
scattergeo: [
{
marker: {
colorbar: {
outlinewidth: 0,
ticks: "",
},
},
type: "scattergeo",
},
],
scattergl: [
{
marker: {
colorbar: {
outlinewidth: 0,
ticks: "",
},
},
type: "scattergl",
},
],
scattermapbox: [
{
marker: {
colorbar: {
outlinewidth: 0,
ticks: "",
},
},
type: "scattermapbox",
},
],
scatterpolargl: [
{
marker: {
colorbar: {
outlinewidth: 0,
ticks: "",
},
},
type: "scatterpolargl",
},
],
scatterpolar: [
{
marker: {
colorbar: {
outlinewidth: 0,
ticks: "",
},
},
type: "scatterpolar",
},
],
scatter: [
{
fillpattern: {
fillmode: "overlay",
size: 10,
solidity: 0.2,
},
type: "scatter",
},
],
scatterternary: [
{
marker: {
colorbar: {
outlinewidth: 0,
ticks: "",
},
},
type: "scatterternary",
},
],
surface: [
{
colorbar: {
outlinewidth: 0,
ticks: "",
},
colorscale: [
[0.0, "#0d0887"],
[0.1111111111111111, "#46039f"],
[0.2222222222222222, "#7201a8"],
[0.3333333333333333, "#9c179e"],
[0.4444444444444444, "#bd3786"],
[0.5555555555555556, "#d8576b"],
[0.6666666666666666, "#ed7953"],
[0.7777777777777778, "#fb9f3a"],
[0.8888888888888888, "#fdca26"],
[1.0, "#f0f921"],
],
type: "surface",
},
],
table: [
{
cells: {
fill: {
color: "#EBF0F8",
},
line: {
color: "white",
},
},
header: {
fill: {
color: "#C8D4E3",
},
line: {
color: "white",
},
},
type: "table",
},
],
candlestick: [
{
decreasing: {
fillcolor: "#c80000",
line: {
color: "#990000",
},
},
increasing: {
fillcolor: "#009600",
line: {
color: "#007500",
},
},
type: "candlestick",
},
],
},
layout: {
annotationdefaults: {
arrowcolor: "#2a3f5f",
arrowhead: 0,
arrowwidth: 1,
showarrow: false,
},
autotypenumbers: "strict",
coloraxis: {
colorbar: {
outlinewidth: 0,
ticks: "",
},
},
colorscale: {
diverging: [
[0, "#8e0152"],
[0.1, "#c51b7d"],
[0.2, "#de77ae"],
[0.3, "#f1b6da"],
[0.4, "#fde0ef"],
[0.5, "#f7f7f7"],
[0.6, "#e6f5d0"],
[0.7, "#b8e186"],
[0.8, "#7fbc41"],
[0.9, "#4d9221"],
[1, "#276419"],
],
sequential: [
[0.0, "#0d0887"],
[0.1111111111111111, "#46039f"],
[0.2222222222222222, "#7201a8"],
[0.3333333333333333, "#9c179e"],
[0.4444444444444444, "#bd3786"],
[0.5555555555555556, "#d8576b"],
[0.6666666666666666, "#ed7953"],
[0.7777777777777778, "#fb9f3a"],
[0.8888888888888888, "#fdca26"],
[1.0, "#f0f921"],
],
sequentialminus: [
[0.0, "#0d0887"],
[0.1111111111111111, "#46039f"],
[0.2222222222222222, "#7201a8"],
[0.3333333333333333, "#9c179e"],
[0.4444444444444444, "#bd3786"],
[0.5555555555555556, "#d8576b"],
[0.6666666666666666, "#ed7953"],
[0.7777777777777778, "#fb9f3a"],
[0.8888888888888888, "#fdca26"],
[1.0, "#f0f921"],
],
},
colorway: [
"#254495",
"#c13246",
"#48277c",
"#e4003a",
"#ef7d00",
"#822661",
"#ffed00",
"#00aaff",
"#9b30d9",
"#af005f",
"#5f00af",
"#af87ff",
],
font: {
color: "#2a3f5f",
},
geo: {
bgcolor: "white",
lakecolor: "white",
landcolor: "white",
showlakes: true,
showland: true,
subunitcolor: "#C8D4E3",
},
hoverlabel: {
align: "left",
},
hovermode: "x",
mapbox: {
style: "light",
},
paper_bgcolor: "white",
plot_bgcolor: "white",
polar: {
angularaxis: {
gridcolor: "#EBF0F8",
linecolor: "#EBF0F8",
ticks: "",
},
bgcolor: "white",
radialaxis: {
gridcolor: "#EBF0F8",
linecolor: "#EBF0F8",
ticks: "",
},
},
scene: {
xaxis: {
backgroundcolor: "white",
gridcolor: "#DFE8F3",
gridwidth: 2,
linecolor: "#EBF0F8",
showbackground: true,
ticks: "",
zerolinecolor: "#EBF0F8",
},
yaxis: {
backgroundcolor: "white",
gridcolor: "#DFE8F3",
gridwidth: 2,
linecolor: "#EBF0F8",
showbackground: true,
ticks: "",
zerolinecolor: "#EBF0F8",
},
zaxis: {
backgroundcolor: "white",
gridcolor: "#DFE8F3",
gridwidth: 2,
linecolor: "#EBF0F8",
showbackground: true,
ticks: "",
zerolinecolor: "#EBF0F8",
},
},
shapedefaults: {
line: {
color: "#2a3f5f",
},
},
ternary: {
aaxis: {
gridcolor: "#DFE8F3",
linecolor: "#A2B1C6",
ticks: "",
},
baxis: {
gridcolor: "#DFE8F3",
linecolor: "#A2B1C6",
ticks: "",
},
bgcolor: "white",
caxis: {
gridcolor: "#DFE8F3",
linecolor: "#A2B1C6",
ticks: "",
},
},
title: {
x: 0.05,
},
xaxis: {
automargin: true,
ticks: "outside",
zerolinewidth: 2,
rangeslider: {
visible: false,
},
showgrid: true,
showline: true,
tickfont: {
size: 15,
},
title: {
standoff: 20,
},
mirror: true,
zeroline: false,
},
yaxis: {
automargin: true,
ticks: "outside",
tickfont: {
size: 15,
},
zerolinewidth: 2,
fixedrange: false,
title: {
standoff: 20,
},
nticks: 8,
showgrid: true,
showline: true,
side: "right",
mirror: true,
zeroline: false,
},
dragmode: "pan",
legend: {
bgcolor: "rgba(0, 0, 0, 0)",
x: 1.03,
xanchor: "left",
y: 0.99,
yanchor: "top",
},
},
};

View File

@@ -0,0 +1,47 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
import CloseIcon from "../Icons/Close";
import { ReactNode } from "react";
export const styleDialog = {
margin: "2px 0px 2px 10px",
padding: "5px 2px 2px 5px",
};
export default function CommonDialog({
open,
close,
title,
description,
children,
}: {
open: boolean;
close: () => void;
title: string;
description: string;
children: ReactNode;
}) {
return (
<DialogPrimitive.Root open={open} onOpenChange={close}>
<div id="loading" className="saving">
<div id="loading_text" className="loading_text"></div>
<div id="loader" className="loader"></div>
</div>
<DialogPrimitive.Overlay onClick={close} className="_modal-overlay" />
<DialogPrimitive.Content className="_modal">
<DialogPrimitive.Title className="_modal-title">
{title}
</DialogPrimitive.Title>
<DialogPrimitive.Description className="_modal_description">
{description}
</DialogPrimitive.Description>
<DialogPrimitive.Close>
<CloseIcon />
</DialogPrimitive.Close>
{children}
<DialogPrimitive.Close className="_modal-close" onClick={close}>
<CloseIcon className="w-6 h-6" />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPrimitive.Root>
);
}

View File

@@ -0,0 +1,617 @@
import CommonDialog, { styleDialog } from "../Dialogs/CommonDialog";
import { useState } from "react";
const reader = new FileReader();
const layout_defaults = {
overlaying: "y",
side: "left",
tickfont: { size: 12 },
tickpadding: 5,
showgrid: false,
showline: false,
showticklabels: true,
showlegend: true,
zeroline: false,
anchor: "x",
type: "linear",
autorange: true,
};
export default function OverlayChartDialog({
open,
close,
setLoading,
addOverlay,
plotlyData,
}: {
open: boolean;
close: () => void;
setLoading: (loading: boolean) => void;
addOverlay: (data: any) => void;
plotlyData: any;
}) {
const [traceType, setTraceType] = useState("scatter");
const [traceColor, setTraceColor] = useState("#FFDD00");
const [increasingColor, setIncreasingColor] = useState("#00ACFF");
const [decreasingColor, setDecreasingColor] = useState("#FF0000");
const [traceName, setTraceName] = useState("");
const [csvData, setCsvData] = useState<any[]>([]);
const [csvColumns, setCsvColumns] = useState<string[]>([]);
const [selectedColumns, setSelectedColumns] = useState<any>({});
const [yaxisOptions, setYaxisOptions] = useState<any>({});
const optionIds = ["x", "open", "high", "low", "close"];
let traceTypes: any = {
scatter: "Scatter (Line)",
candlestick: "Candlestick",
bar: "Bar",
};
const [options, setOptions] = useState<any>({});
function onClose() {
close();
setTraceType("scatter");
setTraceName("");
setCsvData([]);
setCsvColumns([]);
setSelectedColumns({});
setOptions({});
}
function onSubmit() {
if (csvData.length == 0) {
document.getElementById("csv_file")?.focus();
document.getElementById("csv_file")?.style.setProperty("border", "1px solid red");
document.getElementById("csv_file_warning")!.style.display = "block";
return;
}
const newPlotydata = CSVonSubmit({
csvData: csvData,
plotlyData: plotlyData,
yaxisOptions: yaxisOptions,
traceType: traceType,
traceColor: traceColor,
traceName: traceName,
options: options,
increasingColor: increasingColor,
decreasingColor: decreasingColor,
});
addOverlay(newPlotydata);
onClose();
}
return (
<CommonDialog
title="Overlay Chart"
description="Upload a CSV file to overlay a chart on the main chart."
open={open}
close={close}
>
<div id="popup_csv" className="popup_content">
<div>
<label htmlFor="csv_file">
<b>CSV file:</b>
<div id="csv_file_warning" className="popup_warning" style={{ marginLeft: "80px", marginBottom: "10px" }}>
CSV file is required.
</div>
</label>
<input
onChange={(e) => {
if (!e.target.files) {
return;
} else if (e.target.files[0].type !== "text/csv") {
document.getElementById("csv_file")?.focus();
document.getElementById("csv_file")?.style.setProperty("border", "1px solid red");
document.getElementById("csv_file_warning")!.style.display = "block";
return;
}
if (csvColumns.length > 0) {
setCsvColumns([]);
setSelectedColumns({});
setOptions({});
setTraceType("scatter");
}
reader.onload = (filebytes) => {
if (
!filebytes.target?.result ||
typeof filebytes.target.result !== "string"
) {
return;
}
let lines = filebytes.target.result
.split("\n")
.map((x) => x.replace(/\r/g, ""));
const headers = lines[0].split(",");
const headers_lower = headers.map((x) =>
x.trim().toLowerCase()
);
let updateOptions: any = {};
for (let i = 0; i < optionIds.length; i++) {
if (headers_lower.includes(optionIds[i])) {
updateOptions[optionIds[i]] =
headers[headers_lower.indexOf(optionIds[i])];
} else if (
optionIds[i] == "x" &&
headers_lower.includes("date")
) {
updateOptions[optionIds[i]] =
headers[headers_lower.indexOf("date")];
}
}
const candle_cols = ["open", "high", "low", "close"];
const candle_cols_present = candle_cols.every((x) =>
headers_lower.includes(x)
);
if (candle_cols_present) {
setTraceType("candlestick");
}
if (headers_lower.includes("close")) {
setOptions({
...options,
y: headers[headers_lower.indexOf("close")],
});
updateOptions.y = headers[headers_lower.indexOf("close")];
}
let data = [];
for (let i = 1; i < lines.length; i++) {
let obj = {};
let currentline = lines[i].split(",");
for (let j = 0; j < headers.length; j++) {
//@ts-ignore
obj[headers[j]] = currentline[j];
}
data.push(obj);
}
//@ts-ignore
let filename = e.target.files[0].name.split(".")[0];
try {
if (filename.includes("_")) {
let name_parts = filename.split("_");
let date_regex = new RegExp("^[0-9]{8}$");
if (name_parts.length > 2) {
// we check if the first 2 parts are date and time
if (date_regex.test(name_parts[0])) {
name_parts.splice(0, 2);
}
// we check if the last 2 parts are date and time
else if (
date_regex.test(name_parts[name_parts.length - 2])
) {
name_parts.splice(name_parts.length - 2, 2);
}
filename = name_parts.join("_").replace(/openbb_/g, "");
}
}
} catch (e) {
console.log(e);
}
setTraceName(filename);
setOptions(updateOptions);
setCsvColumns(headers);
setCsvData(data);
};
reader.readAsText(e.target.files[0]);
}}
type="file"
id="csv_file"
accept=".csv"
style={{ marginLeft: 10 }}
/>
</div>
<div style={{ marginTop: 15 }}>
<label htmlFor="csv_trace_type">
<b>Display data type:</b>
</label>
<select
onChange={(e) => {
setTraceType(e.target.value);
}}
id="csv_trace_type"
style={styleDialog}
defaultValue={traceTypes[traceType]}
>
{traceType && (
<option key={traceType} value={traceType}>
{traceTypes[traceType]}
</option>
)}
{Object.keys(traceTypes).map(
(x) =>
traceType !== x && (
<option key={x} value={x}>
{traceTypes[x]}
</option>
)
)}
</select>
</div>
<div style={{ marginTop: 12 }}>
<label htmlFor="csv_name">
<b>Trace Name:</b>
</label>
<textarea
id="csv_name"
value={traceName}
onChange={(e) => {
setTraceName(e.target.value);
}}
style={{
padding: "5px 2px 2px 5px",
width: "100%",
maxWidth: "100%",
maxHeight: 200,
marginTop: 2,
}}
rows={2}
cols={20}
placeholder="Enter a name to give this trace"
/>
</div>
{csvColumns.length > 0 && (
<>
{["scatter", "bar"].includes(traceType) && (
<div
style={{ marginTop: 15, marginBottom: 10 }}
id="csv_columns"
className="csv_column_container"
>
{["x", "y"].map((x) => (
<div
key={x}
style={{
marginTop: 10,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<label htmlFor={`csv_${x}`} style={{ width: "100px" }}>
{x.toUpperCase()} Axis
</label>
<select
onChange={(e) => {
setSelectedColumns({
...selectedColumns,
[x]: e.target.value,
});
setOptions({
...options,
[x]: e.target.value,
});
}}
id={`csv_${x}`}
style={{ width: "100%" }}
defaultValue={options[x]}
>
{options[x] && (
<option key={x} value={options[x]}>
{options[x]}
</option>
)}
{csvColumns.map((x) => (
<option key={x} value={x}>
{x}
</option>
))}
</select>
</div>
))}
</div>
)}
{traceType === "candlestick" && (
<div
id="csv_columns"
className="csv_column_container"
style={{ marginTop: 15 }}
>
{["x", "open", "high", "low", "close"].map((x) => (
<div
key={x}
style={{
marginTop: 10,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<label htmlFor={`csv_${x}`} style={{ width: "100px" }}>
{x.charAt(0).toUpperCase() + x.slice(1)}
</label>
<select
onChange={(e) => {
setSelectedColumns({
...selectedColumns,
[x]: e.target.value,
});
setOptions({
...options,
[x]: e.target.value,
});
}}
id={`csv_${x}`}
style={{ width: "100%" }}
>
{options[x] && (
<option key={x} value={options[x]}>
{options[x]}
</option>
)}
{csvColumns.map(
(x) =>
x != options[x] && (
<option key={x} value={x}>
{x}
</option>
)
)}
</select>
</div>
))}
</div>
)}
<div style={{ marginTop: 20 }} id="csv_colors">
{["scatter", "bar"].includes(traceType) && (
<div>
<label htmlFor="csv_color">{`${traceType
.charAt(0)
.toUpperCase()}${traceType.slice(1)} color`}</label>
<input
type="color"
id="csv_color"
defaultValue="#FFDD00"
style={{ margin: "2px 2px 2px 10px" }}
onChange={(e) => {
console.log(e.target.value);
setTraceColor(e.target.value);
}}
/>
</div>
)}
{traceType === "candlestick" && (
<>
<label htmlFor="csv_increasing">Increasing color</label>
<input
type="color"
id="csv_increasing"
defaultValue="#00ACFF"
style={{ margin: "2px 0px 2px 10px" }}
onChange={(e) => {
setIncreasingColor(e.target.value);
}}
/>
<label htmlFor="csv_decreasing" style={{ marginLeft: 15 }}>
Decreasing color
</label>
<input
style={{ margin: "2px 0px 2px 10px" }}
type="color"
id="csv_decreasing"
defaultValue="#FF0000"
onChange={(e) => {
setDecreasingColor(e.target.value);
}}
/>
</>
)}
</div>
<div style={{ marginTop: 20 }} id="csv_plot_yaxis_options">
{traceType !== "candlestick" && (
<>
<input
type="checkbox"
id="csv_percent_change"
name="csv_plot_yaxis_check"
style={{ marginBottom: 2 }}
onChange={(e) => {
setYaxisOptions({
...yaxisOptions,
percentChange: e.target.checked,
sameYaxis: false,
});
}}
checked={
!yaxisOptions.sameYaxis && yaxisOptions.percentChange
}
/>
<label htmlFor="csv_percent_change" style={{ marginLeft: 5 }}>
Plot as percent change from first value
</label>
<br />
</>
)}
<input
style={{ marginTop: 2 }}
type="checkbox"
id="csv_same_yaxis"
name="csv_plot_yaxis_check"
onChange={(e) => {
setYaxisOptions({
...yaxisOptions,
sameYaxis: e.target.checked,
percentChange: false,
});
}}
checked={!yaxisOptions.percentChange && yaxisOptions.sameYaxis}
/>
<label htmlFor="csv_same_yaxis" style={{ marginLeft: 5 }}>
Share Y-axis
</label>
{traceType === "bar" && (
<div style={{ marginTop: 2 }} id="csv_bar_orientation">
<input
type="checkbox"
id="csv_bar_horizontal"
onChange={(e) => {
setOptions({
...options,
orientation: e.target.checked ? "h" : "v",
});
}}
/>
<label htmlFor="csv_bar_horizontal" style={{ marginLeft: 5 }}>
Plot horizontally
</label>
</div>
)}
</div>
</>
)}
<br />
<div style={{ float: "right", marginTop: 20 }}>
<button className="_btn-tertiary" id="csv_cancel" onClick={onClose}>
Cancel
</button>
<button className="_btn" id="csv_submit" onClick={onSubmit}>
Submit
</button>
</div>
</div>
</CommonDialog>
);
}
export function CSVonSubmit({
csvData,
plotlyData,
yaxisOptions,
traceType,
traceColor,
traceName,
options,
increasingColor,
decreasingColor,
}: {
csvData: any[];
plotlyData: any;
yaxisOptions: any;
traceType: string;
traceColor: string;
traceName: string;
options: any;
increasingColor: string;
decreasingColor: string;
}) {
let main_trace = plotlyData.data[0];
if (main_trace.xaxis == undefined) {
main_trace.xaxis = "x";
}
if (main_trace.yaxis == undefined) {
main_trace.yaxis = "y";
}
let yaxis_id = main_trace.yaxis;
let yaxis;
const left_yaxis_ticks = Object.keys(plotlyData.layout)
.filter((k) => k.startsWith("yaxis"))
.map((k) => plotlyData.layout[k])
.filter(
(yaxis) =>
yaxis.side == "left" &&
(yaxis.overlaying == "y" ||
(yaxis.fixedrange != undefined && yaxis.fixedrange == true))
).length;
let ticksuffix = left_yaxis_ticks > 0 ? " " : "";
if (yaxisOptions.sameYaxis !== true) {
const yaxes = Object.keys(plotlyData.layout)
.filter((k) => k.startsWith("yaxis"))
.map((k) => plotlyData.layout[k]);
yaxis = `y${yaxes.length + 1}`;
yaxis_id = `yaxis${yaxes.length + 1}`;
console.log(`yaxis: ${yaxis} ${yaxis_id}`);
plotlyData.layout[yaxis_id] = {
...layout_defaults,
title: {
text: traceName,
font: {
size: 14,
},
standoff: 0,
},
ticksuffix: ticksuffix,
layer: "below traces",
};
} else {
// Plot on the same yaxis
yaxis = main_trace.yaxis.replace("yaxis", "y");
}
const traceBase: any = {
type: traceType,
name: traceName,
showlegend: true,
yaxis: yaxis,
};
let trace: any = {};
if (["scatter", "bar"].includes(traceType)) {
const non_null = csvData.findIndex(
(x: any) => x[options.y] != null && x[options.y] != 0
);
trace = {
...traceBase,
x: csvData.map((row: any) => row[options.x]),
y: csvData.map(function (row: any) {
if (yaxisOptions.percentChange && traceType === "scatter") {
return (
(row[options.y] - csvData[non_null][options.y]) /
csvData[non_null][options.y]
);
} else {
return row[options.y];
}
}),
customdata: csvData.map((row: any) => row[options.y]),
hovertemplate: "%{customdata:.2f}<extra></extra>",
connectgaps: true,
marker: { color: traceColor },
};
if (traceType === "bar") {
trace.orientation = options.orientation;
trace.marker.opacity = 0.7;
delete trace.connectgaps;
delete trace.hovertemplate;
delete trace.customdata;
}
} else if (traceType === "candlestick") {
trace = {
...traceBase,
x: csvData.map((row: any) => row[options.x]),
open: csvData.map((row: any) => row[options.open]),
high: csvData.map((row: any) => row[options.high]),
low: csvData.map((row: any) => row[options.low]),
close: csvData.map((row: any) => row[options.close]),
increasing: { line: { color: increasingColor } },
decreasing: { line: { color: decreasingColor } },
};
}
const newPlotlyData = {
...plotlyData,
data: [...plotlyData.data, trace],
};
return newPlotlyData;
}

View File

@@ -0,0 +1,201 @@
import CommonDialog from "./CommonDialog";
import { useState } from "react";
const style = {
padding: "5px 2px 2px 5px",
margin: "2px 0",
};
export default function TextChartDialog({
open,
close,
addAnnotation,
deleteAnnotation,
popupData,
}: {
plotlyData: any;
open: boolean;
close: () => void;
addAnnotation: (annotation: any) => void;
updateAnnotation: (annotation: any) => void;
deleteAnnotation: (annotation: any) => void;
popupData: any | null;
}) {
const defaultPopupData = {
text: "",
color: "#0088CC",
size: 18,
bordercolor: "#822661",
yanchor: "above",
};
const [popUpData, setPopUpData] = useState<any>(defaultPopupData);
const [newPopupData, setNewPopupData] = useState<any>(defaultPopupData);
if (popupData && popupData !== popUpData) {
if (popupData.annotation) {
popupData.annotation = popupData?.annotation || {};
setPopUpData(popupData);
setNewPopupData(popupData);
}
}
function onClose() {
console.log("closing");
setPopUpData(defaultPopupData);
setNewPopupData(defaultPopupData);
close();
}
function onChange(e: any) {
console.log(e.target.id.replace("addtext_", ""), e.target.value);
const name = e.target.id.replace("addtext_", "");
const value =
e.target.type === "checkbox"
? e.target.checked
? 1
: 0
: e.target.value;
setNewPopupData({ ...newPopupData, [name]: value });
}
function onSubmit() {
console.log("submitting", newPopupData);
if (newPopupData.text !== "") {
let above = ["above", 1].includes(newPopupData.yanchor);
newPopupData.yanchor = above ? "above" : "below";
if (popUpData?.annotation) {
setNewPopupData({ ...newPopupData, annotation: popUpData.annotation });
}
addAnnotation(newPopupData);
close();
} else {
document.getElementById("popup_textarea_warning")!.style.display =
"block";
document.getElementById("addtext_text")!.style.border = "1px solid red";
}
}
function onDelete() {
deleteAnnotation(popUpData);
onClose();
}
return (
<CommonDialog
title="Add Text to Chart"
description="Change the titles on the chart."
open={open}
close={onClose}
>
<div id="popup_title" className="popup_content">
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<div style={{ marginBottom: 20 }}>
<label htmlFor="popup_text">
<b>Text:</b>
<div id="popup_textarea_warning" className="popup_warning">
Text is required
</div>
</label>
<textarea
id="addtext_text"
style={{
...style,
width: "100%",
maxWidth: "100%",
maxHeight: "200px",
marginTop: "8px",
}}
rows={4}
cols={50}
placeholder="Enter text here"
onChange={onChange}
defaultValue={popUpData?.annotation?.text || newPopupData?.text}
></textarea>
</div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div>
<label htmlFor="addtext_color">
<b>Font color:</b>
</label>
<input
type="color"
id="addtext_color"
defaultValue={
popUpData?.annotation?.color || newPopupData?.color
}
onChange={onChange}
></input>
</div>
<div>
<label htmlFor="addtext_bordercolor">
<b>Border color:</b>
</label>
<input
type="color"
id="addtext_bordercolor"
defaultValue={
popUpData?.annotation?.bordercolor ||
newPopupData?.bordercolor
}
onChange={onChange}
></input>
</div>
</div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div>
<label htmlFor="addtext_size">
<b>Font size:</b>
</label>
<input
style={{ ...style, width: "45px" }}
type="number"
id="addtext_size"
onChange={onChange}
defaultValue={popUpData?.annotation?.size || newPopupData?.size}
></input>
</div>
<div>
<label htmlFor="addtext_yanchor">
<b>Position (Above):</b>
</label>
<input
type="checkbox"
id="addtext_yanchor"
name="check"
defaultChecked={
newPopupData?.yanchor === "above" ? true : false
}
onChange={onChange}
></input>
</div>
</div>
</div>
<div style={{ float: "right", marginTop: 20 }}>
<button
className="_btn-tertiary ph-capture"
id="title_cancel"
onClick={onClose}
>
Cancel
</button>
<button
className="_btn ph-capture"
id="title_delete"
onClick={onDelete}
>
Delete
</button>
<button
className="_btn ph-capture"
id="title_submit"
onClick={onSubmit}
>
Submit
</button>
</div>
</div>
</CommonDialog>
);
}

View File

@@ -0,0 +1,137 @@
import CommonDialog, { styleDialog } from "../Dialogs/CommonDialog";
import { useState } from "react";
export default function TitleChartDialog({
plotlyData,
open,
close,
defaultTitle,
updateTitle,
updateAxesTitles,
}: {
plotlyData?: any;
open: boolean;
close: () => void;
defaultTitle: string;
updateTitle: (title: string) => void;
updateAxesTitles: (axesTitles: any) => void;
}) {
const [title, setTitle] = useState(defaultTitle);
const yAxes = Object.keys(plotlyData.layout || {}).filter(
(k) => k.startsWith("yaxis") && plotlyData.layout[k].range != undefined
);
const xAxes = Object.keys(plotlyData.layout || {}).filter(
(k) =>
k.startsWith("xaxis") &&
plotlyData.layout[k].showticklabels != undefined &&
plotlyData.layout[k]?.anchor
);
const [axesTitles, setAxesTitles] = useState<any>({});
return (
<CommonDialog
title="Chart Titles"
description="Change the titles on the chart."
open={open}
close={close}
>
<div id="popup_title" className="popup_content">
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
<div>
<label htmlFor="title_text">
<b>Title:</b>
</label>
<textarea
id="title_text"
style={{
...styleDialog,
width: "100%",
maxWidth: "100%",
maxHeight: "200px",
marginTop: "8px",
marginLeft: "0px",
}}
rows={2}
cols={20}
value={title}
onChange={(e) => setTitle(e.target.value)}
></textarea>
</div>
<div
id="xaxis_div"
className="csv_column_container"
style={{ marginTop: 5, marginBottom: -5 }}
>
{xAxes.map((x, i) => (
<div key={x} style={{ marginTop: 5, marginBottom: 5 }}>
<label htmlFor={`title_${x}`}>
{i === 0 ? <b>X axis:</b> : <b>X axis {i + 1}:</b>}
</label>
<input
id={`title_${x}`}
style={{ marginLeft: "0px", padding: "5px 2px 2px 5px" }}
type="text"
defaultValue={plotlyData?.layout[x]?.title?.text || ""}
onChange={(e) => {
setAxesTitles({
...axesTitles,
[x]: e.target.value,
});
}}
/>
</div>
))}
</div>
<div
id="yaxis_div"
className="csv_column_container"
style={{ marginTop: 5, marginBottom: 5 }}
>
{yAxes.map((y, i) => (
<div key={y} style={{ marginTop: 10 }}>
<label htmlFor={`title_${y}`}>
{i === 0 ? <b>Y axis:</b> : <b>Y axis {i + 1}:</b>}
</label>
<input
id={`title_${y}`}
style={{ marginLeft: "0px", padding: "5px 2px 2px 5px" }}
type="text"
defaultValue={plotlyData?.layout[y]?.title?.text || ""}
onChange={(e) => {
setAxesTitles({
...axesTitles,
[y]: e.target.value,
});
}}
/>
</div>
))}
</div>
</div>
<div style={{ float: "right", marginTop: 20 }}>
<button
className="_btn-tertiary ph-capture"
id="title_cancel"
onClick={close}
>
Cancel
</button>
<button
className="_btn ph-capture"
id="title_submit"
onClick={() => {
updateTitle(title);
updateAxesTitles(axesTitles);
close();
}}
>
Submit
</button>
</div>
</div>
</CommonDialog>
);
}

View File

@@ -0,0 +1,28 @@
import type { SVGProps } from "react";
interface SVGRProps {
title?: string;
titleId?: string;
}
const CloseIcon = ({
title,
titleId,
...props
}: SVGProps<SVGSVGElement> & SVGRProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
);
export default CloseIcon;

View File

@@ -0,0 +1,32 @@
import type { SVGProps } from "react";
interface SVGRProps {
title?: string;
titleId?: string;
}
const CloseCircleIcon = ({
title,
titleId,
...props
}: SVGProps<SVGSVGElement> & SVGRProps) => (
<svg
viewBox="0 0 18 18"
width={18}
height={18}
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-labelledby={titleId}
{...props}
>
{title ? <title id={titleId}>{title}</title> : null}
<path
d="M9 16.5a7.5 7.5 0 1 0 0-15 7.5 7.5 0 0 0 0 15ZM11.25 6.75l-4.5 4.5M6.75 6.75l4.5 4.5"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default CloseCircleIcon;

View File

@@ -0,0 +1,32 @@
import type { SVGProps } from "react";
interface SVGRProps {
title?: string;
titleId?: string;
}
const InfoIcon = ({
title,
titleId,
...props
}: SVGProps<SVGSVGElement> & SVGRProps) => (
<svg
viewBox="0 0 24 24"
width={24}
height={24}
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-labelledby={titleId}
{...props}
>
{title ? <title id={titleId}>{title}</title> : null}
<path
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10ZM12 16v-4M12 8h.01"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default InfoIcon;

View File

@@ -0,0 +1,39 @@
import type { SVGProps } from "react";
interface SVGRProps {
title?: string;
titleId?: string;
}
const SuccessIcon = ({
title,
titleId,
...props
}: SVGProps<SVGSVGElement> & SVGRProps) => (
<svg
width={18}
height={18}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-labelledby={titleId}
{...props}
>
{title ? <title id={titleId}>{title}</title> : null}
<path
d="M16.5 8.31V9a7.5 7.5 0 1 1-4.447-6.855"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M16.5 3 9 10.508l-2.25-2.25"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SuccessIcon;

View File

@@ -0,0 +1,32 @@
import type { SVGProps } from "react";
interface SVGRProps {
title?: string;
titleId?: string;
}
const WarningIcon = ({
title,
titleId,
...props
}: SVGProps<SVGSVGElement> & SVGRProps) => (
<svg
viewBox="0 0 18 18"
width={18}
height={18}
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-labelledby={titleId}
{...props}
>
{title ? <title id={titleId}>{title}</title> : null}
<path
d="M7.718 2.895 1.366 13.5a1.5 1.5 0 0 0 1.282 2.25h12.705a1.5 1.5 0 0 0 1.283-2.25L10.283 2.895a1.5 1.5 0 0 0-2.565 0v0ZM9 6.75v3M9 12.75h.008"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default WarningIcon;

View File

@@ -0,0 +1,172 @@
import { Icons as PlotlyIcons } from "plotly.js-dist-min";
import { ICONS } from "./Config";
import { useHotkeys } from "react-hotkeys-hook";
import { downloadCSV, downloadImage } from "../utils/utils";
export function hideModebar(hide: boolean = true) {
return new Promise((resolve) => {
if (!window.MODEBAR) {
window.MODEBAR = window.document.getElementsByClassName(
"modebar-container"
)[0] as HTMLElement;
window.MODEBAR.style.cssText = `${window.MODEBAR.style.cssText}; display:flex;`;
}
let includes_text = "display: none";
if (window.MODEBAR) {
if (window.MODEBAR.style.cssText.includes("display: none") && !hide) {
includes_text = "display: flex";
window.MODEBAR.style.cssText = `${window.MODEBAR.style.cssText}; display:flex;`;
} else {
window.MODEBAR.style.cssText = `${window.MODEBAR.style.cssText}; display:none;`;
}
resolve(true);
}
});
}
export function PlotConfig({
setModal,
changeTheme,
autoScaling,
Loading,
changeColor,
}: {
setModal: (modal: { modal: string; data?: any }) => void;
changeTheme: (change: boolean) => void;
autoScaling: (change: boolean) => void;
Loading: (change: boolean) => void;
changeColor: (change: boolean) => void;
}) {
useHotkeys(
"ctrl+shift+t",
() => {
setModal({ modal: "titleDialog" });
},
{ preventDefault: true }
);
useHotkeys(
"ctrl+t",
() => {
setModal({ modal: "textDialog" });
},
{ preventDefault: true }
);
useHotkeys(
"ctrl+o",
() => {
setModal({ modal: "overlayChart" });
},
{ preventDefault: true }
);
useHotkeys(
"ctrl+shift+h",
() => {
hideModebar();
},
{ preventDefault: true }
);
useHotkeys(
"ctrl+w",
() => {
window.close();
},
{ preventDefault: true }
);
const CONFIG = {
plotGlPixelRatio: 1,
scrollZoom: true,
responsive: true,
displaylogo: false,
displayModeBar: true,
modeBarButtonsToRemove: ["lasso2d", "select2d", "downloadImage"],
modeBarButtons: [
[
{
name: "Download CSV (Ctrl+Shift+S)",
icon: ICONS.downloadCsv,
click: async function (gd: any) {
setModal({ modal: "downloadCsv" });
await downloadCSV(gd);
},
},
{
name: "Download Chart as Image (Ctrl+S)",
icon: ICONS.downloadImage,
click: async function () {
hideModebar();
await downloadImage("MainChart", hideModebar, Loading);
},
},
// {
// name: "Upload Image (Ctrl+U)",
// icon: Plotly.Icons.uploadImage,
// click: function (gd) {
// downloadImage();
// },
// },
],
[
{
name: "Edit Color (Ctrl+E)",
icon: ICONS.changeColor,
click: function () {
changeColor(true);
},
},
"drawline",
"drawopenpath",
"drawcircle",
"drawrect",
"eraseshape",
],
[
{
name: "Overlay chart from CSV (Ctrl+O)",
icon: ICONS.plotCsv,
click: function () {
setModal({ modal: "overlayChart" });
},
},
{
name: "Add Text (Ctrl+T)",
icon: ICONS.addText,
click: function () {
setModal({ modal: "textDialog", data: { text: "" } });
},
},
{
name: "Change Titles (Ctrl+Shift+T)",
icon: ICONS.changeTitle,
click: function () {
setModal({ modal: "titleDialog" });
},
},
{
name: "Change Theme",
icon: ICONS.sunIcon,
click: function () {
changeTheme(true);
},
},
],
["hoverClosestCartesian", "hoverCompareCartesian", "toggleSpikelines"],
[
{
name: "Auto Scale (Ctrl+Shift+A)",
icon: PlotlyIcons.autoscale,
click: function () {
autoScaling(true);
},
},
"zoomIn2d",
"zoomOut2d",
"autoScale2d",
"zoom2d",
"pan2d",
],
],
};
return CONFIG;
}

View File

@@ -0,0 +1,84 @@
//@ts-nocheck
import { Figure } from "react-plotly.js";
import { hideModebar } from "./PlotlyConfig";
export default async function ResizeHandler({
plotData,
Volume,
}: {
plotData: Figure;
Volume: any;
}) {
// We hide the modebar and set the number of ticks to 5
const XAXIS = Object.keys(plotData.layout)
.filter((x) => x.startsWith("xaxis"))
.filter((x) => plotData.layout[x].showticklabels);
const TRACES = plotData.data.filter((trace) =>
trace?.name?.startsWith("Volume")
);
let layout_update: any = {};
let volume: any = Volume || { old_nticks: {} };
const width = window.innerWidth;
const height = window.innerHeight;
let tick_size =
height > 420 && width < 920 ? 9 : height > 420 && width < 500 ? 10 : 8;
if (width < 750) {
// We hide the modebar and set the number of ticks to 5
TRACES.forEach((trace) => {
if (trace.type == "bar") {
trace.opacity = 1;
trace.marker.line.width = 0.09;
if (Volume.yaxis == undefined) {
volume.yaxis = "yaxis" + trace.yaxis.replace("y", "");
layout_update[volume.yaxis + ".tickfont.size"] = tick_size;
volume.tickfont = plotData.layout[volume.yaxis].tickfont || {};
plotData.layout.margin.l -= 40;
}
}
});
XAXIS.forEach((x) => {
if (volume.old_nticks?.[x] == undefined) {
layout_update[x + ".nticks"] = 6;
volume.old_nticks[x] = plotData.layout[x].nticks || 10;
}
});
await hideModebar();
} else if (window.MODEBAR.style.cssText.includes("display: none")) {
// We show the modebar
await hideModebar(false);
if (Volume.old_nticks != undefined) {
XAXIS.forEach((x) => {
if (volume.old_nticks[x] != undefined) {
layout_update[x + ".nticks"] = volume.old_nticks[x];
volume.old_nticks[x] = undefined;
}
});
}
if (Volume.yaxis != undefined) {
TRACES.forEach((trace) => {
if (trace.type == "bar") {
trace.opacity = 0.5;
trace.marker.line.width = 0.2;
layout_update[volume.yaxis + ".tickfont.size"] =
volume.tickfont.size + 3;
plotData.layout.margin.l += 40;
volume.yaxis = undefined;
}
});
}
}
return {
volume_update: volume,
layout_update: layout_update,
plotData: plotData,
};
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

483
frontend-components/plotly/src/index.css vendored Normal file
View File

@@ -0,0 +1,483 @@
@font-face {
font-family: "Fira Code";
src: url("./fonts/FiraCode-VF.ttf") format("truetype");
/* fix Fire Code font width issue that makes Text get cut off in Plotly.js */
font-weight: 400 700;
font-stretch: 50%;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
._input {
@apply w-full text-xs px-2 py-1 bg-grey-50 dark:bg-grey-900 h-[36px] border-[1.5px] border-grey-300 dark:border-grey-700 rounded outline-none dark:outline-1 focus:border-grey-500 active:border-grey-500 dark:text-white dark:focus:text-white dark:focus:border-white dark:active:border-white dark:hover:border-white disabled:border-grey-600 font-normal;
}
._label {
@apply text-grey-500 text-xs uppercase tracking-widest font-bold;
}
._btn-common {
@apply whitespace-nowrap inline-flex w-full items-center justify-center gap-2 rounded py-2 px-6 text-center !no-underline transition duration-150 ease-out md:w-fit;
}
._btn {
@apply _btn-common text-sm bg-grey-800 text-white hover:bg-grey-600 dark:bg-grey-100 dark:text-grey-800 dark:hover:bg-white focus:outline focus:outline-2 focus:outline-grey-500 dark:active:bg-grey-200 dark:hover:active:text-grey-900 disabled:bg-grey-300 disabled:text-grey-500 active:disabled:bg-grey-300 active:disabled:text-grey-500;
}
._btn-secondary {
@apply _btn-common border border-grey-300 text-grey-300 outline-offset-0 hover:border-white hover:text-white focus:outline focus:outline-2 focus:outline-grey-500 active:border-white active:bg-grey-800 active:text-white disabled:border-grey-500 disabled:bg-transparent disabled:text-grey-500;
}
._btn-tertiary {
@apply _btn-common bg-grey-900 text-grey-50 hover:bg-grey-800 hover:text-white focus:bg-grey-900 focus:outline focus:outline-2 focus:outline-grey-500 active:bg-grey-700 active:text-white disabled:bg-grey-500 disabled:text-grey-700;
}
._icon-btn {
@apply _btn-common bg-grey-900 text-grey-50 hover:bg-grey-800 hover:text-white focus:bg-grey-900 focus:outline focus:outline-2 focus:outline-grey-500 active:bg-grey-700 active:text-white disabled:bg-grey-500 disabled:text-grey-700;
}
._hyper-link {
@apply underline text-blue-300 hover:text-blue-400 active:text-blue-500 disabled:text-grey-600;
}
._modal {
@apply card fixed top-1/2 left-1/2 z-50 max-h-[608px] w-[80%] max-w-[500px] -translate-x-1/2 -translate-y-1/2 overflow-auto text-white bg-grey-900 p-2 duration-300 focus:outline-none md:p-10;
}
._modal-overlay {
@apply bg-grey-900 opacity-60 z-40 inset-0 fixed;
}
._modal-close {
@apply absolute top-[10px] right-[20px] text-grey-200 hover:text-white rounded-[4px] focus:outline focus:outline-2 focus:outline-grey-500;
}
._modal-title {
@apply text-white uppercase tracking-widest text-lg font-bold;
}
.card {
@apply w-full rounded border border-grey-600 p-6 shadow bg-grey-850;
}
}
._header {
background: #062d48;
background: linear-gradient(
90deg,
#062d48,
#0b3054 15%,
#0e386c 45%,
#0b203d 64%,
#06101a 82%,
#060709
);
}
/* custom scrollbar */
::-webkit-scrollbar {
width: 20px;
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: #d6dee1;
border-radius: 20px;
border: 6px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background-color: #a8bbbf;
}
::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0);
}
body, html {
height: 100%;
overflow:hidden;
background: #000;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: "Fira Code", monospace, "Arial Black";
font-size: 12px;
font-size: 1.2rem;
color: #ffffff;
background-color: #000000;
font-stretch: 50%;
overflow: hidden;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
.saving {
position: absolute;
z-index: 9999999;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 100px;
width: 200px;
display: none;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.7);
color: white;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5);
border: 1px solid rgba(250, 250, 250, 0.5);
font-size: 0.9em;
border-radius: 20px;
animation: popup 0.3s ease-in-out;
}
.saving.show {
display: flex;
}
.popup_overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 420px;
background: #070707;
border: 1px solid #dcdcdc;
box-shadow: 0px 4px 24px rgba(0, 0, 0, 0.06);
border-radius: 4px;
padding: 40px;
box-sizing: border-box;
display: none;
font-family: "Fira Code", monospace, "Arial Black";
animation: popup 0.3s ease-in-out;
box-shadow: 0 3px 10px rgb(0 0 0 / 0.2);
z-index: 2;
overflow: auto;
}
.popup_content {
position: relative;
background-color: transparent;
padding: 0px;
font-size: 0.75rem !important;
width: 100%;
height: 100%;
z-index: 3;
}
.popup_warning {
color: #ff0000;
font-size: 12px;
font-size: 0.8rem;
font-family: "Fira Code", monospace, "Arial Black";
display: none;
background-color: rgba(255, 0, 0, 0.1);
border-radius: 4px;
font-weight: bold;
margin-top: -18px;
margin-left: 45px;
}
._modal {font-size: 0.65rem !important; animation: popup 0.3s ease-in-out;}
#changecolor {
position: absolute;
z-index: 9;
text-align: center;
top: 50%;
left: 50%;
width: 100px;
height: 60px;
background-color: transparent;
padding: 10px;
box-sizing: border-box;
display: none;
}
#changecolor_header:hover {
margin-top: 0px !important;
margin-bottom: 0px !important;
border: 1px solid gold;
box-shadow: 0 0 5px gold;
}
#changecolor_header {
padding: 10px;
cursor: move;
z-index: 10;
color: #fff;
}
#changecolor_header input[type="color"] {
width: 100%;
height: 40px;
border: none;
border-radius: 5px;
margin-bottom: 20px !important;
cursor: pointer;
border: 1px solid #7d7d7d;
padding: 5px;
background-color: #131313;
color: #ffffff;
margin-right: 50px !important;
}
@keyframes popup {
0% {
-webkit-transition: all 0.3s ease-in-out;
transform: translate(-50%, -50%) scale(0.2);
}
40% {
transform: translate(-50%, -50%) scale(1.1);
}
80% {
transform: translate(-50%, -50%) scale(1.05);
}
100% {
transform: translate(-50%, -50%) scale(1);
}
}
.loader {
position: relative;
bottom: 0px;
right: -5px;
border: 4px solid #f3f3f3;
border-radius: 50%;
border-top: 4px solid #00acff;
width: 20px;
height: 20px;
animation: spin 1.5s linear infinite;
opacity: 1;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.csv_column_container {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 0px 10px;
margin-top: 10px;
margin-bottom: 10px;
}
.plotly-graph-div .modebar--hover .modebar-group {
border: 2px solid #5a5a5a !important;
border-radius: 4px;
padding: 2px 2px 2px !important;
}
.modebar-group {
display: flex !important;
background-color: transparent !important;
}
.modebar {
z-index: 5000 !important;
position: fixed !important;
bottom: 10px !important;
top: auto !important;
left: 50% !important;
right: auto !important;
align-items: center;
transform: translate(-50%, 10%) !important;
}
@media screen and (max-width: 600px) {
.modebar-group {
display: block !important;
background-color: transparent !important;
}
.modebar {
bottom: 20px !important;
}
}
@media screen and (max-width: 900px) {
._modal {font-size: 0.65rem !important; animation: popup 0.3s ease-in-out; height: 70% !important; overflow: auto !important;}
}
.modebar-group {
margin-left: 10px;
margin-right: 10px;
background: #0f0f0f !important;
border: 1px solid #404040;
box-shadow: 0px 4px 24px rgba(0, 0, 0, 0.06);
border-radius: 8px;
}
.modebar-group:nth-of-type(1):after {
content: "Panning Tools";
height: fit-content;
width: fit-content;
position: absolute;
display: none;
}
.modebar-group:nth-of-type(1):hover:after {
padding: 10px;
display: block;
font-size: 10px;
color: white;
cursor: pointer;
background: #0f0f0f !important;
border: 1px solid #404040;
box-shadow: 0px 4px 24px rgba(0, 0, 0, 0.06);
border-radius: 8px;
top: -40px;
left: 50%;
transform: translateX(-50%);
}
.modebar-group:nth-of-type(2):after {
content: "Indicator Tools";
height: fit-content;
width: fit-content;
position: absolute;
display: none;
}
.modebar-group:nth-of-type(2):hover:after {
padding: 10px;
display: block;
font-size: 10px;
color: white;
cursor: pointer;
background: #0f0f0f !important;
border: 1px solid #404040;
box-shadow: 0px 4px 24px rgba(0, 0, 0, 0.06);
border-radius: 8px;
top: -40px;
left: 50%;
transform: translateX(-50%);
}
.modebar-group:nth-of-type(3):after {
content: "Chart Tools";
height: fit-content;
width: fit-content;
position: absolute;
display: none;
}
.modebar-group:nth-of-type(3):hover:after {
padding: 10px;
display: block;
font-size: 10px;
color: white;
cursor: pointer;
background: #0f0f0f !important;
border: 1px solid #404040;
box-shadow: 0px 4px 24px rgba(0, 0, 0, 0.06);
border-radius: 8px;
top: -40px;
left: 50%;
transform: translateX(-50%);
}
.modebar-group:nth-of-type(4):after {
content: "Drawing Tools";
height: fit-content;
width: fit-content;
position: absolute;
display: none;
}
.modebar-group:nth-of-type(4):hover:after {
padding: 10px;
display: block;
font-size: 10px;
color: white;
cursor: pointer;
background: #0f0f0f !important;
border: 1px solid #404040;
box-shadow: 0px 4px 24px rgba(0, 0, 0, 0.06);
border-radius: 8px;
top: -40px;
left: 50%;
transform: translateX(-50%);
}
.modebar-group:nth-of-type(5):after {
content: "Export Tools";
height: fit-content;
width: fit-content;
position: absolute;
display: none;
}
.modebar-group:nth-of-type(5):hover:after {
padding: 10px;
display: block;
font-size: 10px;
color: white;
cursor: pointer;
background: #0f0f0f !important;
border: 1px solid #404040;
box-shadow: 0px 4px 24px rgba(0, 0, 0, 0.06);
border-radius: 8px;
top: -40px;
left: 50%;
transform: translateX(-50%);
}
.modebar.vertical {
flex-direction: row !important;
flex-flow: initial !important;
gap: 6px !important;
}
.js-plotly-plot .plotly .modebar.vertical .modebar-group {
padding-bottom: 0px !important;
}
.modebar-container {
position: initial !important;
}
.modebar-btn:hover .icon path {
fill: #00acff !important;
}
.modebar-btn.active .icon path {
fill: #00acff !important;
}
.modebar-btn {
padding: 4px !important;
margin-left: 4px !important;
margin-right: 4px !important;
margin-top: 4px !important;
margin-bottom: 4px !important;
}
.modebar-btn:hover {
background-color: #1f1f1f !important;
border-radius: 8px;
}

22
frontend-components/plotly/src/main.tsx vendored Normal file
View File

@@ -0,0 +1,22 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "./index.css";
declare global {
interface Window {
plotly_figure: any;
export_image: string;
save_image: boolean;
title: string;
Plotly: any;
MODEBAR: HTMLElement;
}
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root") as HTMLElement
);

View File

@@ -0,0 +1,239 @@
//@ts-nocheck
import {
Annotations,
PlotMouseEvent,
PlotlyHTMLElement,
} from "plotly.js-dist-min";
import { Figure } from "react-plotly.js";
type PopupData = {
x: number;
y: number;
yref: string;
text: string;
yshift: number;
yanchor: string;
bordercolor: string;
color: string;
size: number;
annotation?: any;
};
export function add_annotation({
plotData,
popup_data,
current_text,
}: {
plotData: Figure;
popup_data: PopupData;
current_text?: string;
}) {
let x = popup_data.x;
let y = popup_data.y;
let yref = popup_data.yref;
let annotations = plotData?.layout?.annotations || [];
let index = -1;
for (let i = 0; i < annotations.length; i++) {
if (
annotations[i].x == x &&
annotations[i].y == y &&
annotations[i].text == current_text
) {
index = i;
break;
}
}
if (index == -1) {
let annotation: Annotations = {
x: x,
y: y,
xref: "x",
yref: yref,
xanchor: "center",
text: popup_data.text,
showarrow: true,
arrowhead: 2,
arrowsize: 1,
arrowwidth: 2,
ax: x,
ay: y + popup_data.yshift,
ayref: yref,
axref: "x",
bordercolor: popup_data.bordercolor,
bgcolor: "#000000",
borderwidth: 2,
borderpad: 4,
opacity: 0.8,
font: {
color: popup_data.color,
size: popup_data.size,
},
clicktoshow: "onoff",
captureevents: true,
};
annotations.push(annotation);
} else {
annotations[index].text = popup_data.text;
annotations[index].font.color = popup_data.color;
annotations[index].font.size = popup_data.size;
annotations[index].ay = y + popup_data.yshift;
annotations[index].bordercolor = popup_data.bordercolor;
}
return { annotations: annotations, annotation: annotations[index] };
}
export function plot_text({
plotData,
popup_data,
current_text,
}: {
plotData: Figure;
popup_data: PopupData;
current_text?: string;
}) {
// Plots text on the chart based on the popup_data
// If current_text is not null, it will be replaced with the new text
// If current_text is null, a new annotation will be added
// popup_data is the data from the popup
// data is the data from the chart
console.log("plot_text: current_text", current_text);
let output = undefined;
let yaxis = popup_data.yref.replace("y", "yaxis");
let yrange = plotData.layout[yaxis].range;
let yshift = (yrange[1] - yrange[0]) * 0.2;
if (popup_data.yanchor == "below") {
yshift = -yshift;
}
popup_data.yshift = yshift;
output = add_annotation({ plotData, popup_data, current_text });
let to_update = { annotations: output.annotations, dragmode: "pan" };
to_update[yaxis + ".type"] = "linear";
return { update: to_update, annotation: output.annotation };
}
export function init_annotation({
plotData,
popupData,
setPlotData,
setModal,
setOnAnnotationClick,
setAnnotations,
onAnnotationClick,
annotations,
plotDiv,
}: {
plotData: Figure;
popupData: Partial<PopupData>;
setPlotData: (plotData: Partial<Figure>) => void;
setModal: (modal: { modal: string; data?: any }) => void;
onAnnotationClick: any;
setOnAnnotationClick: (onAnnotationClick: any) => void;
setAnnotations: (annotations: Partial<Annotations>[]) => void;
annotations: Annotations[];
plotDiv: PlotlyHTMLElement;
}) {
if (popupData.text != undefined && popupData.text != "") {
popupData.text = popupData.text.replace(/\n/g, "<br>");
let popup_data: Partial<PopupData>;
if (popupData.annotation) {
console.log("data", popupData);
popup_data = {
x: popupData.annotation.x,
y: popupData.annotation.y,
yref: popupData.annotation.yref,
yanchor:
popupData.annotation.y < popupData.annotation.ay ? "above" : "below",
...popupData,
};
console.log("popup_data", popup_data);
let to_update = plot_text({
plotData,
popup_data: popup_data as PopupData,
current_text: popupData.annotation.text,
});
setAnnotations(
[...annotations, to_update.annotation].filter((a) => a != undefined)
);
plotData.layout.dragmode = "pan";
setPlotData({ ...plotData, ...to_update.update });
setOnAnnotationClick({});
return;
}
plotDiv.on("plotly_clickannotation", (eventData) => {
console.log("plotly_clickannotation", eventData);
let annotation = eventData.annotation;
if (annotation.text == undefined) {
console.log("annotation.text is undefined");
return;
}
console.log("annotation.text", annotation.text);
// we replace <br> with \n so that the textarea can display the text properly
annotation.text = annotation.text.replace(/<br>/g, "\n");
let popup_data = {
x: annotation.x,
y: annotation.y,
yanchor: annotation.y < annotation.ay ? "above" : "below",
text: annotation.text,
color: annotation.font.color,
size: annotation.font.size,
bordercolor: annotation.bordercolor,
annotation: annotation,
};
console.log("popup_data", popup_data);
setOnAnnotationClick(popup_data);
setModal({ modal: "textDialog", data: popup_data });
setOnAnnotationClick({});
});
function clickHandler(eventData: PlotMouseEvent) {
console.log("plotly_click", eventData);
let x = eventData.points[0].x;
let yaxis = eventData.points[0].fullData.yaxis;
let y = 0;
// We need to check if the trace is a candlestick or not
// this is because the y value is stored in the high or low
if (eventData.points[0].y != undefined) {
y = eventData.points[0].y;
} else if (eventData.points[0].low != undefined) {
if (popup_data?.yanchor == "below") {
y = eventData.points[0].low;
} else {
y = eventData.points[0].high;
}
}
popup_data = {
x: onAnnotationClick?.annotation?.x ?? x,
y: onAnnotationClick?.annotation?.y ?? y,
yref: onAnnotationClick?.annotation?.yref ?? yaxis,
...popupData,
};
let to_update = plot_text({
plotData,
popup_data: popup_data as PopupData,
current_text: onAnnotationClick?.annotation?.text,
});
setAnnotations(
[...annotations, to_update.annotation].filter((a) => a != undefined)
);
plotData.layout.dragmode = "pan";
setPlotData({ ...plotData, ...to_update.update });
plotDiv.removeAllListeners("plotly_click");
}
plotData.layout.dragmode = "select";
setPlotData({ ...plotData });
plotDiv.on("plotly_click", clickHandler);
}
}

View File

@@ -0,0 +1,42 @@
import { RefObject, useEffect } from "react";
export default function useOnClickOutside(
ref: RefObject<HTMLElement>,
handler: (event: MouseEvent | TouchEvent) => void
) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
}
export function useOnClickInside(
ref: RefObject<HTMLElement>,
handler: (event: MouseEvent | TouchEvent) => void
) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
// Do nothing if clicking ref's element or descendent elements
if (ref.current && ref.current.contains(event.target as Node)) {
handler(event);
}
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
}

View File

@@ -0,0 +1,338 @@
// @ts-nocheck
import domtoimage from "dom-to-image";
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"],
},
},
];
const getNewFileHandle = ({
filename,
is_image,
}: {
filename: string;
is_image?: boolean;
}): Promise<FileSystemFileHandle | null> => {
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);
}
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) {
let data = gd.data;
let columns: string[] = [];
let rows: any[] = [];
let xaxis =
"title" in gd.layout["xaxis"] &&
gd.layout["xaxis"]["title"]["text"] != undefined
? gd.layout["xaxis"]["title"]["text"]
: "x";
let 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 (trace.type == "scatter") {
if (columns.length == 0) {
columns.push(xaxis);
}
columns.push(trace.name);
trace.x.forEach(function (x, i) {
if (rows[i] == undefined) {
rows[i] = [x];
}
rows[i].push(trace.y[i]);
});
}
});
return await downloadData(columns, rows);
}
export async function downloadData(columns: any, data: any) {
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 {
let fileHandle = await getNewFileHandle({
filename: filename,
});
let ext: string = "png";
if (fileHandle !== null) {
// @ts-ignore
ext = fileHandle.name.split(".").pop();
}
await loadingOverlay(`Saving ${ext.toUpperCase()}`);
non_blocking(async function () {
// @ts-ignore
saveToFile(blob, filename, fileHandle).then(async function () {
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");
}
let is_loaded = setInterval(function () {
if (
is_close
? !loading.classList.contains("show")
: loading.classList.contains("show")
) {
clearInterval(is_loaded);
resolve(true);
}
}, 0.1);
});
}
export const non_blocking = (func: Function, delay: number) => {
let timeout: number;
return function () {
// @ts-ignore
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), delay);
};
};
const openbb_watermark = {
yref: "paper",
xref: "paper",
x: 1,
y: 0,
text: "OpenBB Terminal",
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) {
if (init) {
globals.CHART_DIV.layout.annotations.push(openbb_watermark);
if (globals.cmd_idx && globals.cmd_src) {
globals.CHART_DIV.layout.annotations[globals.cmd_idx].text =
globals.cmd_src;
}
Plotly.relayout(globals.CHART_DIV, {
"title.text": globals.title,
margin: globals.old_margin,
});
}
if (!init) {
if (globals.cmd_idx && globals.cmd_src) {
globals.CHART_DIV.layout.annotations[globals.cmd_idx].text = "";
}
globals.CHART_DIV.layout.annotations.splice(old_index, 1);
Plotly.relayout(globals.CHART_DIV, {
"title.text": "",
margin: margin,
});
}
}
export async function saveImage(
id: string,
filename: string,
extension: string
) {
const chart = document.getElementById(id) as HTMLElement;
if (["svg", "pdf"].includes(extension)) {
const margin = globals.CHART_DIV.layout.margin;
const old_index = globals.CHART_DIV.layout.annotations.length;
await setWatermarks(margin, old_index, true);
Plotly.downloadImage(globals.CHART_DIV, {
format: "svg",
height: globals.CHART_DIV.clientHeight,
width: globals.CHART_DIV.clientWidth,
filename: filename,
});
await setWatermarks(margin, old_index, false);
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
) {
const chart = document.getElementById(id) as HTMLElement;
const filename = `${window.title}.png`;
try {
let fileHandle = await getNewFileHandle({
filename: filename,
is_image: true,
});
let ext: string = "png";
if (fileHandle !== null) {
// @ts-ignore
ext = fileHandle.name.split(".").pop();
}
loading(true);
await loadingOverlay(`Saving ${ext.toUpperCase()}`);
non_blocking(async function () {
domtoimage.toBlob(chart).then(function (blob: Blob) {
saveToFile(blob, filename, fileHandle).then(async function () {
await loadingOverlay("", true);
hidemodebar(false);
});
});
}, 2)();
loading(false);
} catch (error) {
console.error(error);
hidemodebar(false);
}
}