diff options
author | teh_coderer <me@tehcoderer.com> | 2023-05-18 11:24:29 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-18 16:24:29 +0000 |
commit | a6a2268ddc3724ef69e9fb5e6c110a2fbdb11ffd (patch) | |
tree | 1c972acac50d334227f2c6a594285469e38e6f1e | |
parent | b54c47e2e99d0b952f38a069430116a48e231982 (diff) |
Hotfix/ Candle --log (#5039)
* init... might break stuff
* fix
* clean up
* check if array for color keys
-rw-r--r-- | frontend-components/plotly/src/App.tsx | 359 | ||||
-rw-r--r-- | frontend-components/plotly/src/components/Chart.tsx | 1097 | ||||
-rw-r--r-- | frontend-components/plotly/src/data/mockup.ts | 24967 | ||||
-rw-r--r-- | openbb_terminal/core/plots/plotly.html | 208 |
4 files changed, 11439 insertions, 15192 deletions
diff --git a/frontend-components/plotly/src/App.tsx b/frontend-components/plotly/src/App.tsx index 632f5351d05..0b899b53b0c 100644 --- a/frontend-components/plotly/src/App.tsx +++ b/frontend-components/plotly/src/App.tsx @@ -19,185 +19,186 @@ declare global { } function App() { - const [data, setData] = useState( - process.env.NODE_ENV === "production" ? null : candlestickMockup - ); - const [options, setOptions] = useState({}); - - useEffect(() => { - if (process.env.NODE_ENV === "production") { - const interval = setInterval(() => { - if (window.json_data) { - const data = window.json_data; - console.log(data); - setData(data); - clearInterval(interval); - } - }, 100); - return () => clearInterval(interval); - } - }, []); - - const transformData = (data: any) => { - if (!data) return null; - let globals = { - added_traces: [], - csv_yaxis_id: null, - cmd_src_idx: null, - cmd_idx: null, - cmd_src: "", - old_margin: null, - title: "", - }; - let filename = data.layout?.title?.text - .replace(/ -/g, "") - .replace(/-/g, "") - .replace(/<b>|<\/b>/g, "") - .replace(/ /g, "_"); - let date = new Date().toISOString().slice(0, 10).replace(/-/g, ""); - let time = new Date().toISOString().slice(11, 19).replace(/:/g, ""); - window.title = `openbb_${filename}_${date}_${time}`; - - if (data.layout.annotations != undefined) { - data.layout.annotations.forEach(function (annotation) { - if (annotation.text != undefined) - if (annotation.text[0] == "/") { - globals.cmd_src = annotation.text; - globals.cmd_idx = data.layout.annotations.indexOf(annotation); - annotation.text = ""; - - let margin = data.layout.margin; - globals.old_margin = { ...margin }; - if (margin.t != undefined && margin.t > 40) margin.t = 40; - - if (data.cmd == "/stocks/candle") margin.r -= 50; - } - }); - } - - // We add spaces to all trace names, due to Fira Code font width issues - // to make sure that the legend is not cut off - data.data.forEach(function (trace) { - if (trace.name != undefined) { - const name_length = trace.name.length; - trace.name = trace.name + " "; - trace.hoverlabel = { - namelength: name_length, - }; - } - }); - - let title = data.layout?.title?.text || "Interactive Chart"; - globals.title = title; - return { - data: data, - date: new Date(), - globals: globals, - cmd: data.command_location, - posthog: data.posthog, - python_version: data.python_version, - pywry_version: data.pywry_version, - terminal_version: data.terminal_version, - theme: data.theme, - title, - }; - }; - - const transformedData = transformData(data); - - if (transformedData) { - if (transformedData.posthog.collect_logs && !options) { - const opts = { - api_host: "https://app.posthog.com", - autocapture: { - css_selector_allowlist: [".ph-capture"], - }, - capture_pageview: false, - loaded: function (posthog: any) { - const log_id = transformedData?.log_id || ""; - - if (log_id != "" && log_id != "REPLACE_ME") posthog.identify(log_id); - - posthog.onFeatureFlags(function () { - if ( - !posthog.isFeatureEnabled("record-pywry", { send_event: false }) - ) - posthog.stopSessionRecording(); - - if ( - !posthog.isFeatureEnabled("collect-logs-pywry", { - send_event: false, - }) - ) - posthog.opt_out_capturing(); - else if (posthog.has_opted_out_capturing()) - posthog.opt_in_capturing(); - }); - }, - }; - setOptions(opts); - } - - const info = { - INFO: { - command: transformedData.cmd, - title: transformedData.title, - date: transformedData.date, - python_version: transformedData.python_version, - pywry_version: transformedData.pywry_version, - terminal_version: transformedData.terminal_version, - }, - }; - - const chartDiv = ( - <Chart - json={transformedData.data} - date={transformedData.date} - cmd={transformedData.cmd} - title={transformedData.title} - globals={transformedData.globals} - theme={transformedData.theme} - info={info} - /> - ); - - if (transformedData.posthog.collect_logs && options) { - return ( - <PostHogProvider - apiKey="phc_vhssDAMod5qIplznQ75Kdgz4aB1qPFmeVmfEOZ4hkRw" - options={options} - > - {chartDiv} - </PostHogProvider> - ); - } - - return chartDiv; - } else - return ( - <div className="absolute inset-0 flex items-center justify-center z-[100]"> - <svg - className="animate-spin h-20 w-20 text-white" - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - > - <circle - className="opacity-25" - cx="12" - cy="12" - r="10" - stroke="currentColor" - strokeWidth="4" - ></circle> - <path - className="opacity-75" - fill="currentColor" - d="M4 12a8 8 0 018-8v8z" - ></path> - </svg> - </div> - ); + const [data, setData] = useState( + process.env.NODE_ENV === "production" ? null : candlestickMockup, + ); + const [options, setOptions] = useState({}); + + useEffect(() => { + if (process.env.NODE_ENV === "production") { + const interval = setInterval(() => { + if (window.json_data) { + const data = window.json_data; + console.log(data); + setData(data); + clearInterval(interval); + } + }, 100); + return () => clearInterval(interval); + } + }, []); + + const transformData = (data: any) => { + if (!data) return null; + const globals = { + added_traces: [], + csv_yaxis_id: null, + cmd_src_idx: null, + cmd_idx: null, + cmd_src: "", + old_margin: null, + title: "", + }; + const filename = data.layout?.title?.text + .replace(/ -/g, "") + .replace(/-/g, "") + .replace(/<b>|<\/b>/g, "") + .replace(/ /g, "_"); + const date = new Date().toISOString().slice(0, 10).replace(/-/g, ""); + const time = new Date().toISOString().slice(11, 19).replace(/:/g, ""); + window.title = `openbb_${filename}_${date}_${time}`; + + if (data.layout.annotations !== undefined) { + data.layout.annotations.forEach(function (annotation) { + if (annotation.text !== undefined) + if (annotation.text[0] === "/") { + globals.cmd_src = annotation.text; + globals.cmd_idx = data.layout.annotations.indexOf(annotation); + annotation.text = ""; + + const margin = data.layout.margin; + globals.old_margin = { ...margin }; + if (margin.t !== undefined && margin.t > 40) margin.t = 40; + + if (data.cmd === "/stocks/candle") margin.r -= 50; + } + }); + } + + // We add spaces to all trace names, due to Fira Code font width issues + // to make sure that the legend is not cut off + data.data.forEach(function (trace) { + if (trace.name !== undefined) { + const name_length = trace.name.length; + trace.name = `${trace.name} `; + trace.hoverlabel = { + namelength: name_length, + }; + } + }); + + const title = data.layout?.title?.text || "Interactive Chart"; + globals.title = title; + return { + data: data, + date: new Date(), + globals: globals, + cmd: data.command_location, + posthog: data.posthog, + python_version: data.python_version, + pywry_version: data.pywry_version, + terminal_version: data.terminal_version, + theme: data.theme, + title, + }; + }; + + const transformedData = transformData(data); + + if (transformedData) { + if (transformedData.posthog.collect_logs && !options) { + const opts = { + api_host: "https://app.posthog.com", + autocapture: { + css_selector_allowlist: [".ph-capture"], + }, + capture_pageview: false, + loaded: function (posthog: any) { + const log_id = transformedData?.log_id || ""; + + if (log_id !== "" && log_id !== "REPLACE_ME") + posthog.identify(log_id); + + posthog.onFeatureFlags(function () { + if ( + !posthog.isFeatureEnabled("record-pywry", { send_event: false }) + ) + posthog.stopSessionRecording(); + + if ( + !posthog.isFeatureEnabled("collect-logs-pywry", { + send_event: false, + }) + ) + posthog.opt_out_capturing(); + else if (posthog.has_opted_out_capturing()) + posthog.opt_in_capturing(); + }); + }, + }; + setOptions(opts); + } + + const info = { + INFO: { + command: transformedData.cmd, + title: transformedData.title, + date: transformedData.date, + python_version: transformedData.python_version, + pywry_version: transformedData.pywry_version, + terminal_version: transformedData.terminal_version, + }, + }; + + const chartDiv = ( + <Chart + json={transformedData.data} + date={transformedData.date} + cmd={transformedData.cmd} + title={transformedData.title} + globals={transformedData.globals} + theme={transformedData.theme} + info={info} + /> + ); + + if (transformedData.posthog.collect_logs && options) { + return ( + <PostHogProvider + apiKey="phc_vhssDAMod5qIplznQ75Kdgz4aB1qPFmeVmfEOZ4hkRw" + options={options} + > + {chartDiv} + </PostHogProvider> + ); + } + + return chartDiv; + } else + return ( + <div className="absolute inset-0 flex items-center justify-center z-[100]"> + <svg + className="animate-spin h-20 w-20 text-white" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + > + <circle + className="opacity-25" + cx="12" + cy="12" + r="10" + stroke="currentColor" + strokeWidth="4" + /> + <path + className="opacity-75" + fill="currentColor" + d="M4 12a8 8 0 018-8v8z" + /> + </svg> + </div> + ); } export default App; diff --git a/frontend-components/plotly/src/components/Chart.tsx b/frontend-components/plotly/src/components/Chart.tsx index f83da6d388d..da8304a7654 100644 --- a/frontend-components/plotly/src/components/Chart.tsx +++ b/frontend-components/plotly/src/components/Chart.tsx @@ -19,499 +19,612 @@ import DownloadFinishedDialog from "./Dialogs/DownloadFinishedDialog"; const Plot = createPlotlyComponent(Plotly); +function CreateDataXrangeChunks(data: Plotly.PlotData[], xrange?: any) { + const chunks = []; + let chunk = []; + const XDATA = data.filter( + (trace) => + trace.x !== undefined && trace.x.length > 0 && trace.x[0] !== undefined, + ); + const xaxis = XDATA[0]?.x ? XDATA[0].x : XDATA[1].x ? XDATA[1].x : []; + for (let i = 0; i < xaxis.length; i++) { + if (xaxis[i] >= xrange[0] && xaxis[i] <= xrange[1]) { + chunk.push(i); + } else if (chunk.length > 0) { + chunks.push(chunk); + chunk = []; + } + } + + if (chunk.length > 0) chunks.push(chunk); + return chunks; +} + +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 chunks = CreateDataXrangeChunks(data, xrange); + const new_data = []; + chunks.forEach((chunk) => { + data.forEach((trace) => { + const new_trace = { ...trace }; + const data_keys = ["x", "y", "low", "high", "open", "close", "text"]; + data_keys.forEach((key) => { + if (trace[key] && Array.isArray(trace[key])) { + new_trace[key] = trace[key].filter((_, i) => chunk.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) => + chunk.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, + json, + date, + cmd, + title, + globals, + theme, + info, }: { - // @ts-ignore - json: Figure; - date: Date; - cmd: string; - title: string; - globals: any; - theme: string; - info?: any; + // @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); - }, []); - - delete json.layout.width; - delete json.layout.height; - if (json.layout?.title?.text) { - json.layout.title.text = ""; - } - - const [barButtons, setModeBarButtons] = useState({}); - const [LogYaxis, setLogYaxis] = useState(false); - const [chartTitle, setChartTitle] = useState(title); - const [axesTitles, setAxesTitles] = useState({}); - const [plotLoaded, setPlotLoaded] = useState(false); - const [modal, setModal] = useState({ 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 [plotData, setPlotData] = useState(json); - const [annotations, setAnnotations] = useState([]); - const [changeTheme, setChangeTheme] = useState(false); - const [darkMode, setDarkMode] = useState(true); - const [autoScale, setAutoScaling] = useState(false); - const [changeColor, setChangeColor] = useState(false); - const [colorActive, setColorActive] = useState(false); - const [onAnnotationClick, setOnAnnotationClick] = useState({}); - const [ohlcAnnotation, setOhlcAnnotation] = 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 - let shapes = plotDiv.layout.shapes; - if (!shapes || shapes.length == 0) { - return; - } - // we change last added shape color - let last_shape = shapes[shapes.length - 1]; - last_shape.line.color = color; - Plotly.update(plotDiv, {}, { shapes: shapes }); - } - - function button_pressed(title, active = false) { - // changes the style of the button when it is pressed - // title is the title of the button - // active is true if the button is active, false otherwise - - let button = - barButtons[title] || document.querySelector(`[data-title="${title}"]`); - if (!active) { - button.style.border = "1px solid rgba(0, 151, 222, 1.0)"; - button.style.borderRadius = "5px"; - button.style.borderpadding = "5px"; - button.style.boxShadow = "0 0 5px rgba(0, 151, 222, 1.0)"; - } else { - button.style.border = "transparent"; - button.style.boxShadow = "none"; - } - setModeBarButtons({ ...barButtons, [title]: button }); - } - - function autoscaleButton() { - // We need to check if the button is active or not - let title = "Auto Scale (Ctrl+Shift+A)"; - let button = - barButtons[title] || document.querySelector(`[data-title="${title}"]`); - let active = true; - - if (button.style.border == "transparent") { - active = false; - plotDiv.on( - "plotly_relayout", - non_blocking(async function (eventdata) { - if (eventdata["xaxis.range[0]"] == undefined) return; - - let to_update = await autoScaling(eventdata, plotDiv); - Plotly.update(plotDiv, {}, to_update); - }, 100) - ); - } - // If the button isn't active, we remove the listener so - // the graphs don't autoscale anymore - else plotDiv.removeAllListeners("plotly_relayout"); - - button_pressed(title, active); - } - - function changecolorButton() { - // We need to check if the button is active or not - let title = "Edit Color (Ctrl+E)"; - let button = - barButtons[title] || document.querySelector(`[data-title="${title}"]`); - let active = true; - - if (button.style.border == "transparent") { - active = false; - } - - setColorActive(!active); - button_pressed(title, active); - } - - useEffect(() => { - if (autoScale) { - let scale = !autoScale; - console.log("activateAutoScale", scale); - autoscaleButton(); - setAutoScaling(false); - } - }, [autoScale]); - - useEffect(() => { - if (changeColor) { - changecolorButton(); - setChangeColor(false); - } - }, [changeColor]); - - useEffect(() => { - if (changeTheme) { - try { - console.log("changeTheme", changeTheme); - const TRACES = plotData?.data.filter((trace) => - trace?.name?.startsWith("Volume") - ); - 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") - trace.marker.color = trace.marker.color.map((color) => { - return volumeColors[color] || color; - }); - }); - plotData.layout.template = darkmode - ? DARK_CHARTS_TEMPLATE - : LIGHT_CHARTS_TEMPLATE; - setPlotData({ ...plotData }); - Plotly.react(plotDiv, plotData.data, plotData.layout); - setDarkMode(darkmode); - setChangeTheme(false); - } catch (e) { - console.log("error", e); - } - } - }, [changeTheme]); - - useEffect(() => { - if (plotLoaded) { - setDarkMode(true); - setAutoScaling(false); - const captureButtons = [ - "Download CSV", - "Download Chart as Image", - "Overlay chart from CSV", - "Add Text", - "Change Titles", - "Auto Scale (Ctrl+Shift+A)", - "Reset Axes", - ]; - const autoscale = document.querySelector('[data-title="Autoscale"]'); - if (autoscale) { - autoscale - .getElementsByTagName("path")[0] - .setAttribute("d", PlotlyIcons.home.path); - autoscale.setAttribute("data-title", "Reset Axes"); - } - - window.MODEBAR = document.getElementsByClassName( - "modebar-container" - )[0] as HTMLElement; - const modeBarButtons = window.MODEBAR.getElementsByClassName( - "modebar-btn" - ) as HTMLCollectionOf<HTMLElement>; - - window.MODEBAR.style.cssText = `${window.MODEBAR.style.cssText}; display:flex;`; - - if (modeBarButtons) { - let barbuttons: any = {}; - for (let i = 0; i < modeBarButtons.length; i++) { - let btn = modeBarButtons[i]; - if (captureButtons.includes(btn.getAttribute("data-title"))) { - btn.classList.add("ph-capture"); - } - btn.style.border = "transparent"; - barbuttons[btn.getAttribute("data-title")] = btn; - } - setModeBarButtons(barbuttons); - } - - if (plotData.layout.yaxis.type != undefined) { - if (plotData.layout.yaxis.type == "log" && !LogYaxis) { - console.log("yaxis.type changed to log"); - setLogYaxis(true); - - // We update the yaxis exponent format to SI, - // set the tickformat to '.0s' and the exponentbase to 100 - let layout_update = { - "yaxis.exponentformat": "SI", - "yaxis.tickformat": ".0s", - "yaxis.exponentbase": 100, - }; - Plotly.update(plotDiv, layout_update); - } - if (plotData.layout.yaxis.type == "linear" && LogYaxis) { - console.log("yaxis.type changed to linear"); - setLogYaxis(false); - - // We update the yaxis exponent format to none, - // set the tickformat to null and the exponentbase to 10 - let layout_update = { - "yaxis.exponentformat": "none", - "yaxis.tickformat": null, - "yaxis.exponentbase": 10, - }; - Plotly.update(plotDiv, layout_update); - } - } - - // We check to see if window.export_image is defined - if (window.export_image != undefined) { - // We get the extension of the file and check if it is valid - let filename = window.export_image.split("/").pop(); - const extension = filename.split(".").pop().replace("jpg", "jpeg"); - - if (["jpeg", "png", "svg", "pdf"].includes(extension)) - non_blocking(async function () { - await hideModebar(); - await saveImage("MainChart", filename.split(".")[0], extension); - }, 2)(); - } - - window.addEventListener("resize", async function () { - let update = await ResizeHandler({ - plotData, - volumeBars, - setMaximizePlot, - }); - let layout_update = update.layout_update; - let newPlotData = update.plotData; - let volume_update = update.volume_update; - - if (Object.keys(layout_update).length > 0) { - setPlotData(newPlotData); - setVolumeBars(volume_update); - Plotly.relayout(plotDiv, layout_update); - } - }); - - if (theme !== "dark") { - setChangeTheme(true); - } - } - }, [plotLoaded]); - - return ( - <div className="relative h-full"> - {loading && ( - <div className="absolute inset-0 flex items-center justify-center z-[100]"> - <svg - className="animate-spin h-20 w-20 text-white" - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - > - <circle - className="opacity-25" - cx="12" - cy="12" - r="10" - stroke="currentColor" - strokeWidth="4" - ></circle> - <path - className="opacity-75" - fill="currentColor" - d="M4 12a8 8 0 018-8v8z" - ></path> - </svg> - </div> - )} - <div id="loading" className="saving"> - <div id="loading_text" className="loading_text"></div> - <div id="loader" className="loader"></div> - </div> - <OverlayChartDialog - addOverlay={(overlay) => { - console.log(overlay); - plotData.layout.showlegend = true; - setPlotData(overlay); - setPlotLoaded(false); - }} - plotlyData={plotData} - setLoading={setLoading} - open={modal.name === "overlayChart"} - close={onClose} - /> - <TitleChartDialog - updateTitle={(title) => setChartTitle(title)} - updateAxesTitles={(axesTitles) => setAxesTitles(axesTitles)} - defaultTitle={chartTitle} - plotlyData={plotData} - open={modal.name === "titleDialog"} - close={onClose} - /> - <TextChartDialog - popupData={modal.name === "textDialog" ? modal?.data : null} |