Skip to content

How to show missing data when charting multiple lines dynamically #557

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

Open
cyburns opened this issue May 7, 2025 · 0 comments
Open

How to show missing data when charting multiple lines dynamically #557

cyburns opened this issue May 7, 2025 · 0 comments

Comments

@cyburns
Copy link

cyburns commented May 7, 2025

How to show missing data when charting multiple lines dynamically

I have time series data returned from my backend. In my app the user can select x amount of devices to chart (restricted to five right now). Therefore, I need to chart a dynamic number of lines on one CartesianChart. I have figured out a way to do this and get the chart press state y value for each one. However, I have found that when there is missing data in one of the data arrays, it looks like there is no missing data until you inspect the timestamps (y values). As you may notice in my example below I have started to work on this by padding the start but this does not really help. It only accounts for if there is missing data right at the beginning of the data.

I am wondering how I would show a break in 1 or more of the lines and still get the results I am looking for?

      <CartesianChart
          data={formattedData}
          xKey="time"
          yKeys={yKeys as never[]}
          domain={{ y: [minY, maxY + (isCumulative ? 0.0175 : 5)] }}
          chartPressState={state}
          axisOptions={{
            formatYLabel: (value) => {
              const linearTick = `${value}${units}`;
              const dp = isBoolVal ? value === 'true' : value;
              const categoricalTick = formatDatapoint(
                { value: dp },
                attr.datatype,
                unitPreferences,
              );

              return isCategorical ? categoricalTick : linearTick;
            },
            formatXLabel: (value) => (value ? String(value) : ''),
            font: chartLabelFont,
            tickCount: {
              x: getXTickCount(),
              y: isCategorical ? 1 : 10,
            },
            labelOffset: { x: 5, y: 5 },
            labelPosition: { x: 'outset', y: 'outset' },
            axisSide: { x: 'bottom', y: 'left' },
            lineColor: Colors.lightGray,
            labelColor: Colors.darkGray,
          }}
          onChartBoundsChange={({ bottom, left }) => {
            setChartBottom(bottom);
            setChartLeft(left);
          }}
        >
          {({ points, chartBounds }) => (
            <Fragment>
              {yKeys.map((key, index) => {
                const pointsArray = points[
                  key as keyof typeof points
                ] as PointsArray;
                const chartColor =
                  yKeys.length === 1 ? colorOfDevice : randomHexColors[index];
                const isAreaVisible = yKeys.length === 1;
                const curveType = CURVE_TYPES[selectedCurveType].type;

                return (
                  <Fragment key={key + 'chart-line'}>
                    <LineAndArea
                      points={pointsArray}
                      isWindowActive={isActive}
                      startX={state.x.position}
                      left={chartBounds.left}
                      right={chartBounds.right}
                      top={chartBounds.top}
                      bottom={chartBounds.bottom}
                      isAreaVisible={isAreaVisible}
                      connectMissingData={false}
                      curveType={curveType}
                      chartColor={chartColor}
                    />

                    {isActive && (
                      <ActiveValueIndicator
                        xPosition={state.x.position}
                        yPosition={state.y[key].position}
                        top={chartBounds.top}
                        bottom={chartBounds.bottom}
                        left={chartBounds.left}
                        right={chartBounds.right}
                        font={chartActiveTitleFont}
                        topOffset={10}
                        state={state}
                        showCircle
                        color={chartColor}
                        index={index}
                      />
                    )}
                  </Fragment>
                );
              })}
            </Fragment>
          )}
        </CartesianChart>
interface FormattedData {
  [key: string]: number | string;
  time: string;
}

export const useFormatDeviceData = (
  chartData: ChartData,
  attr: Attr,
  unitPreferences: UnitPreferences,
  tickDateFormat: string,
): {
  formattedData: FormattedData[];
  yKeys: string[];
  average: number;
  min: number;
  max: number;
  timeStamps: string[];
  sum: number;
} => {
  const valueType = getValueType(attr.datatype);
  const isCategorical =
    valueType === ValueType.String || valueType === ValueType.Boolean;

  const timeStamps = Array.from(
    new Set(
      chartData.flatMap((dataset) =>
        dataset.data.map((point) => point.timestamp),
      ),
    ),
  ).sort((a, b) => new Date(a).getTime() - new Date(b).getTime());

  const filteredData = chartData.filter(
    (dataset) => dataset.attribute.attributeId === attr.attributeId,
  );

  const dataMap = new Map<string, FormattedData>();

  const longestDataArr = Math.max(
    ...filteredData.map((dataset) => dataset.data.length),
  );

  const referenceTimestamps =
    filteredData
      .find((dataset) => dataset.data.length === longestDataArr)
      ?.data.map((point) => point.timestamp) || [];

  for (const dataset of filteredData) {
    const deviceName = dataset.device.name;

    const padLength = longestDataArr - dataset.data.length;
    const paddedData = Array(padLength).fill(null).concat(dataset.data);

    paddedData.forEach((point, j) => {
      const time = referenceTimestamps[j] ?? point?.timestamp;

      if (!time) return;

      const formattedTimeXValue = dayjs(time).format(tickDateFormat);

      let yValue = point
        ? convertDatatypeUnits(
            extractDatapointValue(point, attr.datatype, true),
            attr.datatype,
            unitPreferences,
          )
        : null;

      let entry = dataMap.get(formattedTimeXValue);

      if (!entry) {
        entry = { time: formattedTimeXValue };
        dataMap.set(formattedTimeXValue, entry);
      }

      if (isCategorical) {
        yValue = yValue ? 1 : 0;
      }

      entry[deviceName] = yValue;
    });
  }

  const formattedData = Array.from(dataMap.values());
  const yKeys = filteredData.map((dataset) => dataset.device.name);

  const numericValues = formattedData.flatMap((entry) =>
    yKeys.map((key) => entry[key]).filter((value) => typeof value === 'number'),
  ) as number[];

  const totalValues = numericValues.length;
  const sumValues = numericValues.reduce((sum, value) => sum + value, 0);
  const average = totalValues ? sumValues / totalValues : 0;
  const max = isCategorical ? 1 : Math.max(...numericValues);
  const min = isCategorical ? 0 : Math.min(...numericValues);
  const sum = numericValues.reduce((sum, value) => sum + value, 0);

  return { formattedData, yKeys, average, min, max, timeStamps, sum };
};
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

1 participant