mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-28 21:12:16 +00:00
init
This commit is contained in:
70
frontend-components/plotly/README.md
vendored
Normal file
70
frontend-components/plotly/README.md
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||
|
||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||
40
frontend-components/plotly/index.html
vendored
Normal file
40
frontend-components/plotly/index.html
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<title>OpenBB Interactive Charts</title>
|
||||
<script>
|
||||
if (
|
||||
// check if user had saved dark as their
|
||||
// theme when accessing page before
|
||||
localStorage.theme === "dark" ||
|
||||
// or user's requesting dark color
|
||||
// scheme through operating system
|
||||
(!("theme" in localStorage) &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
) {
|
||||
// then if we have access to the document and the element
|
||||
// we add the dark class to the html element and
|
||||
// store the dark value in the localStorage
|
||||
if (document && document.documentElement) {
|
||||
document.documentElement.classList.add("dark");
|
||||
localStorage.setItem("theme", "dark");
|
||||
}
|
||||
} else {
|
||||
// else if we have access to the document and the element
|
||||
// we remove the dark class to the html element and
|
||||
// store the value light in the localStorage
|
||||
if (document && document.documentElement) {
|
||||
document.documentElement.classList.remove("dark");
|
||||
localStorage.setItem("theme", "light");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" class="h-full"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
5740
frontend-components/plotly/package-lock.json
generated
vendored
Normal file
5740
frontend-components/plotly/package-lock.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
50
frontend-components/plotly/package.json
vendored
Normal file
50
frontend-components/plotly/package.json
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "plotly",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build_tsc": "tsc && vite build",
|
||||
"deploy": "npm run build && mv dist/index.html ../../openbb_terminal/core/plots/plotly.html",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.0.3",
|
||||
"@radix-ui/react-context-menu": "^2.1.3",
|
||||
"@radix-ui/react-dialog": "^1.0.3",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
||||
"@radix-ui/react-icons": "^1.2.0",
|
||||
"@radix-ui/react-radio-group": "^1.1.2",
|
||||
"@radix-ui/react-select": "^1.2.1",
|
||||
"@radix-ui/react-toast": "^1.1.3",
|
||||
"dom-to-image": "^2.6.0",
|
||||
"plotly.js-dist-min": "^2.21.0",
|
||||
"posthog-js": "^1.55.0",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"react-plotly.js": "^2.6.0",
|
||||
"xlsx": "^0.18.5",
|
||||
"xss": "^1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dom-to-image": "^2.6.4",
|
||||
"@types/node": "^18.16.3",
|
||||
"@types/plotly.js-dist-min": "^2.3.1",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react-plotly.js": "^2.6.0",
|
||||
"@types/react-table": "^7.7.14",
|
||||
"@types/wicg-file-system-access": "^2020.9.6",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"clsx": "^1.2.1",
|
||||
"posthog-js": "^1.55.0",
|
||||
"postcss": "^8.4.21",
|
||||
"react-hotkeys-hook": "^4.4.0",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.1.0",
|
||||
"vite-plugin-singlefile": "^0.13.3"
|
||||
}
|
||||
}
|
||||
6
frontend-components/plotly/postcss.config.cjs
vendored
Normal file
6
frontend-components/plotly/postcss.config.cjs
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
175
frontend-components/plotly/src/App.tsx
vendored
Normal file
175
frontend-components/plotly/src/App.tsx
vendored
Normal 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;
|
||||
141
frontend-components/plotly/src/components/AutoScaling.tsx
vendored
Normal file
141
frontend-components/plotly/src/components/AutoScaling.tsx
vendored
Normal 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 {};
|
||||
}
|
||||
95
frontend-components/plotly/src/components/ChangeColor.tsx
vendored
Normal file
95
frontend-components/plotly/src/components/ChangeColor.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
469
frontend-components/plotly/src/components/Chart.tsx
vendored
Normal file
469
frontend-components/plotly/src/components/Chart.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
756
frontend-components/plotly/src/components/Config.tsx
vendored
Normal file
756
frontend-components/plotly/src/components/Config.tsx
vendored
Normal 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",
|
||||
},
|
||||
},
|
||||
};
|
||||
47
frontend-components/plotly/src/components/Dialogs/CommonDialog.tsx
vendored
Normal file
47
frontend-components/plotly/src/components/Dialogs/CommonDialog.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
617
frontend-components/plotly/src/components/Dialogs/OverlayChartDialog.tsx
vendored
Normal file
617
frontend-components/plotly/src/components/Dialogs/OverlayChartDialog.tsx
vendored
Normal 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;
|
||||
}
|
||||
201
frontend-components/plotly/src/components/Dialogs/TextChartDialog.tsx
vendored
Normal file
201
frontend-components/plotly/src/components/Dialogs/TextChartDialog.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
137
frontend-components/plotly/src/components/Dialogs/TitleChartDialog.tsx
vendored
Normal file
137
frontend-components/plotly/src/components/Dialogs/TitleChartDialog.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
28
frontend-components/plotly/src/components/Icons/Close.tsx
vendored
Normal file
28
frontend-components/plotly/src/components/Icons/Close.tsx
vendored
Normal 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;
|
||||
32
frontend-components/plotly/src/components/Icons/CloseCircle.tsx
vendored
Normal file
32
frontend-components/plotly/src/components/Icons/CloseCircle.tsx
vendored
Normal 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;
|
||||
32
frontend-components/plotly/src/components/Icons/Info.tsx
vendored
Normal file
32
frontend-components/plotly/src/components/Icons/Info.tsx
vendored
Normal 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;
|
||||
39
frontend-components/plotly/src/components/Icons/Success.tsx
vendored
Normal file
39
frontend-components/plotly/src/components/Icons/Success.tsx
vendored
Normal 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;
|
||||
32
frontend-components/plotly/src/components/Icons/Warning.tsx
vendored
Normal file
32
frontend-components/plotly/src/components/Icons/Warning.tsx
vendored
Normal 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;
|
||||
172
frontend-components/plotly/src/components/PlotlyConfig.tsx
vendored
Normal file
172
frontend-components/plotly/src/components/PlotlyConfig.tsx
vendored
Normal 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;
|
||||
}
|
||||
84
frontend-components/plotly/src/components/ResizeHandler.tsx
vendored
Normal file
84
frontend-components/plotly/src/components/ResizeHandler.tsx
vendored
Normal 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,
|
||||
};
|
||||
}
|
||||
14420
frontend-components/plotly/src/data/mockup.ts
vendored
Normal file
14420
frontend-components/plotly/src/data/mockup.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
frontend-components/plotly/src/fonts/FiraCode-Regular.ttf
vendored
Normal file
BIN
frontend-components/plotly/src/fonts/FiraCode-Regular.ttf
vendored
Normal file
Binary file not shown.
BIN
frontend-components/plotly/src/fonts/FiraCode-VF.ttf
vendored
Normal file
BIN
frontend-components/plotly/src/fonts/FiraCode-VF.ttf
vendored
Normal file
Binary file not shown.
483
frontend-components/plotly/src/index.css
vendored
Normal file
483
frontend-components/plotly/src/index.css
vendored
Normal 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
22
frontend-components/plotly/src/main.tsx
vendored
Normal 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
|
||||
);
|
||||
239
frontend-components/plotly/src/utils/addAnnotation.tsx
vendored
Normal file
239
frontend-components/plotly/src/utils/addAnnotation.tsx
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
42
frontend-components/plotly/src/utils/useClickOutside.tsx
vendored
Normal file
42
frontend-components/plotly/src/utils/useClickOutside.tsx
vendored
Normal 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]);
|
||||
}
|
||||
338
frontend-components/plotly/src/utils/utils.ts
vendored
Normal file
338
frontend-components/plotly/src/utils/utils.ts
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
30
frontend-components/plotly/tailwind.config.cjs
vendored
Normal file
30
frontend-components/plotly/tailwind.config.cjs
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
screens: {
|
||||
smh: { raw: "(max-height: 450px)" },
|
||||
},
|
||||
colors: {
|
||||
"grey-50": "#f6f6f6ff",
|
||||
"grey-100": "#eaeaeaff",
|
||||
"grey-200": "#dcdcdcff",
|
||||
"grey-300": "#c8c8c8ff",
|
||||
"grey-400": "#a2a2a2ff",
|
||||
"grey-500": "#808080ff",
|
||||
"grey-600": "#5a5a5aff",
|
||||
"grey-700": "#474747ff",
|
||||
"grey-800": "#2a2a2aff",
|
||||
"grey-850": "#131313ff",
|
||||
"grey-900": "#070707ff",
|
||||
"burgundy-300": "#B47DA0",
|
||||
"burgundy-400": "#9B5181",
|
||||
"burgundy-500": "#822661",
|
||||
"burgundy-900": "#340F27",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
22
frontend-components/plotly/tsconfig.json
vendored
Normal file
22
frontend-components/plotly/tsconfig.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"typeRoots": ["node_modules/@types"],
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
9
frontend-components/plotly/tsconfig.node.json
vendored
Normal file
9
frontend-components/plotly/tsconfig.node.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
8
frontend-components/plotly/vite.config.ts
vendored
Normal file
8
frontend-components/plotly/vite.config.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { viteSingleFile } from "vite-plugin-singlefile";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), viteSingleFile()],
|
||||
});
|
||||
@@ -18,7 +18,7 @@ console = Console()
|
||||
menus = Literal["", "featflags", "settings"]
|
||||
|
||||
openbb_posthog = Posthog(
|
||||
"phc_vhssDAMod5qIplznQ75Kdgz4aB1qPFmeVmfEOZ4hkRw",
|
||||
"phc_w3vscAiNobDjUM9gYw2ffAFPUc8COciilsvvp3tAPi",
|
||||
host="https://app.posthog.com",
|
||||
)
|
||||
|
||||
|
||||
@@ -76,9 +76,8 @@ class Backend(PyWry):
|
||||
|
||||
super().__init__(**init_kwargs)
|
||||
|
||||
self.plotly_html: Path = (PLOTS_CORE_PATH / "plotly_temp.html").resolve()
|
||||
self.plotly_html: Path = (PLOTS_CORE_PATH / "plotly.html").resolve()
|
||||
self.table_html: Path = (PLOTS_CORE_PATH / "table.html").resolve()
|
||||
self.inject_path_to_html()
|
||||
self.isatty = (
|
||||
not JUPYTER_NOTEBOOK
|
||||
and sys.stdin.isatty()
|
||||
@@ -102,28 +101,6 @@ class Backend(PyWry):
|
||||
|
||||
self.WIDTH, self.HEIGHT = int(width), int(height)
|
||||
|
||||
def inject_path_to_html(self):
|
||||
"""Update the script tag in html with local path."""
|
||||
try:
|
||||
with open(PLOTS_CORE_PATH / "plotly.html", encoding="utf-8") as file: # type: ignore
|
||||
html = file.read()
|
||||
html = html.replace(
|
||||
"{{MAIN_PATH}}", str(PLOTS_CORE_PATH.as_uri())
|
||||
).replace("{{PLOTLYJS_PATH}}", str(PLOTLYJS_PATH.as_uri()))
|
||||
|
||||
# We create a temporary file to inject the path to the script tag
|
||||
# This is so we don't have to modify the original file
|
||||
# The file is deleted at program exit.
|
||||
with open(self.plotly_html, "w", encoding="utf-8") as file: # type: ignore
|
||||
file.write(html)
|
||||
except FileNotFoundError as error:
|
||||
console.print(
|
||||
"[bold red]plotly.html file not found, check the path:[/]"
|
||||
f"[green]{PLOTS_CORE_PATH / 'plotly.html'}[/]"
|
||||
)
|
||||
self.max_retries = 0 # pylint: disable=W0201
|
||||
raise error
|
||||
|
||||
def get_pending(self) -> list:
|
||||
"""Get the pending data that has not been sent to the backend."""
|
||||
# pylint: disable=W0201,E0203
|
||||
@@ -135,11 +112,15 @@ class Backend(PyWry):
|
||||
def get_plotly_html(self) -> str:
|
||||
"""Get the plotly html file."""
|
||||
self.set_window_dimensions()
|
||||
if not self.plotly_html.exists():
|
||||
self.inject_path_to_html()
|
||||
return self.get_plotly_html()
|
||||
if self.plotly_html.exists():
|
||||
return str(self.plotly_html)
|
||||
|
||||
return str(self.plotly_html)
|
||||
console.print(
|
||||
"[bold red]plotly.html file not found, check the path:[/]"
|
||||
f"[green]{PLOTS_CORE_PATH / 'plotly.html'}[/]"
|
||||
)
|
||||
self.max_retries = 0 # pylint: disable=W0201
|
||||
raise FileNotFoundError
|
||||
|
||||
def get_table_html(self) -> str:
|
||||
"""Get the table html file."""
|
||||
@@ -387,10 +368,6 @@ class Backend(PyWry):
|
||||
"download_path": str(get_current_user().preferences.USER_EXPORTS_DIRECTORY),
|
||||
}
|
||||
|
||||
def del_temp(self):
|
||||
"""Delete the temporary html file."""
|
||||
self.plotly_html.unlink(missing_ok=True)
|
||||
|
||||
def start(self, debug: bool = False):
|
||||
"""Start the backend WindowManager process."""
|
||||
if self.isatty:
|
||||
@@ -425,8 +402,6 @@ class Backend(PyWry):
|
||||
"""Close the backend."""
|
||||
if reset:
|
||||
self.max_retries = 50 # pylint: disable=W0201
|
||||
elif self.isatty:
|
||||
self.del_temp()
|
||||
|
||||
super().close(reset)
|
||||
|
||||
|
||||
4011
openbb_terminal/core/plots/plotly.html
vendored
4011
openbb_terminal/core/plots/plotly.html
vendored
File diff suppressed because one or more lines are too long
@@ -1550,9 +1550,9 @@ class OpenBBFigure(go.Figure):
|
||||
return
|
||||
|
||||
margin_add = (
|
||||
dict(l=80, r=60, b=90, t=40, pad=0)
|
||||
dict(l=80, r=60, b=80, t=40, pad=0)
|
||||
if not self._has_secondary_y or not self.has_subplots
|
||||
else dict(l=60, r=50, b=95, t=40, pad=0)
|
||||
else dict(l=60, r=50, b=85, t=40, pad=0)
|
||||
)
|
||||
|
||||
# We adjust margins
|
||||
|
||||
@@ -165,9 +165,9 @@ class Momentum(PltTA):
|
||||
yref="paper",
|
||||
text="<b>CLenow</b>",
|
||||
x=0,
|
||||
xanchor="left",
|
||||
xanchor="right",
|
||||
xshift=-6,
|
||||
yshift=-inchart_index * 18,
|
||||
xshift=-60,
|
||||
y=0.98,
|
||||
font_size=14,
|
||||
font_color=self.inchart_colors[inchart_index],
|
||||
@@ -225,9 +225,9 @@ class Momentum(PltTA):
|
||||
yref="paper",
|
||||
text="<b>Demark</b>",
|
||||
x=0,
|
||||
xanchor="left",
|
||||
xanchor="right",
|
||||
xshift=-6,
|
||||
yshift=-inchart_index * 18,
|
||||
xshift=-60,
|
||||
y=0.98,
|
||||
font_size=14,
|
||||
font_color=self.inchart_colors[inchart_index],
|
||||
|
||||
@@ -49,9 +49,9 @@ class Overlap(PltTA):
|
||||
yref="paper",
|
||||
text=f"<b>{column.replace('_', '').replace('RMA', 'MA')}</b>",
|
||||
x=0,
|
||||
xanchor="left",
|
||||
xanchor="right",
|
||||
xshift=-6,
|
||||
yshift=-inchart_index * 18,
|
||||
xshift=-60,
|
||||
y=0.98,
|
||||
font_size=14,
|
||||
font_color=self.inchart_colors[inchart_index],
|
||||
@@ -83,9 +83,9 @@ class Overlap(PltTA):
|
||||
yref="paper",
|
||||
text="<b>VWAP</b>",
|
||||
x=0,
|
||||
xanchor="left",
|
||||
xanchor="right",
|
||||
xshift=-6,
|
||||
yshift=-inchart_index * 18,
|
||||
xshift=-60,
|
||||
y=0.98,
|
||||
font_size=14,
|
||||
font_color=self.inchart_colors[inchart_index],
|
||||
|
||||
@@ -94,9 +94,9 @@ class Volatility(PltTA):
|
||||
yref="paper",
|
||||
text=f"<b>{bbands_text}</b>",
|
||||
x=0,
|
||||
xanchor="left",
|
||||
xanchor="right",
|
||||
xshift=-6,
|
||||
yshift=-inchart_index * 18,
|
||||
xshift=-60,
|
||||
y=0.98,
|
||||
font_size=14,
|
||||
font_color=theme.get_colors()[1],
|
||||
@@ -153,9 +153,9 @@ class Volatility(PltTA):
|
||||
yref="paper",
|
||||
text=f"<b>{donchian_text}</b>",
|
||||
x=0,
|
||||
xanchor="left",
|
||||
xanchor="right",
|
||||
xshift=-6,
|
||||
yshift=-inchart_index * 18,
|
||||
xshift=-60,
|
||||
y=0.98,
|
||||
font_size=14,
|
||||
font_color="#B47DA0",
|
||||
@@ -211,9 +211,9 @@ class Volatility(PltTA):
|
||||
yref="paper",
|
||||
text=f"<b>{kctext}</b>",
|
||||
x=0,
|
||||
xanchor="left",
|
||||
xanchor="right",
|
||||
xshift=-6,
|
||||
yshift=-inchart_index * 18,
|
||||
xshift=-60,
|
||||
y=0.98,
|
||||
font_size=14,
|
||||
font_color="#B47DA0",
|
||||
|
||||
@@ -452,7 +452,7 @@ class PlotlyTA(PltTA):
|
||||
if indicator in self.ma_mode:
|
||||
if ma_done:
|
||||
continue
|
||||
indicator, ma_done = "ma", True
|
||||
indicator, ma_done = "ma", True # noqa
|
||||
|
||||
figure, inchart_index = getattr(self, f"plot_{indicator}")(
|
||||
figure, self.df_ta, inchart_index
|
||||
|
||||
Reference in New Issue
Block a user