ABTSoftware / SciChart.JS.Examples

MIT License
77 stars 37 forks source link

Hover/Legend not working when updating data asyncronously (React) #57

Closed jwo closed 3 years ago

jwo commented 3 years ago

Hi there! Using the https://demo.scichart.com/javascript-line-chart if I move the data to update asyncronously (like from an API), the legend and the hover bar don't seem to work. They'll work on the next render, but not right away.

Below the green hover bar only appears if I move my mouse between the graph and the incorrectly positioned legend. image

Expectations

Notes:

Code


import React from "react";
import { MouseWheelZoomModifier } from 'scichart/Charting/ChartModifiers/MouseWheelZoomModifier';
import { ZoomExtentsModifier } from 'scichart/Charting/ChartModifiers/ZoomExtentsModifier';
import { ZoomPanModifier } from 'scichart/Charting/ChartModifiers/ZoomPanModifier';
import { XyDataSeries } from 'scichart/Charting/Model/XyDataSeries';
import { NumericAxis } from 'scichart/Charting/Visuals/Axis/NumericAxis';
import { FastLineRenderableSeries } from 'scichart/Charting/Visuals/RenderableSeries/FastLineRenderableSeries';
import { SciChartSurface } from 'scichart/Charting/Visuals/SciChartSurface';
import { LegendModifier } from 'scichart/Charting/ChartModifiers/LegendModifier';
import { ELegendOrientation, ELegendPlacement } from 'scichart/Charting/Visuals/Legend/SciChartLegendBase';
import { EllipsePointMarker } from 'scichart/Charting/Visuals/PointMarkers/EllipsePointMarker';
import { RolloverModifier } from 'scichart/Charting/ChartModifiers/RolloverModifier';
import { TSciChart } from 'scichart/types/TSciChart';

const DIVID = "CHARTID"

SciChartSurface.setRuntimeLicenseKey('REDACTED');

const streamSelectors = {
  56: {
    uiLabel: 'First Stream',
    color: "#cecece"
  },
  65: {
    uiLabel: "Second Stream",
    color: "#000000"
  }
}
type ChartData = Record<number, Array<{ timestamp: number, value: number, label: string }>>

const drawExample = async (
  chartData: ChartData
) => {
  // Create a SciChartSurface
  const { sciChartSurface, wasmContext } = await SciChartSurface.create(DIVID);

  // Create the X,Y Axis
  const xAxis = new NumericAxis(wasmContext);
  xAxis.labelProvider.formatLabel = (unixTimestamp: number) => {
    return new Date(unixTimestamp * 1000).toLocaleDateString('en-us', {
      month: 'numeric',
      year: 'numeric',
      day: 'numeric',
    });
  };

  const yAxis = new NumericAxis(wasmContext);
  sciChartSurface.yAxes.add(yAxis);

  const streamIds = Object.keys(chartData);
  streamIds.forEach((s) => {
    const streamId = Number(s) as keyof ChartData;
    const streamSelectorId = streamId as keyof typeof streamSelectors;
    const xyDataSeries = new XyDataSeries(wasmContext);
    xyDataSeries.dataSeriesName = streamSelectors[streamSelectorId].uiLabel;
    chartData[streamId].forEach((value) => {
      xyDataSeries.append(value.timestamp, value.value);
    });

    const lineColor = streamSelectors[streamSelectorId].color;

    const lineSeries = new FastLineRenderableSeries(wasmContext, {
      stroke: lineColor,
      strokeThickness: 2,
      dataSeries: xyDataSeries,
      pointMarker: new EllipsePointMarker(wasmContext, {
        width: 4,
        height: 4,
        strokeThickness: 1,
        fill: lineColor,
      }),
    });
    lineSeries.rolloverModifierProps.tooltipLabelX = 'Date';
    lineSeries.rolloverModifierProps.tooltipLabelY = '';
    lineSeries.rolloverModifierProps.tooltipColor = lineColor;
    sciChartSurface.renderableSeries.add(lineSeries);
  });

  sciChartSurface.xAxes.add(xAxis);

  sciChartSurface.chartModifiers.add(
    new LegendModifier({
      placement: ELegendPlacement.TopLeft,
      orientation: ELegendOrientation.Vertical,
      showLegend: true,
      showCheckboxes: true,
      showSeriesMarkers: true,
    })
  );
  sciChartSurface.chartModifiers.add(new RolloverModifier({}));
  sciChartSurface.chartModifiers.add(new ZoomPanModifier());
  sciChartSurface.chartModifiers.add(new ZoomExtentsModifier());
  sciChartSurface.chartModifiers.add(new MouseWheelZoomModifier());

  sciChartSurface.zoomExtents();
  return { sciChartSurface, wasmContext };
}

export const Chart = () => {

  const chartRef = React.useRef<HTMLDivElement>(null);
  const sciChartSurfaceRef = React.useRef<SciChartSurface>();
  const wasmContextRef = React.useRef<TSciChart>();
  const [chartData, setChartData] = React.useState<ChartData>({} as ChartData);

  React.useEffect(() => {
    (async () => {
      const { sciChartSurface, wasmContext } = await drawExample(chartData);
      sciChartSurfaceRef.current = sciChartSurface;
      wasmContextRef.current = wasmContext;
    })();
    return () => {
      sciChartSurfaceRef.current?.delete();
    };
  }, [sciChartSurfaceRef, chartData]);

  React.useEffect(() => {
    const data: ChartData = {
      56: [{
        timestamp: new Date("2021-01-01").getTime() / 1000,
        value: 5,
        label: "Jan 2021"
      },
      {
        timestamp: new Date("2021-02-01").getTime() / 1000,
        value: 10,
        label: "Feb 2021"
      },
      {
        timestamp: new Date("2021-03-01").getTime() / 1000,
        value: 15,
        label: "March 2021"
      },
      ],
      65: [{
        timestamp: new Date("2021-01-01").getTime() / 1000,
        value: 15,
        label: "Jan 2021"
      },
      {
        timestamp: new Date("2021-02-01").getTime() / 1000,
        value: 20,
        label: "Feb 2021"
      },
      {
        timestamp: new Date("2021-03-01").getTime() / 1000,
        value: 25,
        label: "March 2021"
      }]
    }
    setChartData(data)
  }, [])

  return <>
    <div ref={chartRef} id={DIVID} style={{ width: 800, height: 800 }} />
  </>
}
observerjnr commented 3 years ago

Looks like the layout issue is caused by the chart being initialized multiple times:

  React.useEffect(() => {
    (async () => {
      const { sciChartSurface, wasmContext } = await drawExample(chartData);
      sciChartSurfaceRef.current = sciChartSurface;
      wasmContextRef.current = wasmContext;
    })();
    return () => {
      sciChartSurfaceRef.current?.delete();
    };
  }, [sciChartSurfaceRef, chartData]);

I suggest initializing it once, and then, on incoming data updates, just add new Line Series or append the values to the existing data series.

Please check the snippet below. I tried modifying your example to implement the behavior that I believe you are trying to achieve.

import React from "react";
import { MouseWheelZoomModifier } from 'scichart/Charting/ChartModifiers/MouseWheelZoomModifier';
import { ZoomExtentsModifier } from 'scichart/Charting/ChartModifiers/ZoomExtentsModifier';
import { ZoomPanModifier } from 'scichart/Charting/ChartModifiers/ZoomPanModifier';
import { XyDataSeries } from 'scichart/Charting/Model/XyDataSeries';
import { NumericAxis } from 'scichart/Charting/Visuals/Axis/NumericAxis';
import { FastLineRenderableSeries } from 'scichart/Charting/Visuals/RenderableSeries/FastLineRenderableSeries';
import { SciChartSurface } from 'scichart/Charting/Visuals/SciChartSurface';
import { LegendModifier } from 'scichart/Charting/ChartModifiers/LegendModifier';
import { ELegendOrientation, ELegendPlacement } from 'scichart/Charting/Visuals/Legend/SciChartLegendBase';
import { EllipsePointMarker } from 'scichart/Charting/Visuals/PointMarkers/EllipsePointMarker';
import { RolloverModifier } from 'scichart/Charting/ChartModifiers/RolloverModifier';
import { TSciChart } from 'scichart/types/TSciChart';

const DIVID = "CHARTID"

SciChartSurface.setRuntimeLicenseKey('REDACTED');

const streamSelectors = {
    56: {
        uiLabel: 'First Stream',
        color: "#cecece"
    },
    65: {
        uiLabel: "Second Stream",
        color: "#000000"
    }
}
type ChartData = Record<number, Array<{ timestamp: number, value: number, label: string }>>

// this function is supposed to add new data series to the chart or update the existing ones
const addOrUpdateSeries = (
    dataSeriesMap: Map<keyof typeof streamSelectors, XyDataSeries>,
    sciChartSurface: SciChartSurface,
    wasmContext: TSciChart,
    chartData: ChartData,
) => {
    const streamIds = Object.keys(chartData);
    streamIds.forEach((s) => {
        const streamId = Number(s) as keyof ChartData;
        const streamSelectorId = streamId as keyof typeof streamSelectors;
        if (dataSeriesMap.has(streamSelectorId)) {
            const xyDataSeries = dataSeriesMap.get(streamSelectorId);
            chartData[streamId].forEach((value) => {
                xyDataSeries.append(value.timestamp, value.value);
            });
        } else {
            // create new data series for the specific stream
            const xyDataSeries = new XyDataSeries(wasmContext);
            dataSeriesMap.set(streamSelectorId, xyDataSeries);

            xyDataSeries.dataSeriesName = streamSelectors[streamSelectorId].uiLabel;
            const lineColor = streamSelectors[streamSelectorId].color;

            chartData[streamId].forEach((value) => {
                xyDataSeries.append(value.timestamp, value.value);
            });

            const lineSeries = new FastLineRenderableSeries(wasmContext, {
                stroke: lineColor,
                strokeThickness: 2,
                dataSeries: xyDataSeries,
                pointMarker: new EllipsePointMarker(wasmContext, {
                    width: 4,
                    height: 4,
                    strokeThickness: 1,
                    fill: lineColor,
                }),
            });

            lineSeries.rolloverModifierProps.tooltipLabelX = 'Date';
            lineSeries.rolloverModifierProps.tooltipLabelY = '';
            lineSeries.rolloverModifierProps.tooltipColor = lineColor;
            sciChartSurface.renderableSeries.add(lineSeries);
        }
    });
};

// drawExample will initialize the chart and modifiers
const drawExample = async () => {
    // Create a SciChartSurface
    const { sciChartSurface, wasmContext } = await SciChartSurface.create(DIVID);

    // Create the X,Y Axis
    const xAxis = new NumericAxis(wasmContext);
    xAxis.labelProvider.formatLabel = (unixTimestamp: number) => {
        return new Date(unixTimestamp * 1000).toLocaleDateString('en-us', {
            month: 'numeric',
            year: 'numeric',
            day: 'numeric',
        });
    };

    const yAxis = new NumericAxis(wasmContext);
    sciChartSurface.yAxes.add(yAxis);
    sciChartSurface.xAxes.add(xAxis);

    sciChartSurface.chartModifiers.add(
        new LegendModifier({
            placement: ELegendPlacement.TopLeft,
            orientation: ELegendOrientation.Vertical,
            showLegend: true,
            showCheckboxes: true,
            showSeriesMarkers: true,
        })
    );
    sciChartSurface.chartModifiers.add(new RolloverModifier({}));
    sciChartSurface.chartModifiers.add(new ZoomPanModifier());
    sciChartSurface.chartModifiers.add(new ZoomExtentsModifier());
    sciChartSurface.chartModifiers.add(new MouseWheelZoomModifier());

    sciChartSurface.zoomExtents();
    return { sciChartSurface, wasmContext };
}

export const Chart = () => {
    const chartRef = React.useRef<HTMLDivElement>(null);
    const [sciChartSurface, setSciChartSurface] = React.useState<SciChartSurface>();
    const [wasmContext, setWasmContext] = React.useState<TSciChart>();

    // a structure to hold references of dataSeries corresponding to the specific streams
    const dataSeriesMapRef = React.useRef<Map<keyof typeof streamSelectors, XyDataSeries>>();

    const [chartData, setChartData] = React.useState<ChartData>({} as ChartData);

    React.useEffect(() => {
        (async () => {
            const { sciChartSurface, wasmContext } = await drawExample();
            setSciChartSurface(sciChartSurface);
            setWasmContext(wasmContext);
        })();

        dataSeriesMapRef.current = new Map<keyof typeof streamSelectors, XyDataSeries>();

        return () => {
            sciChartSurface?.delete();
        };
    }, []);// make sure the chart is initialized only once

    React.useEffect(() => {
        if (dataSeriesMapRef.current && sciChartSurface && wasmContext) {
            addOrUpdateSeries(
                dataSeriesMapRef.current,
                sciChartSurface,
                wasmContext,
                chartData
            );
        }
    }, [chartData])

    React.useEffect(() => {
        // make sure the chart is initialized before passing new data
        if (!sciChartSurface) {
            return;
        }

        const data: ChartData = {
            56: [{
                timestamp: new Date("2021-01-01").getTime() / 1000,
                value: 5,
                label: "Jan 2021"
            },
            {
                timestamp: new Date("2021-02-01").getTime() / 1000,
                value: 10,
                label: "Feb 2021"
            },
            {
                timestamp: new Date("2021-03-01").getTime() / 1000,
                value: 15,
                label: "March 2021"
            },
            ],
            65: [{
                timestamp: new Date("2021-01-01").getTime() / 1000,
                value: 15,
                label: "Jan 2021"
            },
            {
                timestamp: new Date("2021-02-01").getTime() / 1000,
                value: 20,
                label: "Feb 2021"
            },
            {
                timestamp: new Date("2021-03-01").getTime() / 1000,
                value: 25,
                label: "March 2021"
            }]
        }

        const nextData: ChartData = {
            56: [{
                timestamp: new Date("2021-04-01").getTime() / 1000,
                value: 5,
                label: "April 2021"
            },
            {
                timestamp: new Date("2021-05-01").getTime() / 1000,
                value: 10,
                label: "May 2021"
            },
            {
                timestamp: new Date("2021-06-01").getTime() / 1000,
                value: 15,
                label: "June 2021"
            },
            ],
            65: [{
                timestamp: new Date("2021-04-01").getTime() / 1000,
                value: 15,
                label: "April 2021"
            },
            {
                timestamp: new Date("2021-05-01").getTime() / 1000,
                value: 20,
                label: "May 2021"
            },
            {
                timestamp: new Date("2021-06-01").getTime() / 1000,
                value: 25,
                label: "June 2021"
            }]
        };

        setChartData(data)

        // simulate new data updates
        setTimeout(() => {
            setChartData(nextData)
        }, 5000)

    }, [sciChartSurface])

    return <>
        <div ref={chartRef} id={DIVID} style={{ width: 800, height: 800 }} />
    </>
}
jwo commented 3 years ago

@observerjnr I don't hate the idea; being able to simply add/change data on an existing wasm instance seems an optimal way to go.

What I might suggest is a way to clear it out and rebuild it easily. For example, in react, you might make changes on a form and get data from API pretty frequently, so instead of me keeping track of what's been on the chart, a usual pattern is to simply give the data and it gets redrawn.

That's why I tried the sciChartSurface?.delete(); at the start of the effect.

Could you add a way to clear out the series? that way I'd be able to easy say, "OK my data changed, here you go, rerender"

andyb1979 commented 3 years ago

Hi Jesse

The function call sciChartSurface.invalidateElement() schedules a redraw, however it may not draw immediately as we internally throttle/schedule draws.

Is that what you were looking for?

Best regards, Andrew

On Wed, Mar 10, 2021 at 3:59 PM Jesse Wolgamott notifications@github.com wrote:

@observerjnr https://github.com/observerjnr I don't hate the idea; being able to simply add/change data on an existing wasm instance seems an optimal way to go.

What I might suggest is a way to clear it out and rebuild it easily. For example, in react, you might make changes on a form and get data from API pretty frequently, so instead of me keeping track of what's been on the chart, a usual pattern is to simply give the data and it gets redrawn.

That's why I tried the sciChartSurface?.delete(); at the start of the effect.

Could you add a way to clear out the series? that way I'd be able to easy say, "OK my data changed, here you go, rerender"

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/ABTSoftware/SciChart.JS.Examples/issues/57#issuecomment-795650653, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADLEDVNY76K7YRE3DEBMI4LTC6CMTANCNFSM4YZL4MJQ .

jwo commented 3 years ago

The function call sciChartSurface.invalidateElement() schedules a redraw, however it may not draw immediately as we internally throttle/schedule draws.

@andyb1979 that seems reasonable, but I found sciChartSurface.renderableSeries.clear() which I think accompishes what I really want. and if I combine it with @observerjnr method of splitting chart and data, I get something that works pretty well. The hover works, and the legend works.

The Zoom on the chart is not quite where I'd want it to be.. any suggestions to force the surface to re-figure out the zoom based on the new series in the chart?

image

import React from "react";
import { MouseWheelZoomModifier } from 'scichart/Charting/ChartModifiers/MouseWheelZoomModifier';
import { ZoomExtentsModifier } from 'scichart/Charting/ChartModifiers/ZoomExtentsModifier';
import { ZoomPanModifier } from 'scichart/Charting/ChartModifiers/ZoomPanModifier';
import { XyDataSeries } from 'scichart/Charting/Model/XyDataSeries';
import { NumericAxis } from 'scichart/Charting/Visuals/Axis/NumericAxis';
import { FastLineRenderableSeries } from 'scichart/Charting/Visuals/RenderableSeries/FastLineRenderableSeries';
import { SciChartSurface } from 'scichart/Charting/Visuals/SciChartSurface';
import { LegendModifier } from 'scichart/Charting/ChartModifiers/LegendModifier';
import { ELegendOrientation, ELegendPlacement } from 'scichart/Charting/Visuals/Legend/SciChartLegendBase';
import { EllipsePointMarker } from 'scichart/Charting/Visuals/PointMarkers/EllipsePointMarker';
import { RolloverModifier } from 'scichart/Charting/ChartModifiers/RolloverModifier';
import { TSciChart } from 'scichart/types/TSciChart';

const DIVID = "CHARTID"

SciChartSurface.setRuntimeLicenseKey('');

const streamSelectors = {
  56: {
    uiLabel: 'First Stream',
    color: "#cecece"
  },
  65: {
    uiLabel: "Second Stream",
    color: "#000000"
  }
}
type ChartData = Record<number, Array<{ timestamp: number, value: number, label: string }>>

const updateChartWithData = (chartData: ChartData,
  wasmContext: TSciChart,
  sciChartSurface: SciChartSurface) => {

  sciChartSurface.renderableSeries.clear();

  const streamIds = Object.keys(chartData);
  streamIds.forEach((s) => {
    const streamId = Number(s) as keyof ChartData;
    const streamSelectorId = streamId as keyof typeof streamSelectors;
    const xyDataSeries = new XyDataSeries(wasmContext);
    xyDataSeries.dataSeriesName = streamSelectors[streamSelectorId].uiLabel;
    chartData[streamId].forEach((value) => {
      xyDataSeries.append(value.timestamp, value.value);
    });

    const lineColor = streamSelectors[streamSelectorId].color;

    const lineSeries = new FastLineRenderableSeries(wasmContext, {
      stroke: lineColor,
      strokeThickness: 2,
      dataSeries: xyDataSeries,
      pointMarker: new EllipsePointMarker(wasmContext, {
        width: 4,
        height: 4,
        strokeThickness: 1,
        fill: lineColor,
        // stroke: 'LightSteelBlue',
      }),
    });
    lineSeries.rolloverModifierProps.tooltipLabelX = 'Date';
    lineSeries.rolloverModifierProps.tooltipLabelY = '';
    // lineSeries.rolloverModifierProps.tooltipTextColor = theme.palette.getContrastText(lineColor);
    lineSeries.rolloverModifierProps.tooltipColor = lineColor;
    sciChartSurface.renderableSeries.add(lineSeries);
    sciChartSurface.zoomExtents(); // added after next comment
  });

}

const drawExample = async () => {
  // Create a SciChartSurface
  const { sciChartSurface, wasmContext } = await SciChartSurface.create(DIVID);

  // Create the X,Y Axis
  const xAxis = new NumericAxis(wasmContext);
  xAxis.labelProvider.formatLabel = (unixTimestamp: number) => {
    return new Date(unixTimestamp * 1000).toLocaleDateString('en-us', {
      month: 'numeric',
      year: 'numeric',
      day: 'numeric',
    });
  };

  const yAxis = new NumericAxis(wasmContext); // ;, { growBy: new NumberRange(0.05, 0.05) });
  sciChartSurface.yAxes.add(yAxis);

  sciChartSurface.xAxes.add(xAxis);

  sciChartSurface.chartModifiers.add(
    new LegendModifier({
      placement: ELegendPlacement.TopLeft,
      orientation: ELegendOrientation.Vertical,
      showLegend: true,
      showCheckboxes: true,
      showSeriesMarkers: true,
    })
  );
  sciChartSurface.chartModifiers.add(new RolloverModifier({}));
  sciChartSurface.chartModifiers.add(new ZoomPanModifier());
  sciChartSurface.chartModifiers.add(new ZoomExtentsModifier());
  sciChartSurface.chartModifiers.add(new MouseWheelZoomModifier());

  sciChartSurface.zoomExtents();
  return { sciChartSurface, wasmContext };
}

export const Chart = () => {

  const chartRef = React.useRef<HTMLDivElement>(null);
  const sciChartSurfaceRef = React.useRef<SciChartSurface>();
  const wasmContextRef = React.useRef<TSciChart>();
  const [chartData, setChartData] = React.useState<ChartData>({} as ChartData);

  React.useEffect(() => {
    console.log('RUN React.useEffect');
    (async () => {
      const { sciChartSurface, wasmContext } = await drawExample();
      sciChartSurfaceRef.current = sciChartSurface;
      wasmContextRef.current = wasmContext;

    })();
    // Deleting sciChartSurface to prevent memory leak
    return () => {
      sciChartSurfaceRef.current?.delete();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [sciChartSurfaceRef]);

  React.useEffect(() => {
    if (wasmContextRef.current && sciChartSurfaceRef.current) {
      updateChartWithData(chartData, wasmContextRef.current, sciChartSurfaceRef.current)
    }
  }, [chartData])

  React.useEffect(() => {
    const data: ChartData = {
      56: [{
        timestamp: new Date("2021-01-01").getTime() / 1000,
        value: 5,
        label: "Jan 2021"
      },
      {
        timestamp: new Date("2021-02-01").getTime() / 1000,
        value: 10,
        label: "Feb 2021"
      },
      {
        timestamp: new Date("2021-03-01").getTime() / 1000,
        value: 15,
        label: "March 2021"
      },
      ],
      65: [{
        timestamp: new Date("2021-01-01").getTime() / 1000,
        value: 15,
        label: "Jan 2021"
      },
      {
        timestamp: new Date("2021-02-01").getTime() / 1000,
        value: 20,
        label: "Feb 2021"
      },
      {
        timestamp: new Date("2021-03-01").getTime() / 1000,
        value: 25,
        label: "March 2021"
      }]
    }
    setTimeout(() => {
      setChartData(data)
    }, 250)

  }, [])

  return <>
    <div ref={chartRef} id={DIVID} style={{ width: 800, height: 800 }} />
  </>
}
klishevich commented 3 years ago

hi @jwo try to call sciChartSurface.zoomExtents()

jwo commented 3 years ago

@klishevich that worked!

Ok, so yes I think with all these things, no code change needed.

I would recommend that y’all add a documentation with async data that uses the techniques above. Feel free to use my example, but you probably have better ones.

Thanks for working this through with me!

feel free to close if you like, I’m all good here