Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
jwo opened this issue Mar 8, 2021 · 6 comments
Closed

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

jwo opened this issue Mar 8, 2021 · 6 comments
Assignees

Comments

@jwo
Copy link

jwo commented Mar 8, 2021

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

  • It should re-render the chart each time drawExample is run

Notes:

  • It works perfectly if I have the data before I render the chart, but in a real situation, I'll be updating the data constantly as the user changes dates and other paramters.
  • I'm using refs here instead of state, but it makes no difference if the surface/wasm are stored in refs or state
  • I've also tried adding sciChartSurfaceRef.current?.delete(); to the start of the Effect to force a new wasm, but it did not seem to change the outcome

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
Copy link
Contributor

observerjnr commented Mar 10, 2021

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 }} />
    </>
}

@observerjnr observerjnr self-assigned this Mar 10, 2021
@jwo
Copy link
Author

jwo commented Mar 10, 2021

@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
Copy link
Collaborator

andyb1979 commented Mar 10, 2021 via email

@jwo
Copy link
Author

jwo commented Mar 10, 2021

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
Copy link
Collaborator

hi @jwo try to call sciChartSurface.zoomExtents()

@jwo
Copy link
Author

jwo commented Mar 10, 2021

@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants