recharts#ContentRenderer TypeScript Examples

The following examples show how to use recharts#ContentRenderer. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: BarChart.tsx    From backstage with Apache License 2.0 6 votes vote down vote up
defaultTooltip: ContentRenderer<RechartsTooltipProps> = ({
  label,
  payload = [],
}) => {
  if (isInvalid({ label, payload })) return null;

  const title = titleOf(label);
  const items = payload.map(tooltipItemOf).filter(notEmpty);
  return (
    <BarChartTooltip title={title}>
      {items.map((item, index) => (
        <BarChartTooltipItem key={`${item.label}-${index}`} item={item} />
      ))}
    </BarChartTooltip>
  );
}
Example #2
Source File: CostOverviewBreakdownChart.tsx    From backstage with Apache License 2.0 4 votes vote down vote up
CostOverviewBreakdownChart = ({
  costBreakdown,
}: CostOverviewBreakdownChartProps) => {
  const theme = useTheme<CostInsightsTheme>();
  const classes = useStyles(theme);
  const lastCompleteBillingDate = useLastCompleteBillingDate();
  const { duration } = useFilters(mapFiltersToProps);
  const [isExpanded, setExpanded] = useState(false);

  if (!costBreakdown) {
    return null;
  }

  const flattenedAggregation = costBreakdown
    .map(cost => cost.aggregation)
    .flat();

  const totalCost = aggregationSum(flattenedAggregation);

  const previousPeriodTotal = getPreviousPeriodTotalCost(
    flattenedAggregation,
    duration,
    lastCompleteBillingDate,
  );
  const currentPeriodTotal = totalCost - previousPeriodTotal;
  const canExpand = costBreakdown.length >= 8;
  const otherCategoryIds: string[] = [];

  const breakdownsByDate = costBreakdown.reduce(
    (breakdownByDate, breakdown) => {
      const breakdownTotal = aggregationSum(breakdown.aggregation);
      // Group breakdown items with less than 10% of the total cost into "Other" category if needed
      const isOtherCategory =
        canExpand && breakdownTotal < totalCost * LOW_COST_THRESHOLD;

      const updatedBreakdownByDate = { ...breakdownByDate };
      if (isOtherCategory) {
        otherCategoryIds.push(breakdown.id);
      }
      breakdown.aggregation.forEach(curAggregation => {
        const costsForDate = updatedBreakdownByDate[curAggregation.date] || {};

        updatedBreakdownByDate[curAggregation.date] = {
          ...costsForDate,
          [breakdown.id]:
            (costsForDate[breakdown.id] || 0) + curAggregation.amount,
        };
      });

      return updatedBreakdownByDate;
    },
    {} as Record<string, Record<string, number>>,
  );

  const chartData: Record<string, number>[] = Object.keys(breakdownsByDate).map(
    date => {
      const costsForDate = Object.keys(breakdownsByDate[date]).reduce(
        (dateCosts, breakdown) => {
          // Group costs for items that belong to 'Other' in the chart.
          const cost = breakdownsByDate[date][breakdown];
          const breakdownCost =
            !isExpanded && otherCategoryIds.includes(breakdown)
              ? { Other: (dateCosts.Other || 0) + cost }
              : { [breakdown]: cost };
          return { ...dateCosts, ...breakdownCost };
        },
        {} as Record<string, number>,
      );
      return {
        ...costsForDate,
        date: Date.parse(date),
      };
    },
  );

  const sortedBreakdowns = costBreakdown.sort(
    (a, b) => aggregationSum(a.aggregation) - aggregationSum(b.aggregation),
  );

  const renderAreas = () => {
    const separatedBreakdowns = sortedBreakdowns
      // Check that the breakdown is a separate group and hasn't been added to 'Other'
      .filter(
        breakdown =>
          breakdown.id !== 'Other' && !otherCategoryIds.includes(breakdown.id),
      )
      .map(breakdown => breakdown.id);
    // Keep 'Other' category at the bottom of the stack
    const breakdownsToDisplay = isExpanded
      ? sortedBreakdowns.map(breakdown => breakdown.id)
      : ['Other', ...separatedBreakdowns];

    return breakdownsToDisplay.map((breakdown, i) => {
      // Logic to handle case where there are more items than data viz colors.
      const color =
        theme.palette.dataViz[
          (breakdownsToDisplay.length - 1 - i) %
            (theme.palette.dataViz.length - 1)
        ];
      return (
        <Area
          key={breakdown}
          dataKey={breakdown}
          isAnimationActive={false}
          stackId="1"
          stroke={color}
          fill={color}
          onClick={() => setExpanded(true)}
          style={{
            cursor: breakdown === 'Other' && !isExpanded ? 'pointer' : null,
          }}
        />
      );
    });
  };

  const tooltipRenderer: ContentRenderer<TooltipProps> = ({
    label,
    payload = [],
  }) => {
    if (isInvalid({ label, payload })) return null;

    const date =
      typeof label === 'number'
        ? DateTime.fromMillis(label)
        : DateTime.fromISO(label!);
    const dateTitle = date.toUTC().toFormat(DEFAULT_DATE_FORMAT);
    const items = payload.map(p => ({
      label: p.dataKey as string,
      value: formatGraphValue(p.value as number),
      fill: p.fill!,
    }));
    const expandText = (
      <Box>
        <Divider
          style={{
            backgroundColor: emphasize(theme.palette.divider, 1),
            margin: '10px 0',
          }}
        />
        <Box display="flex" justifyContent="space-between" alignItems="center">
          <FullScreenIcon />
          <Typography>Click to expand</Typography>
        </Box>
      </Box>
    );
    return (
      <Tooltip title={dateTitle}>
        {items.reverse().map((item, index) => (
          <TooltipItem key={`${item.label}-${index}`} item={item} />
        ))}
        {canExpand && !isExpanded ? expandText : null}
      </Tooltip>
    );
  };

  const options: Partial<BarChartLegendOptions> = {
    previousName: formatPeriod(duration, lastCompleteBillingDate, false),
    currentName: formatPeriod(duration, lastCompleteBillingDate, true),
    hideMarker: true,
  };

  return (
    <Box display="flex" flexDirection="column">
      <Box display="flex" flexDirection="row">
        <BarChartLegend
          costStart={previousPeriodTotal}
          costEnd={currentPeriodTotal}
          options={options}
        />
      </Box>
      <ResponsiveContainer
        width={classes.container.width}
        height={classes.container.height}
      >
        <AreaChart
          data={chartData}
          margin={{
            top: 16,
            right: 30,
            bottom: 40,
          }}
        >
          <CartesianGrid stroke={classes.cartesianGrid.stroke} />
          <XAxis
            dataKey="date"
            domain={['dataMin', 'dataMax']}
            tickFormatter={overviewGraphTickFormatter}
            tickCount={6}
            type="number"
            stroke={classes.axis.fill}
          />
          <YAxis
            domain={[() => 0, 'dataMax']}
            tick={{ fill: classes.axis.fill }}
            tickFormatter={formatGraphValue}
            width={classes.yAxis.width}
          />
          {renderAreas()}
          <RechartsTooltip content={tooltipRenderer} animationDuration={100} />
        </AreaChart>
      </ResponsiveContainer>
    </Box>
  );
}
Example #3
Source File: CostOverviewChart.tsx    From backstage with Apache License 2.0 4 votes vote down vote up
CostOverviewChart = ({
  dailyCostData,
  metric,
  metricData,
  responsive = true,
}: CostOverviewChartProps) => {
  const theme = useTheme<CostInsightsTheme>();
  const styles = useStyles(theme);

  const data = {
    dailyCost: {
      dataKey: 'dailyCost',
      name: `Daily Cost`,
      format: 'currency',
      data: dailyCostData,
    },
    metric: {
      dataKey: metric?.kind ?? 'Unknown',
      name: metric?.name ?? 'Unknown',
      format: metricData?.format ?? 'number',
      data: metricData,
    },
  };

  const metricsByDate = data.metric.data
    ? data.metric.data.aggregation.reduce(groupByDate, {})
    : {};

  const chartData: ChartData[] = data.dailyCost.data.aggregation
    .slice()
    .sort(aggregationSort)
    .map(entry => ({
      date: Date.parse(entry.date),
      trend: trendFrom(data.dailyCost.data.trendline!, Date.parse(entry.date)),
      dailyCost: entry.amount,
      ...(metric && data.metric.data
        ? { [data.metric.dataKey]: metricsByDate[`${entry.date}`] }
        : {}),
    }));

  const tooltipRenderer: ContentRenderer<TooltipProps> = ({
    label,
    payload = [],
  }) => {
    if (isInvalid({ label, payload })) return null;

    const dataKeys = [data.dailyCost.dataKey, data.metric.dataKey];
    const date =
      typeof label === 'number'
        ? DateTime.fromMillis(label)
        : DateTime.fromISO(label!);
    const title = date.toUTC().toFormat(DEFAULT_DATE_FORMAT);
    const items = payload
      .filter(p => dataKeys.includes(p.dataKey as string))
      .map(p => ({
        label:
          p.dataKey === data.dailyCost.dataKey
            ? data.dailyCost.name
            : data.metric.name,
        value:
          p.dataKey === data.dailyCost.dataKey
            ? formatGraphValue(p.value as number, data.dailyCost.format)
            : formatGraphValue(p.value as number, data.metric.format),
        fill:
          p.dataKey === data.dailyCost.dataKey
            ? theme.palette.blue
            : theme.palette.magenta,
      }));

    return (
      <Tooltip title={title}>
        {items.map((item, index) => (
          <TooltipItem key={`${item.label}-${index}`} item={item} />
        ))}
      </Tooltip>
    );
  };

  return (
    <Box display="flex" flexDirection="column">
      <CostOverviewLegend
        dailyCostData={dailyCostData}
        metric={metric}
        metricData={metricData}
      />
      <ResponsiveContainer
        width={responsive ? '100%' : styles.container.width}
        height={styles.container.height}
        className="cost-overview-chart"
      >
        <ComposedChart margin={styles.chart.margin} data={chartData}>
          <CartesianGrid stroke={styles.cartesianGrid.stroke} />
          <XAxis
            dataKey="date"
            domain={['dataMin', 'dataMax']}
            tickFormatter={overviewGraphTickFormatter}
            tickCount={6}
            type="number"
            stroke={styles.axis.fill}
          />
          <YAxis
            domain={[() => 0, 'dataMax']}
            tick={{ fill: styles.axis.fill }}
            tickFormatter={formatGraphValue}
            width={styles.yAxis.width}
            yAxisId={data.dailyCost.dataKey}
          />
          {metric && (
            <YAxis
              hide
              domain={[() => 0, toDataMax(data.metric.dataKey, chartData)]}
              width={styles.yAxis.width}
              yAxisId={data.metric.dataKey}
            />
          )}
          <Area
            dataKey={data.dailyCost.dataKey}
            isAnimationActive={false}
            fill={theme.palette.blue}
            fillOpacity={0.4}
            stroke="none"
            yAxisId={data.dailyCost.dataKey}
          />
          <Line
            activeDot={false}
            dataKey="trend"
            dot={false}
            isAnimationActive={false}
            label={false}
            strokeWidth={2}
            stroke={theme.palette.blue}
            yAxisId={data.dailyCost.dataKey}
          />
          {metric && (
            <Line
              dataKey={data.metric.dataKey}
              dot={false}
              isAnimationActive={false}
              label={false}
              strokeWidth={2}
              stroke={theme.palette.magenta}
              yAxisId={data.metric.dataKey}
            />
          )}
          <RechartsTooltip content={tooltipRenderer} animationDuration={100} />
        </ComposedChart>
      </ResponsiveContainer>
    </Box>
  );
}
Example #4
Source File: ProductInsightsChart.tsx    From backstage with Apache License 2.0 4 votes vote down vote up
ProductInsightsChart = ({
  billingDate,
  entity,
  duration,
}: ProductInsightsChartProps) => {
  const classes = useStyles();
  const layoutClasses = useLayoutStyles();

  // Only a single entities Record for the root product entity is supported
  const entities = useMemo(() => {
    const entityLabel = assertAlways(findAnyKey(entity.entities));
    return entity.entities[entityLabel] ?? [];
  }, [entity]);

  const [activeLabel, setActive] = useState<Maybe<string>>();
  const [selectLabel, setSelected] = useState<Maybe<string>>();
  const isSelected = useMemo(() => !isUndefined(selectLabel), [selectLabel]);

  const isClickable = useMemo(() => {
    const breakdowns = Object.keys(
      entities.find(e => e.id === activeLabel)?.entities ?? {},
    );
    return breakdowns.length > 0;
  }, [entities, activeLabel]);

  const costStart = entity.aggregation[0];
  const costEnd = entity.aggregation[1];
  const resources = entities.map(resourceOf);

  const options: Partial<BarChartLegendOptions> = {
    previousName: formatPeriod(duration, billingDate, false),
    currentName: formatPeriod(duration, billingDate, true),
  };

  const onMouseMove: RechartsFunction = (
    data: Record<'activeLabel', string | undefined>,
  ) => {
    if (isLabeled(data)) {
      setActive(data.activeLabel!);
    } else if (isUnlabeled(data)) {
      setActive(null);
    } else {
      setActive(undefined);
    }
  };

  const onClick: RechartsFunction = (data: Record<'activeLabel', string>) => {
    if (isLabeled(data)) {
      setSelected(data.activeLabel);
    } else if (isUnlabeled(data)) {
      setSelected(null);
    } else {
      setSelected(undefined);
    }
  };

  const renderProductInsightsTooltip: ContentRenderer<RechartsTooltipProps> = ({
    label,
    payload = [],
  }) => {
    /* Labels and payloads may be undefined or empty */
    if (isInvalid({ label, payload })) return null;

    /*
     *  recharts coerces null values to strings
     *  entity       -> resource       -> payload
     *  { id: null } -> { name: null } -> { label: '' }
     */
    const id = label === '' ? null : label;

    const title = titleOf(label);
    const items = payload.map(tooltipItemOf).filter(notEmpty);

    const activeEntity = findAlways(entities, e => e.id === id);
    const breakdowns = Object.keys(activeEntity.entities);

    if (breakdowns.length) {
      const subtitle = breakdowns
        .map(b => pluralize(b, activeEntity.entities[b].length, true))
        .join(', ');
      return (
        <BarChartTooltip
          title={title}
          subtitle={subtitle}
          topRight={
            !!activeEntity.change.ratio && (
              <CostGrowthIndicator
                formatter={formatChange}
                change={activeEntity.change}
                className={classes.indicator}
              />
            )
          }
          actions={
            <Box className={classes.actions}>
              <FullScreenIcon />
              <Typography>Click for breakdown</Typography>
            </Box>
          }
        >
          {items.map((item, index) => (
            <BarChartTooltipItem key={`${item.label}-${index}`} item={item} />
          ))}
        </BarChartTooltip>
      );
    }

    // If an entity doesn't have any sub-entities, there aren't any costs to break down.
    return (
      <BarChartTooltip
        title={title}
        topRight={
          !!activeEntity.change.ratio && (
            <CostGrowthIndicator
              formatter={formatChange}
              change={activeEntity.change}
              className={classes.indicator}
            />
          )
        }
        content={
          id
            ? null
            : "This product has costs that are not labeled and therefore can't be attributed to a specific entity."
        }
      >
        {items.map((item, index) => (
          <BarChartTooltipItem key={`${item.label}-${index}`} item={item} />
        ))}
      </BarChartTooltip>
    );
  };

  const barChartProps = isClickable ? { onClick } : {};

  return (
    <Box className={layoutClasses.wrapper}>
      <BarChartLegend costStart={costStart} costEnd={costEnd} options={options}>
        <LegendItem
          title={choose(['Cost Savings', 'Cost Excess'], entity.change)}
        >
          <CostGrowth change={entity.change} duration={duration} />
        </LegendItem>
      </BarChartLegend>
      <BarChart
        resources={resources}
        tooltip={renderProductInsightsTooltip}
        onMouseMove={onMouseMove}
        options={options}
        {...barChartProps}
      />
      {isSelected && entities.length && (
        <ProductEntityDialog
          open={isSelected}
          onClose={() => setSelected(undefined)}
          entity={findAlways(entities, e => e.id === selectLabel)}
          options={options}
        />
      )}
    </Box>
  );
}