Hotfix/ improve tables/charts (#5228)

* improve charts, tables

* update

---------

Co-authored-by: James Maslek <jmaslek11@gmail.com>
This commit is contained in:
teh_coderer
2023-07-19 18:58:32 -05:00
committed by GitHub
parent 8b007785a1
commit 41ef3d416f
18 changed files with 1685 additions and 5532 deletions

View File

@@ -19,7 +19,7 @@ declare global {
}
function App() {
const [data, setData] = useState(
const [json_data, setData] = useState(
process.env.NODE_ENV === "production" ? null : candlestickMockup,
);
const [options, setOptions] = useState({});
@@ -28,9 +28,9 @@ function App() {
if (process.env.NODE_ENV === "production") {
const interval = setInterval(() => {
if (window.json_data) {
const data = window.json_data;
console.log(data);
setData(data);
const plotly_json = window.json_data;
console.log(plotly_json);
setData(plotly_json);
clearInterval(interval);
}
}, 100);
@@ -79,10 +79,8 @@ function App() {
// 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,
namelength: -1,
};
}
});
@@ -103,7 +101,7 @@ function App() {
};
};
const transformedData = transformData(data);
const transformedData = transformData(json_data);
if (transformedData) {
if (transformedData.posthog.collect_logs && !options) {

View File

@@ -1,6 +1,20 @@
//@ts-nocheck
import { Figure } from "react-plotly.js";
export const isoDateRegex = new RegExp(
"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}",
);
function merge(target, source) {
Object.keys(source).forEach((key) => {
if (typeof source[key] === "object") {
Object.assign(source[key], merge(target[key], source[key]));
}
});
Object.assign(target || {}, source);
return target;
}
export default async function autoScaling(
eventdata: Readonly<Plotly.PlotRelayoutEvent>,
graphs: Figure,
@@ -9,10 +23,19 @@ export default async function autoScaling(
if (eventdata["xaxis.range[0]"] !== undefined) {
const x_min = eventdata["xaxis.range[0]"];
const x_max = eventdata["xaxis.range[1]"];
let x0_min = x_min;
let x1_max = x_max;
if (isoDateRegex.test(x_min.replace(" ", "T").split(".")[0])) {
x0_min = new Date(x_min.replace(" ", "T").split(".")[0]);
x1_max = new Date(x_max.replace(" ", "T").split(".")[0]);
}
const to_update = {};
const yaxis_fixedrange = [];
let y_min;
let y_max;
let y_min: number;
let y_max: number;
let min_xrange: any;
const get_all_yaxis_traces = {};
const get_all_yaxis_annotations = {};
@@ -20,12 +43,14 @@ export default async function autoScaling(
const yaxis_unique = [
...new Set(
graphs.data.map((trace) => {
if (
trace.yaxis ||
trace.y !== undefined ||
trace.type === "candlestick"
) {
graphs.data.map((trace: Plotly.PlotData) => {
if (trace.y !== undefined || trace.type === "candlestick") {
if (
trace.yaxis === undefined &&
trace?.name?.trim() !== "Volume"
) {
trace.yaxis = "y";
}
if (trace.type === "bar" && trace?.name?.trim() === "Volume") {
volumeTraceYaxis = `yaxis${trace.yaxis.replace("y", "")}`;
}
@@ -38,7 +63,7 @@ export default async function autoScaling(
),
];
graphs.layout.annotations.map((annotation, i) => {
graphs.layout.annotations.map((annotation: any, i: number) => {
if (annotation.yref !== undefined && annotation.yref !== "paper") {
annotation.index = i;
const yaxis = `yaxis${annotation.yref.replace("y", "")}`;
@@ -73,16 +98,25 @@ export default async function autoScaling(
}
}
const yx_values = x.map((x, i) => {
let out = null;
const isoDateRegex = new RegExp(
"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}",
);
if (isoDateRegex.test(x)) {
const x_time = new Date(x).getTime();
const x0_min = new Date(x_min.replace(" ", "T")).getTime();
const x1_max = new Date(x_max.replace(" ", "T")).getTime();
if (x_time >= x0_min && x_time <= x1_max) {
const yx_values = x.map(
(x: string | number | Date, i: string | number) => {
let out = null;
if (isoDateRegex.test(x.toString())) {
const x_time = new Date(x).getTime();
if (x_time >= x0_min.getTime() && x_time <= x1_max.getTime()) {
if (trace2.y !== undefined && y[i] !== undefined) {
out = y[i];
}
if (trace2.type === "candlestick") {
y_candle.push(y_low[i]);
y_candle.push(y_high[i]);
}
if (!min_xrange || x_time < min_xrange) {
min_xrange = x_time;
}
}
} else if (x >= x_min && x <= x_max) {
if (trace2.y !== undefined) {
out = y[i];
}
@@ -90,28 +124,27 @@ export default async function autoScaling(
y_candle.push(y_low[i]);
y_candle.push(y_high[i]);
}
if (!min_xrange || x < min_xrange) {
min_xrange = x;
}
}
} else if (x >= x_min && x <= x_max) {
if (trace2.y !== undefined) {
out = y[i];
}
if (trace2.type === "candlestick") {
y_candle.push(y_low[i]);
y_candle.push(y_high[i]);
}
}
return out;
});
return out;
},
);
y_values = y_values.concat(yx_values);
});
y_values = y_values.filter((y2) => y2 !== undefined && y2 !== null);
y_values = y_values
.flat()
.filter((y2) => y2 !== undefined && y2 !== null);
y_min = Math.min(...y_values);
y_max = Math.max(...y_values);
if (y_candle.length > 0) {
y_candle = y_candle.filter((y2) => y2 !== undefined && y2 !== null);
y_candle = y_candle
.flat()
.filter((y2) => y2 !== undefined && y2 !== null);
y_min = Math.min(...y_candle);
y_max = Math.max(...y_candle);
}
@@ -127,12 +160,16 @@ export default async function autoScaling(
y_min -= y_range * y_mult;
y_max += y_range * y_mult;
if (to_update[yaxis] === undefined) {
to_update[yaxis] = {};
}
if (yaxis === volumeTraceYaxis) {
if (graphs.layout[yaxis].tickvals !== undefined) {
const range_x = 7;
const volume_ticks = org_y_max;
let round_digits = -3;
// @ts-ignore
let first_val = Math.round(volume_ticks * 0.2, round_digits);
const x_zipped = [2, 5, 6, 7, 8, 9, 10];
const y_zipped = [1, 4, 5, 6, 7, 8, 9];
@@ -140,6 +177,7 @@ export default async function autoScaling(
for (let i = 0; i < x_zipped.length; i++) {
if (String(volume_ticks).length > x_zipped[i]) {
round_digits = -y_zipped[i];
// @ts-ignore
first_val = Math.round(volume_ticks * 0.2, round_digits);
}
}
@@ -151,16 +189,16 @@ export default async function autoScaling(
];
const volume_range = [0, Math.floor(volume_ticks * range_x)];
to_update[`${yaxis}.tickvals`] = tickvals;
to_update[`${yaxis}.range`] = volume_range;
to_update[`${yaxis}.tickformat`] = ".2s";
to_update[yaxis].tickvals = tickvals;
to_update[yaxis].range = volume_range;
to_update[yaxis].tickformat = ".2s";
return;
}
y_min = 0;
y_max = graphs.layout[yaxis].range[1];
}
to_update[`${yaxis}.range`] = [y_min, y_max];
to_update[`${yaxis}.fixedrange`] = true;
to_update[yaxis].range = [y_min, y_max];
to_update[yaxis].fixedrange = true;
yaxis_fixedrange.push(yaxis);
if (get_all_yaxis_annotations[yaxis] !== undefined) {
@@ -172,14 +210,20 @@ export default async function autoScaling(
y_max - y_range * 0.2,
);
to_update[`annotations[${annotation.index}].ay`] = yshift_new;
if (to_update.annotations === undefined) {
to_update.annotations = graphs.layout.annotations;
}
to_update.annotations[annotation.index].ay = yshift_new;
}
});
}
}
});
return { to_update, yaxis_fixedrange };
graphs.layout = merge(graphs.layout, to_update);
return { to_update: graphs.layout, yaxis_fixedrange };
}
} catch (e) {
console.log(`Error in AutoScaling: ${e}`);

View File

@@ -5,11 +5,10 @@ import * as Plotly from "plotly.js-dist-min";
import { Icons as PlotlyIcons } from "plotly.js-dist-min";
import { usePostHog } from "posthog-js/react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import createPlotlyComponent from "react-plotly.js/factory";
import { init_annotation } from "../utils/addAnnotation";
import { non_blocking, saveImage } from "../utils/utils";
import autoScaling from "./AutoScaling";
import autoScaling, { isoDateRegex } from "./AutoScaling";
import ChangeColor from "./ChangeColor";
import { DARK_CHARTS_TEMPLATE, ICONS, LIGHT_CHARTS_TEMPLATE } from "./Config";
import AlertDialog from "./Dialogs/AlertDialog";
@@ -17,7 +16,7 @@ import DownloadFinishedDialog from "./Dialogs/DownloadFinishedDialog";
import OverlayChartDialog from "./Dialogs/OverlayChartDialog";
import TextChartDialog from "./Dialogs/TextChartDialog";
import TitleChartDialog from "./Dialogs/TitleChartDialog";
import { PlotConfig, hideModebar } from "./PlotlyConfig";
import { PlotConfig, hideModebar, ChartHotkeys } from "./PlotlyConfig";
import ResizeHandler from "./ResizeHandler";
const Plot = createPlotlyComponent(Plotly);
@@ -63,13 +62,43 @@ class PlotComponent extends React.Component {
}
}
function CreateDataXrange(data: Plotly.PlotData[], xrange?: any) {
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) {
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 };
@@ -83,25 +112,22 @@ function CreateDataXrange(data: Plotly.PlotData[], xrange?: any) {
"text",
"customdata",
];
const xaxis = trace.x ? trace.x : [];
const xaxis: any[] = trace.x ? trace.x : [];
const chunks = [];
for (let i = 0; i < xaxis.length; i++) {
const isoDateRegex = new RegExp(
"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}",
);
if (isoDateRegex.test(xaxis[i])) {
const x_time = new Date(xaxis[i]).getTime();
const x0_min = new Date(xrange[0].replace(" ", "T")).getTime();
const x1_max = new Date(xrange[1].replace(" ", "T")).getTime();
if (x_time >= x0_min && x_time <= x1_max) {
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 (xaxis[i] >= xrange[0] && xaxis[i] <= xrange[1]) {
} else if (xval >= xrange[0] && xval <= xrange[1]) {
chunks.push(i);
}
}
data_keys.forEach((key) => {
if (trace[key] && Array.isArray(trace[key])) {
if (trace[key] !== undefined && Array.isArray(trace[key])) {
new_trace[key] = trace[key].filter((_, i) => chunks.includes(i));
}
});
@@ -114,12 +140,22 @@ function CreateDataXrange(data: Plotly.PlotData[], xrange?: any) {
);
}
});
new_data.push(new_trace);
if (chunks.length > 0) new_data.push(new_trace);
});
if (new_data.length === 0) return data;
if (new_data.length === 0)
return {
...figure,
layout: {
...figure.layout,
xaxis: { ...figure.layout.xaxis, range: xrange },
},
};
return new_data;
new_figure.layout.xaxis.range = xrange;
new_figure.data = new_data;
return new_figure;
}
async function DynamicLoad({
@@ -144,14 +180,25 @@ async function DynamicLoad({
XDATA[0]?.x[XDATA[0].x.length - 1],
];
figure.data = CreateDataXrange(figure.data, xaxis_range);
figure.layout.xaxis.range = xaxis_range;
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,
@@ -196,7 +243,7 @@ function Chart({
const [downloadFinished, setDownloadFinished] = useState(false);
const [dateSliced, setDateSliced] = useState(false);
const [plotData, setPlotData] = useState(originalData);
const [plotData, setPlotDataState] = useState(originalData);
const [annotations, setAnnotations] = useState([]);
const [changeTheme, setChangeTheme] = useState(false);
const [darkMode, setDarkMode] = useState(true);
@@ -207,68 +254,17 @@ function Chart({
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: "" });
useHotkeys(
"ctrl+shift+t",
() => {
setModal({ name: "titleDialog" });
},
{ preventDefault: true },
);
useHotkeys(
"ctrl+t",
() => {
setModal({ name: "textDialog" });
},
{ preventDefault: true },
);
useHotkeys(
"ctrl+o",
() => {
setModal({ name: "overlayChart" });
},
{ preventDefault: true },
);
useHotkeys(
["ctrl+shift+h", "ctrl+h"],
() => {
hideModebar();
},
{ preventDefault: true },
);
useHotkeys(
"ctrl+e",
() => {
changeColor(true);
},
{ preventDefault: true },
);
useHotkeys(
"ctrl+shift+s",
async () => {
setModal({ name: "downloadCsv" });
await downloadCSV(
document.getElementById("plotlyChart") as any,
downloadFinished,
);
},
{ preventDefault: true },
);
useHotkeys(
"ctrl+s",
async () => {
hideModebar();
downloadImage("MainChart", hideModebar, Loading, downloadFinished);
},
{ preventDefault: true },
);
useHotkeys(
"ctrl+w",
() => {
window.close();
},
{ preventDefault: true },
);
// @ts-ignore
const onDeleteAnnotation = useCallback(
@@ -314,12 +310,6 @@ function Chart({
}
}, [downloadFinished]);
useEffect(() => {
if (plotDiv) {
Plotly.update(plotDiv, plotData.data, plotData.layout, plotData.config);
}
}, [plotData]);
useEffect(() => {
if (axesTitles && Object.keys(axesTitles).length > 0) {
Object.keys(axesTitles).forEach((k) => {
@@ -365,28 +355,27 @@ function Chart({
}
setModeBarButtons({ ...barButtons, [title]: button });
}
const debouncedDynamicLoad = useCallback(
debounce(async (eventData, figure) => {
if (dateSliced) {
const data = { ...figure };
await DynamicLoad({
event: eventData,
figure: data,
}).then(async (toUpdate) => {
setPlotData(toUpdate);
Plotly.react(plotDiv, toUpdate.data, toUpdate.layout);
const scaled = await autoScaling(eventData, plotDiv);
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);
Plotly.update(plotDiv, {}, scaled.to_update);
setPlotData({ ...toUpdate, layout: scaled.to_update });
});
} else {
const scaled = await autoScaling(eventData, plotDiv);
setYaxisFixedRange(scaled.yaxis_fixedrange);
Plotly.update(plotDiv, {}, scaled.to_update);
}
}, 150),
[setPlotData, plotDiv, setYaxisFixedRange, dateSliced],
);
});
} 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
@@ -398,13 +387,32 @@ function Chart({
if (button.style.border === "transparent") {
plotDiv.removeAllListeners("plotly_relayout");
active = false;
plotDiv.on(
"plotly_relayout",
debounce(async (eventdata) => {
if (eventdata["xaxis.range[0]"] === undefined) return;
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);
}, 500),
);
}, debounceTimer)();
});
}
// If the button isn't active, we remove the listener so
// the graphs don't autoscale anymore
@@ -420,7 +428,7 @@ function Chart({
debounce(async (eventdata) => {
if (eventdata["xaxis.range[0]"] === undefined) return;
debouncedDynamicLoad(eventdata, originalData);
}, 500),
}, 300),
);
}
}
@@ -515,7 +523,6 @@ function Chart({
? DARK_CHARTS_TEMPLATE
: LIGHT_CHARTS_TEMPLATE;
setPlotData({ ...originalData });
Plotly.react(plotDiv, originalData.data, originalData.layout);
setDarkMode(darkmode);
setChangeTheme(false);
} catch (e) {
@@ -643,8 +650,8 @@ function Chart({
Please use the zoom tool to see more data.`,
},
});
const new_data = CreateDataXrange(originalData.data);
setPlotData({ ...originalData, data: new_data });
const new_figure = CreateDataXrange(originalData);
setPlotData(new_figure);
setDateSliced(true);
setAutoScaling(true);
}
@@ -653,12 +660,12 @@ function Chart({
const plotComponent = useMemo(
() => (
<PlotComponent
onInitialized={() => {
onInitialized={(_figure, graphDiv) => {
if (!plotDiv) {
const plot = document.getElementById("plotlyChart");
console.log("plot", plot);
if (plot) setPlotDiv(plot);
plot.globals = globals;
if (graphDiv) {
graphDiv.globals = globals;
setPlotDiv(graphDiv);
}
}
if (!plotLoaded) setPlotLoaded(true);
}}
@@ -712,7 +719,6 @@ function Chart({
overlay.layout.showlegend = true;
setOriginalData(overlay);
setPlotData(overlay);
Plotly.react(plotDiv, overlay.data, overlay.layout);
}}
plotlyData={originalData}
setLoading={setLoading}
@@ -767,6 +773,17 @@ function Chart({
);
}, [modal, onClose]);
const memoizedChartHotkeys = useMemo(() => {
return (
<ChartHotkeys
setModal={setModal}
Loading={setLoading}
changeColor={setChangeColor}
downloadFinished={setDownloadFinished}
/>
);
}, [setModal, setLoading, setChangeColor, setDownloadFinished]);
return (
<div className="relative h-full">
{loading && (
@@ -803,6 +820,7 @@ function Chart({
{memoizedTextChartDialog}
{memoizedChangeColor}
{memoizedDownloadFinishedDialog}
{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">

View File

@@ -1,7 +1,9 @@
import { Icons as PlotlyIcons } from "plotly.js-dist-min";
import { downloadCSV, downloadImage } from "../utils/utils";
import { useHotkeys } from "react-hotkeys-hook";
import { ICONS } from "./Config";
export function hideModebar(hide?: boolean) {
return new Promise((resolve) => {
if (!window.MODEBAR) {
@@ -139,3 +141,78 @@ export function PlotConfig({
};
return CONFIG;
}
export function ChartHotkeys({
setModal,
Loading,
changeColor,
downloadFinished,
}: {
setModal: (modal: { name: string; data?: any }) => void;
Loading: (change: boolean) => void;
changeColor: (change: boolean) => void;
downloadFinished: (change: boolean) => void;
}) {
useHotkeys(
"ctrl+shift+t",
() => {
setModal({ name: "titleDialog" });
},
{ preventDefault: true },
);
useHotkeys(
"ctrl+t",
() => {
setModal({ name: "textDialog" });
},
{ preventDefault: true },
);
useHotkeys(
"ctrl+o",
() => {
setModal({ name: "overlayChart" });
},
{ preventDefault: true },
);
useHotkeys(
["ctrl+shift+h", "ctrl+h"],
() => {
hideModebar();
},
{ preventDefault: true },
);
useHotkeys(
"ctrl+e",
() => {
changeColor(true);
},
{ preventDefault: true },
);
useHotkeys(
"ctrl+shift+s",
async () => {
setModal({ name: "downloadCsv" });
await downloadCSV(
document.getElementById("plotlyChart") as any,
downloadFinished,
);
},
{ preventDefault: true },
);
useHotkeys(
"ctrl+s",
async () => {
hideModebar();
downloadImage("MainChart", hideModebar, Loading, downloadFinished);
},
{ preventDefault: true },
);
useHotkeys(
"ctrl+w",
() => {
window.close();
},
{ preventDefault: true },
);
}