All files / src/components/LineChart/eventAnnotation EventAnnotation.tsx

9.37% Statements 6/64
0% Branches 0/27
0% Functions 0/11
9.09% Lines 5/55

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 13522x   22x 22x     22x                 22x                                                                                                                                                                                                                                              
import * as React from 'react';
import { ScaleTime } from 'd3-scale';
import { findIndex } from '@fluentui/react/lib/Utilities';
import { ILineDef, LabelLink, ILabelDef } from './LabelLink';
import { IEventsAnnotationProps } from '../LineChart.types';
import { ITheme } from '@fluentui/react/lib/Styling';
import { getColorFromToken } from '../../../utilities/colors';
 
interface IEventsAnnotationExtendProps extends IEventsAnnotationProps {
  scale: ScaleTime<number, number>;
  chartYBottom: number;
  chartYTop: number;
  theme: ITheme | undefined;
}
 
export const EventsAnnotation: React.FunctionComponent<IEventsAnnotationExtendProps> = props => {
  const textWidth = props.labelWidth ? props.labelWidth : 105;
  const textY = props.chartYTop - 20;
  const lineTopY = textY + 7;
  const textPadding = 5;
  const lineHeight = 18;
  const fontSize = '10pt';
  const axisRange = props.scale.range();
 
  const lineDefs: ILineDef[] = props.events.map(e => ({ ...e, x: props.scale(e.date) }));
 
  lineDefs.sort((e1, e2) => +e1.date - +e2.date);
 
  const fill: string | undefined = props.strokeColor
    ? getColorFromToken(props.strokeColor, props.theme?.isInverted)
    : props.theme?.palette.black;
 
  const lines = uniqBy(lineDefs, x => x.date.toString()).map((x, i) => (
    <line key={i} x1={x.x} x2={x.x} y1={lineTopY} y2={props.chartYBottom} stroke={fill} strokeDasharray="8" />
  ));
 
  const labelLinks = calculateLabels(lineDefs, textWidth + textPadding, axisRange[1], axisRange[0]).map((x, i) => (
    <LabelLink
      key={i}
      theme={props.theme}
      {...{
        lineDefs,
        labelDef: x,
        textY,
        textWidth,
        textLineHeight: lineHeight,
        textFontSize: fontSize,
        textColor: props.labelColor,
        mergedLabel: props.mergedLabel,
      }}
    />
  ));
 
  return (
    <>
      {lines}
      {labelLinks}
    </>
  );
};
 
function calculateLabels(lineDefs: ILineDef[], textWidth: number, maxX: number, minX: number): ILabelDef[] {
  const calculateLabel = (lastX: number, currentIdx: number): ILabelDef[] => {
    // base case 1
    Iif (currentIdx === lineDefs.length) {
      return [];
    }
 
    const { x } = lineDefs[currentIdx];
    const leftXBoundary = x - textWidth;
 
    // cannot render on top of other text
    Iif (x < lastX) {
      return [];
    }
 
    // base case 2
    Iif (currentIdx === lineDefs.length - 1) {
      if (lastX < leftXBoundary) {
        return [{ x: x, anchor: 'end', aggregatedIdx: [currentIdx] }];
      } else Iif (x + textWidth < maxX) {
        return [{ x: x, anchor: 'start', aggregatedIdx: [currentIdx] }];
      }
 
      return [];
    }
 
    if (lastX < leftXBoundary) {
      // label on left side
      return backtrack(currentIdx, 'end');
    } else {
      // label on right side
      return backtrack(currentIdx, 'start');
    }
  };
 
  const backtrack = (currentIdx: number, anchor: 'start' | 'end'): ILabelDef[] => {
    const bd = anchor === 'end' ? lineDefs[currentIdx].x : lineDefs[currentIdx].x + textWidth;
 
    let idx = findIndex(
      lineDefs,
      ds => ds.x > bd && (ds.x - textWidth >= bd || ds.x + textWidth < maxX),
      currentIdx + 1,
    );
    Iif (idx === -1) {
      idx = lineDefs.length;
    }
 
    const aggregatedIdx: number[] = [];
    for (let i = currentIdx; i < idx; i++) {
      aggregatedIdx.push(i);
    }
    const next = calculateLabel(bd, idx);
 
    next.unshift({ x: lineDefs[currentIdx].x, anchor, aggregatedIdx });
    return next;
  };
 
  return calculateLabel(minX, 0);
}
 
/** Get unique items of `arr`, comparing based on the result of calling `iteratee` on each item. */
function uniqBy<T>(arr: T[], iteratee: (x: T) => string): T[] {
  const seen: string[] = [];
  const result: T[] = [];
  for (const x of arr) {
    const comp = iteratee(x);
    Iif (seen.indexOf(comp) === -1) {
      result.push(x);
      seen.push(comp);
    }
  }
  return result;
}