Download frontend-components/plotly/src/components/Chart.tsx from CatPtain/OpenBB: direct link, hf CLI and curl.
- Browser
- Download file 28.9 kB
-
https://huggingface.co/spaces/CatPtain/OpenBB/resolve/main/frontend-components/plotly/src/components/Chart.tsx
- Command line
-
hf download hf://spaces/CatPtain/OpenBB/frontend-components/plotly/src/components/Chart.tsx
-
curl -L -o Chart.tsx https://huggingface.co/spaces/CatPtain/OpenBB/resolve/main/frontend-components/plotly/src/components/Chart.tsx
28.9 kB
| // @ts-nocheck | |
| import clsx from "clsx"; | |
| import { debounce } from "lodash"; | |
| import * as Plotly from "plotly.js-dist-min"; | |
| import { Icons as PlotlyIcons } from "plotly.js-dist-min"; | |
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |
| import createPlotlyComponent from "react-plotly.js/factory"; | |
| import { init_annotation } from "../utils/addAnnotation"; | |
| import { non_blocking } from "../utils/utils"; | |
| import autoScaling, { isoDateRegex } from "./AutoScaling"; | |
| import ChangeColor from "./ChangeColor"; | |
| import { DARK_CHARTS_TEMPLATE, ICONS, LIGHT_CHARTS_TEMPLATE } from "./Config"; | |
| import AlertDialog from "./Dialogs/AlertDialog"; | |
| import OverlayChartDialog from "./Dialogs/OverlayChartDialog"; | |
| import TextChartDialog from "./Dialogs/TextChartDialog"; | |
| import TitleChartDialog from "./Dialogs/TitleChartDialog"; | |
| import { PlotConfig, hideModebar, ChartHotkeys } from "./PlotlyConfig"; | |
| import ResizeHandler from "./ResizeHandler"; | |
| // Add logging to help debug why annotations aren't working | |
| console.log = ((oldLog) => { | |
| return function(...args) { | |
| if (args[0] === "plotly_click") { | |
| console.trace("plotly_click called with:", args[1]); | |
| } | |
| return oldLog.apply(console, args); | |
| }; | |
| })(console.log); | |
| const Plot = createPlotlyComponent(Plotly); | |
| class PlotComponent extends React.Component { | |
| constructor(props) { | |
| super(props); | |
| this.state = { | |
| data: props.data, | |
| layout: props.layout, | |
| frames: props.frames, | |
| config: props.config, | |
| useResizeHandler: props.useResizeHandler, | |
| style: props.style, | |
| className: props.className, | |
| divId: props.divId, | |
| revision: props.revision, | |
| graphDiv: props.graphDiv, | |
| debug: props.debug, | |
| onInitialized: props.onInitialized, | |
| }; | |
| } | |
| render() { | |
| return ( | |
| <Plot | |
| data={this.state.data} | |
| layout={this.state.layout} | |
| frames={this.state.frames} | |
| config={this.state.config} | |
| useResizeHandler={this.state.useResizeHandler} | |
| style={this.state.style} | |
| className={this.state.className} | |
| divId={this.state.divId} | |
| revision={this.state.revision} | |
| graphDiv={this.state.graphDiv} | |
| debug={this.state.debug} | |
| onInitialized={this.state.onInitialized} | |
| onUpdate={(figure) => this.setState(figure)} | |
| onRelayout={(figure) => this.setState(figure)} | |
| onPurge={(figure) => this.setState(figure)} | |
| /> | |
| ); | |
| } | |
| } | |
| // Check if a chart is a scatter plot to handle annotations differently | |
| function isScatterPlot(data) { | |
| if (!data || !data.data) return false; | |
| // Check if chart is primarily scatter plots | |
| return data.data.some(trace => | |
| (trace.type === 'scatter' || trace.mode === 'markers' || trace.mode === 'lines+markers') && | |
| !(trace.type === 'candlestick' || trace.type === 'ohlc') | |
| ); | |
| } | |
| // Debug function to check annotation structure | |
| function debugAnnotation(annotation, label = "Annotation Debug") { | |
| console.log(`[${label}]`, { | |
| text: annotation.text, | |
| visible: annotation.visible, | |
| x: annotation.x, | |
| y: annotation.y, | |
| layer: annotation.layer, | |
| font: annotation.font, | |
| arrowcolor: annotation.arrowcolor | |
| }); | |
| } | |
| // Exported for external use | |
| window.debugPlotlyAnnotations = function() { | |
| if (window.Plotly && window.Plotly.d3.select('#plotlyChart').node()._fullLayout) { | |
| const annotations = window.Plotly.d3.select('#plotlyChart').node()._fullLayout.annotations || []; | |
| console.log("[All Annotations]", annotations); | |
| annotations.forEach(a => debugAnnotation(a)); | |
| return annotations; | |
| } | |
| return []; | |
| } | |
| export const getXRange = (min: string, max: string) => { | |
| if (isoDateRegex.test(min.replace(" ", "T").split(".")[0])) { | |
| const check_min = new Date(min.replace(" ", "T").split(".")[0]); | |
| const check_max = new Date(max.replace(" ", "T").split(".")[0]); | |
| check_min.setSeconds(0); | |
| check_max.setSeconds(0); | |
| check_min.setMilliseconds(0); | |
| check_max.setMilliseconds(0); | |
| const multiplier = | |
| [5, 0, 1].includes(check_min.getDay()) || | |
| [4, 5, 6].includes(check_max.getDay()) | |
| ? 2 | |
| : 0; | |
| const x0_min = new Date(check_min.getTime() - 86400000 * multiplier); | |
| const x1_max = new Date(check_max.getTime() + 86400000 * multiplier); | |
| const xrange = [x0_min.toISOString(), x1_max.toISOString()]; | |
| return { x0_min, x1_max, xrange }; | |
| } | |
| return { x0_min: min, x1_max: max, xrange: [min, max] }; | |
| }; | |
| function CreateDataXrange(figure: Figure, xrange?: any) { | |
| if (figure.frames && figure.frames.length > 0) { | |
| // Don't filter data for animated charts | |
| return figure; | |
| } | |
| const new_figure = { ...figure }; | |
| const data = new_figure.data; | |
| if (!xrange) { | |
| xrange = [ | |
| data[0]?.x[data[0].x.length - 2000], | |
| data[0]?.x[data[0].x.length - 1], | |
| ]; | |
| } | |
| const { x0_min, x1_max, range } = getXRange(xrange[0], xrange[1]); | |
| xrange = range; | |
| const new_data = []; | |
| data.forEach((trace) => { | |
| const new_trace = { ...trace }; | |
| const data_keys = [ | |
| "x", | |
| "y", | |
| "low", | |
| "high", | |
| "open", | |
| "close", | |
| "text", | |
| "customdata", | |
| ]; | |
| const xaxis: any[] = trace.x ? trace.x : []; | |
| const chunks = []; | |
| for (let i = 0; i < xaxis.length; i++) { | |
| const xval = xaxis[i]; | |
| if (isoDateRegex.test(xval)) { | |
| const x_time = new Date(xval).getTime(); | |
| if (x_time >= x0_min.getTime() && x_time <= x1_max.getTime()) { | |
| chunks.push(i); | |
| } | |
| } else if (xval >= xrange[0] && xval <= xrange[1]) { | |
| chunks.push(i); | |
| } | |
| } | |
| data_keys.forEach((key) => { | |
| if (trace[key] !== undefined && 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), | |
| ); | |
| } | |
| }); | |
| if (chunks.length > 0) new_data.push(new_trace); | |
| }); | |
| if (new_data.length === 0) | |
| return { | |
| ...figure, | |
| layout: { | |
| ...figure.layout, | |
| xaxis: { ...figure.layout.xaxis, range: xrange }, | |
| }, | |
| }; | |
| new_figure.layout.xaxis.range = xrange; | |
| new_figure.data = new_data; | |
| return new_figure; | |
| } | |
| async function DynamicLoad({ | |
| event, | |
| figure, | |
| }: { | |
| event?: any; | |
| figure: any; | |
| }) { | |
| if (figure.frames && figure.frames.length > 0) { | |
| // Don't filter data for animated charts | |
| return figure; | |
| } | |
| 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], | |
| ]; | |
| figure = CreateDataXrange(figure, xaxis_range); | |
| return figure; | |
| } catch (e) { | |
| console.log("error", e); | |
| } | |
| } | |
| function formatDate(date) { | |
| const d = new Date(date); | |
| const month = `${d.getMonth() + 1}`.padStart(2, "0"); | |
| const day = `${d.getDate()}`.padStart(2, "0"); | |
| const year = d.getFullYear(); | |
| const hour = `${d.getHours()}`.padStart(2, "0"); | |
| const minute = `${d.getMinutes()}`.padStart(2, "0"); | |
| const second = `${d.getSeconds()}`.padStart(2, "0"); | |
| return `${year}-${month}-${day} ${hour}:${minute}:${second}`; | |
| } | |
| function Chart({ | |
| json, | |
| date, | |
| cmd, | |
| title, | |
| globals, | |
| theme, | |
| }: { | |
| // @ts-ignore | |
| json: Figure; | |
| date: Date; | |
| cmd: string; | |
| title: string; | |
| globals: any; | |
| theme: string; | |
| }) { | |
| 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 [dateSliced, setDateSliced] = useState(false); | |
| const [plotData, setPlotDataState] = 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([]); function setPlotData(data: any) { | |
| data.layout.datarevision = data.layout.datarevision | |
| ? data.layout.datarevision + 1 | |
| : 1; | |
| setPlotDataState(data); | |
| if (plotDiv && plotData) { | |
| Plotly.react(plotDiv, data.data, data.layout); | |
| } | |
| } | |
| const onClose = () => setModal({ name: "" }); | |
| // @ts-ignore | |
| const onDeleteAnnotation = useCallback( | |
| (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); | |
| } | |
| }, | |
| [plotData], | |
| ); // @ts-ignore | |
| const onAddAnnotation = useCallback( | |
| (data) => { | |
| console.log("onAddAnnotation being called with data:", data); | |
| // Use the standard annotation flow | |
| init_annotation({ | |
| plotData, | |
| popupData: data, | |
| setPlotData, | |
| setModal, | |
| setOnAnnotationClick, | |
| setAnnotations, | |
| onAnnotationClick, | |
| ohlcAnnotation, | |
| setOhlcAnnotation, | |
| annotations, | |
| plotDiv, | |
| }); | |
| }, | |
| [plotData, onAnnotationClick, ohlcAnnotation, annotations, plotDiv], | |
| ); useEffect(() => { | |
| if (axesTitles && Object.keys(axesTitles).length > 0) { | |
| const layoutUpdate = {}; | |
| // Update the layout with the new titles | |
| Object.keys(axesTitles).forEach((k) => { | |
| plotData.layout[k].title = { | |
| ...(plotData.layout[k].title || {}), | |
| text: axesTitles[k], | |
| }; | |
| plotData.layout[k].showticklabels = true; | |
| layoutUpdate[`${k}.title.text`] = axesTitles[k]; | |
| }); | |
| if (plotDiv && Object.keys(layoutUpdate).length > 0) { | |
| Plotly.relayout(plotDiv, layoutUpdate); | |
| } | |
| setAxesTitles({}); | |
| } | |
| }, [axesTitles, plotDiv]); | |
| 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 }); | |
| } | |
| const debouncedDynamicLoad = async (eventData, figure) => { | |
| if (dateSliced) { | |
| const data = { ...figure }; | |
| DynamicLoad({ | |
| event: eventData, | |
| figure: data, | |
| }).then(async (toUpdate) => { | |
| autoScaling(eventData, toUpdate).then((scaled) => { | |
| if (!scaled.to_update) return; | |
| setYaxisFixedRange(scaled.yaxis_fixedrange); | |
| setPlotData({ ...toUpdate, layout: scaled.to_update }); | |
| }); | |
| }); | |
| } else { | |
| const scaled = await autoScaling(eventData, figure); | |
| if (!scaled.to_update) return; | |
| setYaxisFixedRange(scaled.yaxis_fixedrange); | |
| setPlotData({ ...figure, layout: scaled.to_update }); | |
| } | |
| }; | |
| const autoscaleButton = useCallback(() => { | |
| // 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", async (eventdata) => { | |
| if (eventdata["xaxis.range[0]"] === undefined) return; | |
| const debounceTimer = eventdata["relayout"] ? 0 : 300; | |
| if ( | |
| !eventdata["relayout"] && | |
| isoDateRegex.test( | |
| eventdata["xaxis.range[0]"].toString().replace(" ", "T"), | |
| ) | |
| ) { | |
| const date1 = new Date(eventdata["xaxis.range[0]"].replace(" ", "T")); | |
| const date2 = new Date(eventdata["xaxis.range[1]"].replace(" ", "T")); | |
| if (date2.getTime() - date1.getTime() < 3600000 * 2) { | |
| const d1 = new Date(date1.getTime() - 3600000 * 2); | |
| const d2 = new Date(date2.getTime() + 3600000 * 2); | |
| eventdata["xaxis.range[0]"] = formatDate(d1); | |
| eventdata["xaxis.range[1]"] = formatDate(d2); | |
| eventdata["relayout"] = true; | |
| return Plotly.relayout(plotDiv, eventdata); | |
| } | |
| } | |
| debounce(async () => { | |
| debouncedDynamicLoad(eventdata, originalData); | |
| }, debounceTimer)(); | |
| }); | |
| } | |
| // 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", | |
| debounce(async (eventdata) => { | |
| if (eventdata["xaxis.range[0]"] === undefined) return; | |
| debouncedDynamicLoad(eventdata, originalData); | |
| }, 300), | |
| ); | |
| } | |
| } | |
| button_pressed(title, active); | |
| }, [ | |
| barButtons, | |
| dateSliced, | |
| debouncedDynamicLoad, | |
| originalData, | |
| plotDiv, | |
| yaxisFixedRange, | |
| ]); | |
| 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 = originalData?.data.filter( | |
| (trace) => trace?.name?.trim() === "Volume", | |
| ); | |
| const darkmode = !darkMode; | |
| window.document.body.style.backgroundColor = darkmode ? "#000" : "#fff"; | |
| originalData.layout.font = { | |
| ...(originalData.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 = { | |
| "#00ACFF0": "#00ACFF", | |
| "#e4003a": "#e4003a", | |
| }; | |
| const volumeColorsLight = { | |
| "#e4003a": "#e4003a", | |
| "#00ACFF": "#00ACFF", | |
| }; | |
| 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; | |
| }); | |
| }); | |
| originalData.layout.template = darkmode | |
| ? DARK_CHARTS_TEMPLATE | |
| : LIGHT_CHARTS_TEMPLATE; | |
| // Preserve existing annotations as-is (no modifications) | |
| if (plotData.layout.annotations && plotData.layout.annotations.length > 0) { | |
| originalData.layout.annotations = [...plotData.layout.annotations]; | |
| } | |
| setPlotData({ ...originalData }); | |
| setDarkMode(darkmode); | |
| setChangeTheme(false); | |
| } catch (e) { | |
| console.log("error", e); | |
| } | |
| } | |
| }, [changeTheme, plotData.layout.annotations]); | |
| useEffect(() => { | |
| if (plotLoaded) { | |
| setDarkMode(true); | |
| setAutoScaling(false); | |
| const captureButtons = [ | |
| "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;`; | |
| // Add annotation click handler to ensure editing works on scatter plots | |
| if (plotDiv) { | |
| // When an annotation is clicked, open the edit dialog | |
| plotDiv.on('plotly_clickannotation', function(data) { | |
| console.log("Annotation clicked:", data); | |
| if (data && data.annotation && data.annotation.text) { | |
| setModal({ | |
| name: "textDialog", | |
| data: { | |
| annotation_dict: data.annotation, | |
| mode: "edit" | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| 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); | |
| } | |
| 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); | |
| } | |
| } | |
| 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.update(plotDiv, {}, layout_update); | |
| } | |
| }); | |
| if (theme !== "dark") { | |
| setChangeTheme(true); | |
| } | |
| } | |
| }, [plotLoaded]); | |
| useEffect(() => { | |
| // This effect ensures annotations appear correctly on all chart types | |
| if (plotDiv && plotData?.layout?.annotations?.length > 0) { | |
| Plotly.relayout(plotDiv, {'annotations': plotData.layout.annotations}); | |
| } | |
| }, [plotData.layout.annotations, plotDiv]); const plotComponent = useMemo( | |
| () => ( | |
| <PlotComponent | |
| onInitialized={(_figure, graphDiv) => { | |
| if (!plotDiv) { | |
| if (graphDiv) { | |
| graphDiv.globals = globals; | |
| setPlotDiv(graphDiv); | |
| graphDiv.on('plotly_clickannotation', function(data) { | |
| if (data && data.annotation && data.annotation.text) { | |
| setModal({ | |
| name: "textDialog", | |
| data: { | |
| annotation_dict: data.annotation, | |
| mode: "edit" | |
| } | |
| }); | |
| } | |
| }); | |
| } | |
| } | |
| if (!plotLoaded) setPlotLoaded(true); | |
| }} | |
| className="w-full h-full" | |
| divId="plotlyChart" | |
| data={plotData.data} | |
| layout={plotData.layout} | |
| frames={plotData.frames} | |
| config={PlotConfig({ | |
| setModal: setModal, | |
| changeTheme: setChangeTheme, | |
| autoScaling: setAutoScaling, | |
| Loading: setLoading, | |
| changeColor: setChangeColor, | |
| })} | |
| /> | |
| ), | |
| [ | |
| plotDiv, | |
| originalData, | |
| plotLoaded, | |
| plotData, | |
| globals, | |
| setPlotDiv, | |
| setPlotLoaded, | |
| setModal, | |
| setChangeTheme, | |
| setAutoScaling, | |
| setLoading, | |
| onChangeColor, | |
| ], | |
| ); | |
| const memoizedAlertDialog = useMemo(() => { | |
| return ( | |
| <AlertDialog | |
| title={modal?.data?.title} | |
| content={modal?.data?.content} | |
| open={modal?.name === "alertDialog"} | |
| close={onClose} | |
| /> | |
| ); | |
| }, [modal, onClose]); | |
| const memoizedOverlayChartDialog = useMemo(() => { | |
| return ( | |
| <OverlayChartDialog | |
| addOverlay={(overlay) => { | |
| console.log(overlay); | |
| overlay.layout.showlegend = true; | |
| setOriginalData(overlay); | |
| setPlotData(overlay); | |
| }} | |
| plotlyData={originalData} | |
| setLoading={setLoading} | |
| open={modal?.name === "overlayChart"} | |
| close={onClose} | |
| /> | |
| ); | |
| }, [modal, plotData, onClose, setPlotData, setLoading]); | |
| const memoizedTitleChartDialog = useMemo(() => { | |
| return ( | |
| <TitleChartDialog | |
| updateTitle={(title) => setChartTitle(title)} | |
| updateAxesTitles={(axesTitles) => setAxesTitles(axesTitles)} | |
| defaultTitle={chartTitle} | |
| plotlyData={plotData} | |
| open={modal?.name === "titleDialog"} | |
| close={onClose} | |
| /> | |
| ); | |
| }, [modal, plotData, chartTitle, onClose]); | |
| const memoizedTextChartDialog = useMemo(() => { | |
| return ( | |
| <TextChartDialog | |
| popupData={modal?.name === "textDialog" ? modal?.data : null} | |
| open={modal?.name === "textDialog"} | |
| close={onClose} | |
| addAnnotation={(data) => onAddAnnotation(data)} | |
| deleteAnnotation={(data) => onDeleteAnnotation(data)} | |
| /> | |
| ); | |
| }, [ | |
| modal, | |
| onAddAnnotation, | |
| onDeleteAnnotation, | |
| onClose, | |
| plotData, | |
| setPlotData, | |
| ]); | |
| const memoizedChangeColor = useMemo(() => { | |
| return <ChangeColor open={colorActive} onColorChange={onChangeColor} />; | |
| }, [colorActive, onChangeColor]); | |
| const memoizedChartHotkeys = useMemo(() => { | |
| return ( | |
| <ChartHotkeys | |
| setModal={setModal} | |
| Loading={setLoading} | |
| changeColor={setChangeColor} | |
| /> | |
| ); | |
| }, [setModal, setLoading, setChangeColor]); | |
| 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" | |
| /> | |
| <path | |
| className="opacity-75" | |
| fill="currentColor" | |
| d="M4 12a8 8 0 018-8v8z" | |
| /> | |
| </svg> | |
| </div> | |
| )} | |
| <div id="loading" className="saving"> | |
| <div id="loading_text" className="loading_text" /> | |
| <div id="loader" className="loader" /> | |
| </div> | |
| {memoizedAlertDialog} | |
| {memoizedOverlayChartDialog} | |
| {memoizedTitleChartDialog} | |
| {memoizedTextChartDialog} | |
| {memoizedChangeColor} | |
| {memoizedChartHotkeys} | |
| <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" | |
| /> | |
| </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={clsx("w-full sm:pb-12", { | |
| "h-[calc(100%-10px)]": maximizePlot, | |
| "h-[calc(100%-50px)]": !maximizePlot, | |
| })} | |
| > | |
| {plotComponent} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default React.memo(Chart); | |