// @ts-nocheck import clsx from "clsx"; import * as Plotly from "plotly.js-dist-min"; import { Icons as PlotlyIcons } from "plotly.js-dist-min"; import { usePostHog } from "posthog-js/react"; import { useEffect, useState } from "react"; import createPlotlyComponent from "react-plotly.js/factory"; import { init_annotation } from "../utils/addAnnotation"; import { non_blocking, saveImage } from "../utils/utils"; import autoScaling from "./AutoScaling"; import ChangeColor from "./ChangeColor"; import { DARK_CHARTS_TEMPLATE, ICONS, LIGHT_CHARTS_TEMPLATE } from "./Config"; import AlertDialog from "./Dialogs/AlertDialog"; import DownloadFinishedDialog from "./Dialogs/DownloadFinishedDialog"; import OverlayChartDialog from "./Dialogs/OverlayChartDialog"; import TextChartDialog from "./Dialogs/TextChartDialog"; import TitleChartDialog from "./Dialogs/TitleChartDialog"; import { PlotConfig, hideModebar } from "./PlotlyConfig"; import ResizeHandler from "./ResizeHandler"; const Plot = createPlotlyComponent(Plotly); function CreateDataXrange(data: Plotly.PlotData[], xrange?: any) { if (!xrange) { xrange = [ data[0]?.x[data[0].x.length - 1000], data[0]?.x[data[0].x.length - 1], ]; } const new_data = []; data.forEach((trace) => { const new_trace = { ...trace }; const data_keys = [ "x", "y", "low", "high", "open", "close", "text", "customdata", ]; const xaxis = trace.x ? trace.x : []; const chunks = []; for (let i = 0; i < xaxis.length; i++) { if (xaxis[i] >= xrange[0] && xaxis[i] <= xrange[1]) { chunks.push(i); } } data_keys.forEach((key) => { if (trace[key] && Array.isArray(trace[key])) { new_trace[key] = trace[key].filter((_, i) => chunks.includes(i)); } }); const color_keys = ["marker", "line"]; color_keys.forEach((key) => { if (trace[key]?.color && Array.isArray(trace[key].color)) { new_trace[key] = { ...trace[key] }; new_trace[key].color = trace[key].color.filter((_, i) => chunks.includes(i), ); } }); new_data.push(new_trace); }); if (new_data.length === 0) return data; return new_data; } async function DynamicLoad({ event, figure, }: { event?: any; figure: any; }) { try { const XDATA = figure.data.filter( (trace) => trace.x !== undefined && trace.x.length > 0 && trace.x[0] !== undefined, ); if (XDATA.length === 0) return figure; // We get the xaxis range, if no event is passed, we get the last 1000 points const xaxis_range = event ? [event["xaxis.range[0]"], event["xaxis.range[1]"]] : [ XDATA[0]?.x[XDATA[0].x.length - 1000], XDATA[0]?.x[XDATA[0].x.length - 1], ]; const new_data = CreateDataXrange(figure.data, xaxis_range); figure.data = new_data; figure.layout.xaxis.range = xaxis_range; return figure; } catch (e) { console.log("error", e); } } export default function Chart({ json, date, cmd, title, globals, theme, info, }: { // @ts-ignore json: Figure; date: Date; cmd: string; title: string; globals: any; theme: string; info?: any; }) { const posthog = usePostHog(); useEffect(() => { if (posthog) posthog.capture("chart", info); }, []); json.layout.width = undefined; json.layout.height = undefined; if (json.layout?.title?.text) { json.layout.title.text = ""; } const [originalData, setOriginalData] = useState(json); 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({ name: "" }); const [loading, setLoading] = useState(false); const [plotDiv, setPlotDiv] = useState(null); const [volumeBars, setVolumeBars] = useState({ old_nticks: {} }); const [maximizePlot, setMaximizePlot] = useState(false); const [downloadFinished, setDownloadFinished] = useState(false); const [dateSliced, setDateSliced] = useState(false); const [plotData, setPlotData] = useState(originalData); 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 [ohlcAnnotation, setOhlcAnnotation] = useState([]); const [yaxisFixedRange, setYaxisFixedRange] = useState([]); const onClose = () => setModal({ name: "" }); // @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, ohlcAnnotation, setOhlcAnnotation, annotations, plotDiv, }); } useEffect(() => { if (downloadFinished) { setModal({ name: "downloadFinished" }); setDownloadFinished(false); } }, [downloadFinished]); useEffect(() => { 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({}); } }, [axesTitles]); 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 const shapes = plotDiv.layout.shapes; if (!shapes || shapes.length === 0) { return; } // we change last added shape color const 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 const 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 const title = "Auto Scale (Ctrl+Shift+A)"; const button = barButtons[title] || document.querySelector(`[data-title="${title}"]`); let active = true; if (button.style.border === "transparent") { plotDiv.removeAllListeners("plotly_relayout"); active = false; plotDiv.on( "plotly_relayout", non_blocking(async function (eventdata) { if (eventdata["xaxis.range[0]"] === undefined) return; if (dateSliced) { const data = { ...originalData }; await DynamicLoad({ event: eventdata, figure: data, }).then(async (to_update) => { setPlotData(to_update); Plotly.react(plotDiv, to_update.data, to_update.layout); const scaled = await autoScaling(eventdata, plotDiv); setYaxisFixedRange(scaled.yaxis_fixedrange); Plotly.update(plotDiv, {}, scaled.to_update); }); } else { const scaled = await autoScaling(eventdata, plotDiv); setYaxisFixedRange(scaled.yaxis_fixedrange); Plotly.update(plotDiv, {}, scaled.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"); yaxisFixedRange.forEach((yaxis) => { plotDiv.layout[yaxis].fixedrange = false; }); setYaxisFixedRange([]); if (dateSliced) { plotDiv.on( "plotly_relayout", non_blocking(async function (eventdata) { if (eventdata["xaxis.range[0]"] === undefined) return; const data = { ...originalData }; await DynamicLoad({ event: eventdata, figure: data, }).then(async (to_update) => { setPlotData(to_update); Plotly.react(plotDiv, to_update.data, to_update.layout); }); }, 100), ); } } button_pressed(title, active); } function changecolorButton() { // We need to check if the button is active or not const title = "Edit Color (Ctrl+E)"; const 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) { const 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?.trim() === "Volume", ); const 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 volumeColorsDark = { "#009600": "#00ACFF", "#c80000": "#e4003a", }; const volumeColorsLight = { "#e4003a": "#c80000", "#00ACFF": "#009600", }; const volumeColors = darkmode ? volumeColorsDark : volumeColorsLight; TRACES.forEach((trace) => { if (trace.type === "bar" && Array.isArray(trace.marker.color)) 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; window.MODEBAR.style.cssText = `${window.MODEBAR.style.cssText}; display:flex;`; if (modeBarButtons) { const barbuttons: any = {}; for (let i = 0; i < modeBarButtons.length; i++) { const 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); // const layout_update = { // "yaxis.exponentformat": "none" // }; // 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 const layout_update = { "yaxis.exponentformat": "none", "yaxis.tickformat": null, "yaxis.exponentbase": 10, }; Plotly.update(plotDiv, {}, layout_update); } } // We check to see if window.export_image is defined if (window.export_image !== undefined) { // We get the extension of the file and check if it is valid const 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 () { const update = await ResizeHandler({ plotData, volumeBars, setMaximizePlot, }); const layout_update = update.layout_update; const newPlotData = update.plotData; const volume_update = update.volume_update; if (Object.keys(layout_update).length > 0) { setPlotData(newPlotData); setVolumeBars(volume_update); Plotly.relayout(plotDiv, layout_update); } }); if (theme !== "dark") { setChangeTheme(true); } const traceTypes = originalData.data.map( (trace) => trace.type === "candlestick", ); if ( (originalData.data[0]?.x !== undefined && originalData.data[0]?.x.length <= 1000) || !traceTypes.includes(true) ) return; setModal({ name: "alertDialog", data: { title: "Warning", content: `Data has been truncated to 1000 points for performance reasons. Please use the zoom tool to see more data.`, }, }); const new_data = CreateDataXrange(originalData.data); setPlotData({ ...originalData, data: new_data }); setDateSliced(true); setAutoScaling(true); } }, [plotLoaded]); return (
{loading && (
)}
{ console.log(overlay); plotData.layout.showlegend = true; setPlotData(overlay); setPlotLoaded(false); }} plotlyData={plotData} setLoading={setLoading} open={modal.name === "overlayChart"} close={onClose} /> setChartTitle(title)} updateAxesTitles={(axesTitles) => setAxesTitles(axesTitles)} defaultTitle={chartTitle} plotlyData={plotData} open={modal.name === "titleDialog"} close={onClose} /> onAddAnnotation(data)} deleteAnnotation={(data) => onDeleteAnnotation(data)} />

{chartTitle} {/* {source && ( {`[${source}]`} )} */}

{new Intl.DateTimeFormat("en-GB", { dateStyle: "full", timeStyle: "long", }) .format(date) .replace(/:\d\d /, " ")}
{cmd}

{/* {source && typeof source === "string" && source.includes("*") && (

*not affiliated

)} */}
{ if (!plotDiv) { const plot = document.getElementById("plotlyChart"); console.log("plot", plot); if (plot) setPlotDiv(plot); plot.globals = globals; } 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, downloadFinished: setDownloadFinished, })} />
); }