|
|
import { useState } from "react"; |
|
|
import CommonDialog, { styleDialog } from "../Dialogs/CommonDialog"; |
|
|
|
|
|
const reader = new FileReader(); |
|
|
|
|
|
const layout_defaults = { |
|
|
overlaying: "y", |
|
|
side: "left", |
|
|
tickfont: { size: 12 }, |
|
|
tickpadding: 5, |
|
|
showgrid: false, |
|
|
showline: false, |
|
|
showticklabels: true, |
|
|
showlegend: true, |
|
|
zeroline: false, |
|
|
anchor: "x", |
|
|
type: "linear", |
|
|
autorange: true, |
|
|
}; |
|
|
|
|
|
export default function OverlayChartDialog({ |
|
|
open, |
|
|
close, |
|
|
setLoading, |
|
|
addOverlay, |
|
|
plotlyData, |
|
|
}: { |
|
|
open: boolean; |
|
|
close: () => void; |
|
|
setLoading: (loading: boolean) => void; |
|
|
addOverlay: (data: any) => void; |
|
|
plotlyData: any; |
|
|
}) { |
|
|
const [traceType, setTraceType] = useState("scatter"); |
|
|
const [traceColor, setTraceColor] = useState("#FFDD00"); |
|
|
const [increasingColor, setIncreasingColor] = useState("#00ACFF"); |
|
|
const [decreasingColor, setDecreasingColor] = useState("#FF0000"); |
|
|
const [traceName, setTraceName] = useState(""); |
|
|
const [csvData, setCsvData] = useState<any[]>([]); |
|
|
const [csvColumns, setCsvColumns] = useState<string[]>([]); |
|
|
const [yaxisOptions, setYaxisOptions] = useState<any>({}); |
|
|
const optionIds = ["x", "open", "high", "low", "close"]; |
|
|
|
|
|
const traceTypes: any = { |
|
|
scatter: "Scatter (Line)", |
|
|
candlestick: "Candlestick", |
|
|
bar: "Bar", |
|
|
}; |
|
|
|
|
|
const [options, setOptions] = useState<any>({}); |
|
|
|
|
|
function onClose() { |
|
|
close(); |
|
|
setTraceType("scatter"); |
|
|
setTraceName(""); |
|
|
setCsvData([]); |
|
|
setCsvColumns([]); |
|
|
setOptions({}); |
|
|
} |
|
|
|
|
|
function onSubmit() { |
|
|
if (csvData.length === 0) { |
|
|
document.getElementById("csv_file")?.focus(); |
|
|
document |
|
|
.getElementById("csv_file") |
|
|
?.style.setProperty("border", "1px solid red"); |
|
|
document.getElementById("csv_file_warning")!.style.display = "block"; |
|
|
return; |
|
|
} |
|
|
const newPlotydata = CSVonSubmit({ |
|
|
csvData: csvData, |
|
|
plotlyData: plotlyData, |
|
|
yaxisOptions: yaxisOptions, |
|
|
traceType: traceType, |
|
|
traceColor: traceColor, |
|
|
traceName: traceName, |
|
|
options: options, |
|
|
increasingColor: increasingColor, |
|
|
decreasingColor: decreasingColor, |
|
|
}); |
|
|
addOverlay(newPlotydata); |
|
|
onClose(); |
|
|
} |
|
|
|
|
|
return ( |
|
|
<CommonDialog |
|
|
title="Overlay Chart" |
|
|
description="Upload a CSV file to overlay a chart on the main chart." |
|
|
open={open} |
|
|
close={close} |
|
|
> |
|
|
<div id="popup_csv" className="popup_content"> |
|
|
<div> |
|
|
<label htmlFor="csv_file"> |
|
|
<b>CSV file:</b> |
|
|
<div |
|
|
id="csv_file_warning" |
|
|
className="popup_warning" |
|
|
style={{ marginLeft: "80px", marginBottom: "10px" }} |
|
|
> |
|
|
CSV file is required. |
|
|
</div> |
|
|
</label> |
|
|
<input |
|
|
onChange={(e) => { |
|
|
if (!e.target.files) { |
|
|
return; |
|
|
} else if (e.target.files[0].type !== "text/csv") { |
|
|
document.getElementById("csv_file")?.focus(); |
|
|
document |
|
|
.getElementById("csv_file") |
|
|
?.style.setProperty("border", "1px solid red"); |
|
|
document.getElementById("csv_file_warning")!.style.display = |
|
|
"block"; |
|
|
return; |
|
|
} |
|
|
|
|
|
if (csvColumns.length > 0) { |
|
|
setCsvColumns([]); |
|
|
setOptions({}); |
|
|
setTraceType("scatter"); |
|
|
} |
|
|
|
|
|
reader.onload = (filebytes) => { |
|
|
if ( |
|
|
!filebytes.target?.result || |
|
|
typeof filebytes.target.result !== "string" |
|
|
) { |
|
|
return; |
|
|
} |
|
|
const lines = filebytes.target.result |
|
|
.split("\n") |
|
|
.map((x) => x.replace(/\r/g, "")); |
|
|
|
|
|
const headers = lines[0].split(","); |
|
|
const headers_lower = headers.map((x) => |
|
|
x.trim().toLowerCase(), |
|
|
); |
|
|
|
|
|
const updateOptions: { [key: string]: any } = {}; |
|
|
|
|
|
if (headers.length > 1) { |
|
|
updateOptions.x = headers[0]; |
|
|
updateOptions.y = headers[1]; |
|
|
} |
|
|
|
|
|
for (let i = 0; i < optionIds.length; i++) { |
|
|
if (headers_lower.includes(optionIds[i])) { |
|
|
updateOptions[optionIds[i]] = |
|
|
headers[headers_lower.indexOf(optionIds[i])]; |
|
|
} else if ( |
|
|
optionIds[i] === "x" && |
|
|
headers_lower.includes("date") |
|
|
) { |
|
|
updateOptions[optionIds[i]] = |
|
|
headers[headers_lower.indexOf("date")]; |
|
|
} |
|
|
} |
|
|
|
|
|
const candle_cols = ["open", "high", "low", "close"]; |
|
|
const candle_cols_present = candle_cols.every((x) => |
|
|
headers_lower.includes(x), |
|
|
); |
|
|
if (candle_cols_present) { |
|
|
setTraceType("candlestick"); |
|
|
} else if (headers_lower.length >= 5) { |
|
|
candle_cols.forEach((x) => { |
|
|
updateOptions[x] = headers[candle_cols.indexOf(x) + 1]; |
|
|
}); |
|
|
} |
|
|
|
|
|
if (headers_lower.includes("close")) { |
|
|
setOptions({ |
|
|
...options, |
|
|
y: headers[headers_lower.indexOf("close")], |
|
|
}); |
|
|
updateOptions.y = headers[headers_lower.indexOf("close")]; |
|
|
} |
|
|
|
|
|
const data = []; |
|
|
|
|
|
for (let i = 1; i < lines.length; i++) { |
|
|
const obj = {}; |
|
|
const currentline = lines[i].split(","); |
|
|
for (let j = 0; j < headers.length; j++) { |
|
|
//@ts-ignore |
|
|
obj[headers[j]] = currentline[j]; |
|
|
} |
|
|
data.push(obj); |
|
|
} |
|
|
|
|
|
//@ts-ignore |
|
|
let filename = e.target.files[0].name.split(".")[0]; |
|
|
|
|
|
try { |
|
|
if (filename.includes("_")) { |
|
|
const name_parts = filename |
|
|
.replace(/_{2,}/g, "_") |
|
|
.split("_"); |
|
|
const date_regex = new RegExp("^[0-9]{8}$"); |
|
|
|
|
|
if (name_parts.length > 2) { |
|
|
// we check if the first 2 parts are date and time |
|
|
if (date_regex.test(name_parts[0])) { |
|
|
name_parts.splice(0, 2); |
|
|
} |
|
|
// we check if the last 2 parts are date and time |
|
|
else if ( |
|
|
date_regex.test(name_parts[name_parts.length - 2]) |
|
|
) { |
|
|
name_parts.splice(name_parts.length - 2, 2); |
|
|
} |
|
|
filename = name_parts.join("_").replace(/openbb_/g, ""); |
|
|
} |
|
|
} |
|
|
} catch (e) { |
|
|
console.log(e); |
|
|
} |
|
|
|
|
|
setTraceName(filename); |
|
|
setOptions(updateOptions); |
|
|
setCsvColumns(headers); |
|
|
setCsvData(data); |
|
|
}; |
|
|
reader.readAsText(e.target.files[0]); |
|
|
}} |
|
|
type="file" |
|
|
id="csv_file" |
|
|
accept=".csv" |
|
|
style={{ marginLeft: 10 }} |
|
|
/> |
|
|
</div> |
|
|
<div style={{ marginTop: 15 }}> |
|
|
<label htmlFor="csv_trace_type"> |
|
|
<b>Display data type:</b> |
|
|
</label> |
|
|
<select |
|
|
onChange={(e) => { |
|
|
setTraceType(e.target.value); |
|
|
}} |
|
|
id="csv_trace_type" |
|
|
style={styleDialog} |
|
|
defaultValue={traceTypes[traceType]} |
|
|
> |
|
|
{traceType && ( |
|
|
<option key={traceType} value={traceType}> |
|
|
{traceTypes[traceType]} |
|
|
</option> |
|
|
)} |
|
|
{Object.keys(traceTypes).map( |
|
|
(x) => |
|
|
traceType !== x && ( |
|
|
<option key={x} value={x}> |
|
|
{traceTypes[x]} |
|
|
</option> |
|
|
), |
|
|
)} |
|
|
</select> |
|
|
</div> |
|
|
<div style={{ marginTop: 12 }}> |
|
|
<label htmlFor="csv_name"> |
|
|
<b>Trace Name:</b> |
|
|
</label> |
|
|
<textarea |
|
|
id="csv_name" |
|
|
value={traceName} |
|
|
onChange={(e) => { |
|
|
setTraceName(e.target.value); |
|
|
}} |
|
|
style={{ |
|
|
padding: "5px 2px 2px 5px", |
|
|
width: "100%", |
|
|
maxWidth: "100%", |
|
|
maxHeight: 200, |
|
|
marginTop: 2, |
|
|
}} |
|
|
rows={2} |
|
|
cols={20} |
|
|
placeholder="Enter a name to give this trace" |
|
|
/> |
|
|
</div> |
|
|
{csvColumns.length > 0 && ( |
|
|
<> |
|
|
{["scatter", "bar"].includes(traceType) && ( |
|
|
<div |
|
|
style={{ marginTop: 15, marginBottom: 10 }} |
|
|
id="csv_columns" |
|
|
className="csv_column_container" |
|
|
> |
|
|
{["x", "y"].map((key) => ( |
|
|
<div |
|
|
key={key} |
|
|
style={{ |
|
|
marginTop: 10, |
|
|
display: "flex", |
|
|
alignItems: "center", |
|
|
justifyContent: "space-between", |
|
|
}} |
|
|
> |
|
|
<label htmlFor={`csv_${key}`} style={{ width: "100px" }}> |
|
|
{key.toUpperCase()} Axis |
|
|
</label> |
|
|
<select |
|
|
onChange={(e) => { |
|
|
setOptions({ |
|
|
...options, |
|
|
[key]: e.target.value, |
|
|
}); |
|
|
}} |
|
|
id={`csv_${key}`} |
|
|
style={{ width: "100%" }} |
|
|
defaultValue={options[key]} |
|
|
> |
|
|
{csvColumns.map((column) => ( |
|
|
<option key={column} value={column}> |
|
|
{column} |
|
|
</option> |
|
|
))} |
|
|
</select> |
|
|
</div> |
|
|
))} |
|
|
</div> |
|
|
)} |
|
|
{traceType === "candlestick" && ( |
|
|
<div |
|
|
id="csv_columns" |
|
|
className="csv_column_container" |
|
|
style={{ marginTop: 15 }} |
|
|
> |
|
|
{["x", "open", "high", "low", "close"].map((key) => ( |
|
|
<div |
|
|
key={key} |
|
|
style={{ |
|
|
marginTop: 10, |
|
|
display: "flex", |
|
|
alignItems: "center", |
|
|
justifyContent: "space-between", |
|
|
}} |
|
|
> |
|
|
<label htmlFor={`csv_${key}`} style={{ width: "100px" }}> |
|
|
{key.charAt(0).toUpperCase() + key.slice(1)} |
|
|
</label> |
|
|
<select |
|
|
onChange={(e) => { |
|
|
setOptions({ |
|
|
...options, |
|
|
[key]: e.target.value, |
|
|
}); |
|
|
}} |
|
|
id={`csv_${key}`} |
|
|
style={{ width: "100%" }} |
|
|
defaultValue={options[key]} |
|
|
> |
|
|
{csvColumns.map((column) => ( |
|
|
<option key={column} value={column}> |
|
|
{column} |
|
|
</option> |
|
|
))} |
|
|
</select> |
|
|
</div> |
|
|
))} |
|
|
</div> |
|
|
)} |
|
|
<div style={{ marginTop: 20 }} id="csv_colors"> |
|
|
{["scatter", "bar"].includes(traceType) && ( |
|
|
<div> |
|
|
<label htmlFor="csv_color">{`${traceType |
|
|
.charAt(0) |
|
|
.toUpperCase()}${traceType.slice(1)} color`}</label> |
|
|
<input |
|
|
type="color" |
|
|
id="csv_color" |
|
|
defaultValue="#FFDD00" |
|
|
style={{ margin: "2px 2px 2px 10px" }} |
|
|
onChange={(e) => { |
|
|
console.log(e.target.value); |
|
|
setTraceColor(e.target.value); |
|
|
}} |
|
|
/> |
|
|
</div> |
|
|
)} |
|
|
{traceType === "candlestick" && ( |
|
|
<> |
|
|
<label htmlFor="csv_increasing">Increasing color</label> |
|
|
<input |
|
|
type="color" |
|
|
id="csv_increasing" |
|
|
defaultValue="#00ACFF" |
|
|
style={{ margin: "2px 0px 2px 10px" }} |
|
|
onChange={(e) => { |
|
|
setIncreasingColor(e.target.value); |
|
|
}} |
|
|
/> |
|
|
<label htmlFor="csv_decreasing" style={{ marginLeft: 15 }}> |
|
|
Decreasing color |
|
|
</label> |
|
|
<input |
|
|
style={{ margin: "2px 0px 2px 10px" }} |
|
|
type="color" |
|
|
id="csv_decreasing" |
|
|
defaultValue="#FF0000" |
|
|
onChange={(e) => { |
|
|
setDecreasingColor(e.target.value); |
|
|
}} |
|
|
/> |
|
|
</> |
|
|
)} |
|
|
</div> |
|
|
<div style={{ marginTop: 20 }} id="csv_plot_yaxis_options"> |
|
|
{traceType !== "candlestick" && ( |
|
|
<> |
|
|
<input |
|
|
type="checkbox" |
|
|
id="csv_percent_change" |
|
|
name="csv_plot_yaxis_check" |
|
|
style={{ marginBottom: 2 }} |
|
|
onChange={(e) => { |
|
|
setYaxisOptions({ |
|
|
...yaxisOptions, |
|
|
percentChange: e.target.checked, |
|
|
sameYaxis: false, |
|
|
}); |
|
|
}} |
|
|
checked={ |
|
|
!yaxisOptions.sameYaxis && yaxisOptions.percentChange |
|
|
} |
|
|
/> |
|
|
<label htmlFor="csv_percent_change" style={{ marginLeft: 5 }}> |
|
|
Plot as percent change from first value |
|
|
</label> |
|
|
<br /> |
|
|
</> |
|
|
)} |
|
|
<input |
|
|
style={{ marginTop: 2 }} |
|
|
type="checkbox" |
|
|
id="csv_same_yaxis" |
|
|
name="csv_plot_yaxis_check" |
|
|
onChange={(e) => { |
|
|
setYaxisOptions({ |
|
|
...yaxisOptions, |
|
|
sameYaxis: e.target.checked, |
|
|
percentChange: false, |
|
|
}); |
|
|
}} |
|
|
checked={!yaxisOptions.percentChange && yaxisOptions.sameYaxis} |
|
|
/> |
|
|
<label htmlFor="csv_same_yaxis" style={{ marginLeft: 5 }}> |
|
|
Share Y-axis |
|
|
</label> |
|
|
|
|
|
{traceType === "bar" && ( |
|
|
<div style={{ marginTop: 2 }} id="csv_bar_orientation"> |
|
|
<input |
|
|
type="checkbox" |
|
|
id="csv_bar_horizontal" |
|
|
onChange={(e) => { |
|
|
setOptions({ |
|
|
...options, |
|
|
orientation: e.target.checked ? "h" : "v", |
|
|
}); |
|
|
}} |
|
|
/> |
|
|
<label htmlFor="csv_bar_horizontal" style={{ marginLeft: 5 }}> |
|
|
Plot horizontally |
|
|
</label> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</> |
|
|
)} |
|
|
|
|
|
<br /> |
|
|
<div style={{ float: "right", marginTop: 20 }}> |
|
|
<button className="_btn-tertiary" id="csv_cancel" onClick={onClose}> |
|
|
Cancel |
|
|
</button> |
|
|
<button className="_btn" id="csv_submit" onClick={onSubmit}> |
|
|
Submit |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</CommonDialog> |
|
|
); |
|
|
} |
|
|
|
|
|
export function CSVonSubmit({ |
|
|
csvData, |
|
|
plotlyData, |
|
|
yaxisOptions, |
|
|
traceType, |
|
|
traceColor, |
|
|
traceName, |
|
|
options, |
|
|
increasingColor, |
|
|
decreasingColor, |
|
|
}: { |
|
|
csvData: any[]; |
|
|
plotlyData: any; |
|
|
yaxisOptions: any; |
|
|
traceType: string; |
|
|
traceColor: string; |
|
|
traceName: string; |
|
|
options: any; |
|
|
increasingColor: string; |
|
|
decreasingColor: string; |
|
|
}) { |
|
|
console.log("options", options); |
|
|
const main_trace = plotlyData.data[0] || {}; |
|
|
if (main_trace.xaxis === undefined) { |
|
|
main_trace.xaxis = "x"; |
|
|
} |
|
|
if (main_trace.yaxis === undefined) { |
|
|
main_trace.yaxis = "y"; |
|
|
} |
|
|
let yaxis_id = main_trace.yaxis; |
|
|
let yaxis: string; |
|
|
|
|
|
const left_yaxis_ticks = Object.keys(plotlyData.layout) |
|
|
.filter((k) => k.startsWith("yaxis")) |
|
|
.map((k) => plotlyData.layout[k]) |
|
|
.filter( |
|
|
(yaxis) => |
|
|
yaxis.side === "left" && |
|
|
(yaxis.overlaying === "y" || |
|
|
(yaxis.fixedrange !== undefined && yaxis.fixedrange === true)), |
|
|
).length; |
|
|
|
|
|
const ticksuffix = left_yaxis_ticks > 0 ? " " : ""; |
|
|
|
|
|
if (yaxisOptions.sameYaxis !== true) { |
|
|
const yaxes = Object.keys(plotlyData.layout) |
|
|
.filter((k) => k.startsWith("yaxis")) |
|
|
.map((k) => plotlyData.layout[k]); |
|
|
|
|
|
yaxis = `y${yaxes.length + 1}`; |
|
|
yaxis_id = `yaxis${yaxes.length + 1}`; |
|
|
plotlyData.layout[yaxis_id] = { |
|
|
...layout_defaults, |
|
|
title: { |
|
|
text: traceName, |
|
|
font: { |
|
|
size: 14, |
|
|
}, |
|
|
standoff: 0, |
|
|
}, |
|
|
ticksuffix: ticksuffix, |
|
|
layer: "below traces", |
|
|
}; |
|
|
} else { |
|
|
|
|
|
yaxis = main_trace.yaxis.replace("yaxis", "y"); |
|
|
} |
|
|
|
|
|
const traceBase: any = { |
|
|
type: traceType, |
|
|
name: traceName, |
|
|
showlegend: true, |
|
|
yaxis: yaxis, |
|
|
}; |
|
|
|
|
|
let trace: any = {}; |
|
|
|
|
|
if (["scatter", "bar"].includes(traceType)) { |
|
|
if (!csvData || csvData.length === 0) return plotlyData; |
|
|
const non_null = csvData.findIndex( |
|
|
(x: any) => x[options.y] !== null && x[options.y] !== 0, |
|
|
); |
|
|
|
|
|
if (non_null === -1) { |
|
|
return plotlyData; |
|
|
} |
|
|
|
|
|
const scatter_data: { [key: string]: any[] } = { |
|
|
x: [], |
|
|
y: [], |
|
|
customdata: [], |
|
|
}; |
|
|
|
|
|
csvData.forEach((row: any) => { |
|
|
let y = row[options.y]; |
|
|
scatter_data.customdata.push(y); |
|
|
if ( |
|
|
yaxisOptions.percentChange && |
|
|
(traceType === "scatter" || traceType === "line") |
|
|
) { |
|
|
y = |
|
|
(row[options.y] - csvData[non_null][options.y]) / |
|
|
csvData[non_null][options.y]; |
|
|
} |
|
|
scatter_data.x.push(row[options.x]); |
|
|
scatter_data.y.push(y); |
|
|
}); |
|
|
|
|
|
trace = { |
|
|
...traceBase, |
|
|
x: scatter_data.x, |
|
|
y: scatter_data.y, |
|
|
customdata: scatter_data.customdata, |
|
|
hovertemplate: "%{customdata:.2f}", |
|
|
connectgaps: true, |
|
|
marker: { color: traceColor }, |
|
|
}; |
|
|
|
|
|
if (traceType === "bar") { |
|
|
trace.orientation = options.orientation; |
|
|
trace.marker.opacity = 0.7; |
|
|
trace.connectgaps = undefined; |
|
|
trace.hovertemplate = undefined; |
|
|
trace.customdata = undefined; |
|
|
} |
|
|
} else if (traceType === "candlestick") { |
|
|
const candlestick_data: { [key: string]: any[] } = { |
|
|
x: [], |
|
|
open: [], |
|
|
high: [], |
|
|
low: [], |
|
|
close: [], |
|
|
}; |
|
|
|
|
|
csvData.forEach((row: any) => { |
|
|
candlestick_data.x.push(row[options.x]); |
|
|
candlestick_data.open.push(row[options.open]); |
|
|
candlestick_data.high.push(row[options.high]); |
|
|
candlestick_data.low.push(row[options.low]); |
|
|
candlestick_data.close.push(row[options.close]); |
|
|
}); |
|
|
|
|
|
trace = { |
|
|
...traceBase, |
|
|
x: candlestick_data.x, |
|
|
open: candlestick_data.open, |
|
|
high: candlestick_data.high, |
|
|
low: candlestick_data.low, |
|
|
close: candlestick_data.close, |
|
|
increasing: { |
|
|
line: { color: increasingColor, width: 0.8 }, |
|
|
fillcolor: increasingColor, |
|
|
}, |
|
|
decreasing: { |
|
|
line: { color: decreasingColor, width: 0.8 }, |
|
|
fillcolor: decreasingColor, |
|
|
}, |
|
|
}; |
|
|
} |
|
|
|
|
|
return { |
|
|
...plotlyData, |
|
|
data: [...plotlyData.data, trace], |
|
|
}; |
|
|
} |
|
|
|