|
|
|
|
|
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"; |
|
|
|
|
|
|
|
|
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)} |
|
|
/> |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function isScatterPlot(data) { |
|
|
if (!data || !data.data) return false; |
|
|
|
|
|
|
|
|
return data.data.some(trace => |
|
|
(trace.type === 'scatter' || trace.mode === 'markers' || trace.mode === 'lines+markers') && |
|
|
!(trace.type === 'candlestick' || trace.type === 'ohlc') |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
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 |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
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) { |
|
|
|
|
|
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; |
|
|
|
|
|
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: "" }); |
|
|
|
|
|
|
|
|
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], |
|
|
); |
|
|
const onAddAnnotation = useCallback( |
|
|
(data) => { |
|
|
console.log("onAddAnnotation being called with data:", data); |
|
|
|
|
|
|
|
|
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 = {}; |
|
|
|
|
|
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) { |
|
|
|
|
|
|
|
|
|
|
|
const shapes = plotDiv.layout.shapes; |
|
|
if (!shapes || shapes.length === 0) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const last_shape = shapes[shapes.length - 1]; |
|
|
last_shape.line.color = color; |
|
|
Plotly.update(plotDiv, {}, { shapes: shapes }); |
|
|
} |
|
|
|
|
|
function button_pressed(title, active = false) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(() => { |
|
|
|
|
|
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)(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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() { |
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
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;`; |
|
|
|
|
|
|
|
|
if (plotDiv) { |
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
|
|
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(() => { |
|
|
|
|
|
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); |
|
|
|