CatPtain's picture
Upload 100 files
ccefd0b verified
// @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);