Link copied to clipboard!
Mobile navigation button - closed state

Integrate with popular libraries

Using our annotations API, you can easily integrate with libraries for rendering charts, maps, text editors, and more. This section contains sample code for integrating with several popular libraries.


Highcharts

Highchart annotated from the Cord sidebar

import Highcharts from "highcharts";
import HighchartsReact from "highcharts-react-official";
import {
  useCordAnnotationTargetRef,
  useCordAnnotationCaptureHandler,
  useCordAnnotationClickHandler,
  useCordAnnotationRenderer,
} from "@cord-sdk/react";

type HighchartsAnnotationLocation = {
  section: string;
  series: number;
  x: number;
  y: number;
};

// this must be rendered downstream of a CordProvider
function HighchartsComponent() {
  const highchartsRef = useRef<HighchartsReact.RefObject>(null);

  const location = { section: "chart" };

  const annotationTargetRef = useCordAnnotationTargetRef<
    HTMLDivElement,
    HighchartsAnnotationLocation
  >(location);

  useCordAnnotationCaptureHandler<HighchartsAnnotationLocation>(
    location,
    ({ x, y }) => {
      const chart = highchartsRef.current?.chart;
      if (chart) {
        const { plotLeft, plotTop } = chart;
        const plotRelativeX = x - plotLeft;
        const plotRelativeY = y - plotTop;

        let minDistance = Infinity;
        let closestPoint: Highcharts.Point | undefined;

        for (const series of chart.series) {
          for (const point of series.points) {
            const { plotX, plotY } = point;
            if (plotX && plotY) {
              const distance = planarDistance(
                plotX,
                plotY,
                plotRelativeX,
                plotRelativeY
              );
              if (distance < minDistance) {
                minDistance = distance;
                closestPoint = point;
              }
            }
          }
        }

        if (closestPoint) {
          return {
            extraLocation: {
              x: closestPoint.x,
              y: closestPoint.y,
              series: closestPoint.series.index,
            },
            label: `${closestPoint.series.name}: ${closestPoint.x}`,
          };
        }
      }
    }
  );

  useCordAnnotationRenderer<HighchartsAnnotationLocation>(
    location,
    (annotation) => {
      const chart = highchartsRef.current?.chart;
      const container = highchartsRef.current?.container?.current;

      if (chart && container) {
        if (!chart.series[annotation.location.series].visible) {
          return;
        }

        return {
          element: container,
          coordinates: {
            x: chart.xAxis[0].toPixels(annotation.location.x, false),
            y: chart.yAxis[0].toPixels(annotation.location.y, false),
          },
        };
      }
    }
  );

  useCordAnnotationClickHandler<HighchartsAnnotationLocation>(
    location,
    (annotation) => {
      highchartsRef.current?.chart.series[
        annotation.location.series
      ].setVisible(true);
    }
  );

  return (
    <div ref={annotationTargetRef}>
      <HighchartsReact
        ref={highchartsRef}
        highcharts={Highcharts}
        options={{
          title: {
            text: "Solar Employment Growth by Sector, 2010-2016",
          },

          subtitle: {
            text: "Source: thesolarfoundation.com",
          },

          yAxis: {
            title: {
              text: "Number of Employees",
            },
          },

          xAxis: {
            accessibility: {
              rangeDescription: "Range: 2010 to 2017",
            },
          },

          legend: {
            layout: "vertical",
            align: "right",
            verticalAlign: "middle",
          },

          plotOptions: {
            series: {
              label: {
                connectorAllowed: false,
              },
              pointStart: 2010 + startIndex,
            },
          },

          series: [
            {
              name: "Installation",
              data: [43934, 52503, 57177, 69658, 97031, 119931, 137133, 154175],
            },
            {
              name: "Manufacturing",
              data: [24916, 24064, 29742, 29851, 32490, 30282, 38121, 40434],
            },
            {
              name: "Sales & Distribution",
              data: [11744, 17722, 16005, 19771, 20185, 24377, 32147, 39387],
            },
            {
              name: "Project Development",
              data: [null, null, 7988, 12169, 15112, 22452, 34400, 34227],
            },
            {
              name: "Other",
              data: [12908, 5948, 8105, 11248, 8989, 11816, 18274, 18111],
            },
          ],
        }}
      />
    </div>
  );
}

Chart.js: Bar chart

Chart.js bar chart annotated from the Cord sidebar

import React, { useRef } from "react";
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend,
} from "chart.js";
import type { ChartMeta } from "chart.js";
import { Bar } from "react-chartjs-2";
import {
  useCordAnnotationCaptureHandler,
  useCordAnnotationClickHandler,
  useCordAnnotationRenderer,
  useCordAnnotationTargetRef,
} from "opensource/cord-sdk/packages/react";
import type { AnnotationCapturePosition } from "opensource/cord-sdk/packages/types";

ChartJS.register(
  CategoryScale,
  LinearScale,
  BarElement,
  Title,
  Tooltip,
  Legend
);

type DataPoint = {
  x: number;
  y: number;
};

type BarChartAnnotation = {
  page: string;
  element: string;
  x: number;
  y: number;
  datasetIndex: number;
  barElementIndex: number;
};

export const BarChart = () => {
  const barChartRef = useRef<ChartJS<"bar">>();
  const location = { element: "barchart" };

  const barChartContainerRef =
    useCordAnnotationTargetRef<HTMLDivElement>(location);

  useCordAnnotationCaptureHandler<BarChartAnnotation>(
    location,
    (annotation) => {
      // The pin will attach to the top of any bar element clicked by the user.
      // Annotations created outside of a bar element will appear at the clicked location.
      const chart = barChartRef.current;

      if (!chart) {
        return;
      }

      const datasets = chart.getSortedVisibleDatasetMetas();
      const clickedElementData = findClickedElement(datasets, annotation);

      if (!clickedElementData) {
        return;
      }

      const {
        datasetLabel,
        datasetIndex,
        clickedElement,
        clickedElementIndex,
      } = clickedElementData;

      chart.setActiveElements([{ datasetIndex, index: barElementIndex }]);
      chart.update();

      return {
        extraLocation: {
          x: clickedElement.x,
          y: clickedElement.y,
          datasetIndex,
          barElementIndex: clickedElementIndex,
        },
        label: `x: ${chart.scales.x.getLabelForValue(
          clickedElementIndex
        )}, y: ${clickedElement.y}, ${datasetLabel}`,
      };
    }
  );

  useCordAnnotationClickHandler<BarChartAnnotation>(location, (annotation) => {
    const chart = barChartRef.current;

    if (chart) {
      const {
        location: { barElementIndex, datasetIndex },
      } = annotation;

      const dataset = chart.getDatasetMeta(datasetIndex);

      if (dataset.hidden) {
        chart.show(datasetIndex);
      }
      chart.setActiveElements([{ datasetIndex, index: barElementIndex }]);

      chart.update();
    }
  });

  useCordAnnotationRenderer<BarChartAnnotation>(location, (annotation) => {
    const chart = barChartRef.current;

    if (!chart || !barChartContainerRef.current) {
      return;
    }

    const {
      location: { barElementIndex, datasetIndex },
    } = annotation;

    const dataset = chart.getDatasetMeta(datasetIndex);

    if (dataset.hidden) {
      return;
    }
    const datasetData = dataset.data[barElementIndex];

    if (!datasetData) {
      return;
    }

    return {
      coordinates: { x: datasetData.x, y: datasetData.y },
      element: barChartContainerRef.current,
    };
  });

  return (
    <div ref={barChartContainerRef}>
      <Bar options={OPTIONS} data={DATA} ref={barChartRef} />
    </div>
  );
};

const OPTIONS = {
  responsive: true,
  plugins: {
    title: {
      display: true,
      text: "Cord Annotations - Chart.js Bar Chart",
    },
  },
};

const LABELS = ["January", "February", "March", "April", "May", "June", "July"];
const DATASET1 = [123, 543, 634, 975, 234, 84, 99];
const DATASET2 = [973, 125, 385, 442, 487, 192, 827];

const DATA = {
  labels: LABELS,
  datasets: [
    {
      label: "Dataset 1",
      data: DATASET1,
      backgroundColor: "rgba(255, 99, 132, 0.5)",
      hoverBorderColor: "black",
    },
    {
      label: "Dataset 2",
      data: DATASET2,
      backgroundColor: "rgba(53, 162, 235, 0.5)",
      hoverBorderColor: "black",
    },
  ],
};

function findClickedElement(
  datasets: ChartMeta[],
  annotation: AnnotationCapturePosition
) {
  const { x, y } = annotation;

  for (const dataset of datasets) {
    for (const [barElementIndex, data] of dataset.data.entries()) {
      //@ts-ignore
      // Checks if the user click is within any bar elements
      if (data.inRange(x, y, true)) {
        const clickedElement = dataset._parsed[barElementIndex];

        if (!isDataPoint(clickedElement)) {
          return null;
        }

        return {
          datasetLabel: dataset.label,
          datasetIndex: dataset.index,
          clickedElement,
          clickedElementIndex: barElementIndex,
        };
      }
    }
  }

  return null;
}

function isDataPoint(obj: any): obj is DataPoint {
  return typeof obj === "object" && obj !== null && "x" in obj && "y" in obj;
}

Chart.js: Scatter chart

Chart.js scatter chart annotated from the Cord sidebar

import React, { useRef } from "react";
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  Title,
  Tooltip,
  Legend,
  PointElement,
  LineElement,
} from "chart.js";
import { Scatter } from "react-chartjs-2";
import {
  useCordAnnotationCaptureHandler,
  useCordAnnotationClickHandler,
  useCordAnnotationRenderer,
  useCordAnnotationTargetRef,
} from "opensource/cord-sdk/packages/react";
import type { AnnotationCapturePosition } from "opensource/cord-sdk/packages/types";

ChartJS.register(
  CategoryScale,
  LinearScale,
  Title,
  Tooltip,
  Legend,
  PointElement,
  LineElement
);

type DataPoint = {
  x: number;
  y: number;
};

type ScatterPointAnnotation = {
  page: string;
  element: string;
  x: number;
  y: number;
  datasetIndex: number;
  pointIndex: number;
};

export const ScatterChart = () => {
  const location = { element: "scatterchart" };
  const scatterChartRef = useRef<ChartJS<"scatter">>();
  const scatterChartContainerRef =
    useCordAnnotationTargetRef<HTMLDivElement>(location);

  useCordAnnotationCaptureHandler<ScatterPointAnnotation>(
    location,
    (annotation) => {
      // The pin will be attached to the closest data point relative to the location of the user click.
      const chart = scatterChartRef.current;

      if (!chart) {
        return;
      }

      const { x, y } = annotation;

      let shortestDistance: number = Infinity;
      let datasetIndex: number | undefined;
      let closestDataPointIndex: number | undefined;

      const datasets = chart.getSortedVisibleDatasetMetas();

      for (const dataset of datasets) {
        for (const [j, data] of dataset.data.entries()) {
          const distanceFromPin = calcDistanceBetweenCoordinates(
            { x, y },
            { x: data.x, y: data.y }
          );

          if (distanceFromPin < shortestDistance) {
            shortestDistance = distanceFromPin;
            closestDataPointIndex = j;
            datasetIndex = dataset.index;
          }
        }
      }

      if (closestDataPointIndex === undefined || datasetIndex === undefined) {
        return;
      }

      const correctDataset = datasets.find(
        (dataset) => dataset.index === datasetIndex
      );

      const parsed = correctDataset?._parsed[closestDataPointIndex];

      if (!isDataPoint(parsed)) {
        return;
      }

      const { x: scatterPointX, y: scatterPointY } = parsed;

      chart.setActiveElements([{ datasetIndex, index: closestDataPointIndex }]);
      chart.update();

      return {
        extraLocation: {
          x: scatterPointX,
          y: scatterPointY,
          datasetIndex,
          pointIndex: closestDataPointIndex,
        },
        label: `x: ${scatterPointX}, y: ${scatterPointY}, ${correctDataset?.label}`,
      };
    }
  );

  useCordAnnotationClickHandler<ScatterPointAnnotation>(
    location,
    (annotation) => {
      const chart = scatterChartRef.current;

      if (chart) {
        const {
          location: { pointIndex, datasetIndex },
        } = annotation;

        const dataset = chart.getDatasetMeta(datasetIndex);

        if (dataset.hidden) {
          chart.show(datasetIndex);
        }

        chart.setActiveElements([{ datasetIndex, index: pointIndex }]);

        chart.update();
      }
    }
  );

  useCordAnnotationRenderer<ScatterPointAnnotation>(location, (annotation) => {
    const chart = scatterChartRef.current;

    if (!chart || !scatterChartContainerRef.current) {
      return;
    }

    const {
      location: { x, y, datasetIndex },
    } = annotation;

    const dataset = chart.getDatasetMeta(datasetIndex);

    if (dataset.hidden) {
      return;
    }

    return {
      coordinates: {
        x: chart.scales.x.getPixelForValue(x),
        y: chart.scales.y.getPixelForValue(y),
      },
      element: scatterChartContainerRef.current,
    };
  });

  return (
    <div ref={scatterChartContainerRef}>
      <Scatter options={OPTIONS} data={DATA} ref={scatterChartRef} />
    </div>
  );
};

const OPTIONS = {
  scales: {
    y: {
      beginAtZero: true,
    },
  },
  plugins: {
    title: {
      display: true,
      text: "Cord Annotations - Chart.js Scatter Chart",
    },
  },
};

const DATA = {
  datasets: [
    {
      label: "Dataset A",
      // Generate or import dummy data
      data: scatterChartDatasetA,
      backgroundColor: "rgba(255, 99, 132, 1)",
      pointRadius: 5,
      pointHitRadius: 5,
      pointHoverRadius: 7,
    },
    {
      label: "Dataset B",
      // Generate or import dummy data
      data: scatterChartDatasetB,
      backgroundColor: "rgb(75, 192, 192)",
      pointRadius: 5,
      pointHitRadius: 5,
      pointHoverRadius: 7,
    },
  ],
};

function calcDistanceBetweenCoordinates(a: DataPoint, b: DataPoint) {
  return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}

function isDataPoint(obj: any): obj is DataPoint {
  return typeof obj === "object" && obj !== null && "x" in obj && "y" in obj;
}

Monaco Editor

Monaco editor annotated from the Cord sidebar

import React, { useRef } from "react";

import type * as monaco from "monaco-editor";
import Editor from "@monaco-editor/react";

type MonacoLocation = {
  target: string;
  lineNumber?: number;
};

const LOCATION: MonacoLocation = {
  target: "monaco-editor",
};

function MonacoEditorExample() {
  const editorWrapperRef = useCordAnnotationTargetRef<HTMLDivElement>(LOCATION);
  const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);

  // Retrieving the editor instance, to be able to e.g. scroll to a certain line.
  function handleEditorDidMount(editor: monaco.editor.IStandaloneCodeEditor) {
    editorRef.current = editor;

    editor.onDidScrollChange(() => {
      redrawAnnotations();
    });
  }

  // Runs when users click on the screen to annotate.
  useCordAnnotationCaptureHandler(LOCATION, (position) => {
    const { current: editor } = editorRef;
    if (!editor) {
      return;
    }

    const { x, y } = position.element.getBoundingClientRect();
    try {
      const target = editor.getTargetAtClientPoint(
        position.x + x,
        position.y + y
      )?.position;

      if (!target) {
        return;
      }

      const { lineNumber, column } = target;
      const lineContent = editor.getModel()?.getLineContent(lineNumber);
      return {
        // Storing the line number so later we can scroll back to it,
        // and the column to position the pin in the right place horizontally.
        extraLocation: { lineNumber, column },
        // Showing the entire line content as the annotation label.
        label: lineContent,
        // label could be any string though, e.g.
        // label: `line: ${lineNumber}`;
      };
    } catch (e) {
      console.error(e);
      return;
    }
  });

  const { redrawAnnotations } = useCordAnnotationRenderer(
    LOCATION,
    (annotation) => {
      const { current: editor } = editorRef;
      if (!editor) {
        return;
      }

      const { lineNumber, column } = annotation.location;
      const lineCount = editor.getModel()?.getLineCount() ?? 0;
      if (lineNumber > lineCount) {
        return;
      }

      const top =
        editor.getTopForLineNumber(lineNumber + 1) - editor.getScrollTop();
      return {
        coordinates: {
          x:
            // `contentLeft` tells us how many px from the left the actual line content starts
            editor.getLayoutInfo().contentLeft +
            // How many px from the beginning of the actual line
            editor.getOffsetForColumn(lineNumber, column) -
            editor.getScrollLeft(),
          y: top,
        },
        element: editorWrapperRef.current ?? undefined,
      };
    }
  );

  // Runs when a user clicks on the annotation pill within the sidebar.
  useCordAnnotationClickHandler(LOCATION, (annotation) => {
    const { current: editor } = editorRef;
    if (!editor) {
      return;
    }

    editor.revealLineInCenter(annotation.location.lineNumber);
  });

  return (
    <div ref={editorWrapperRef}>
      <Editor
        height="90vh"
        defaultLanguage="javascript"
        // Filling the editor with some dummy lines
        defaultValue={new Array(150)
          .fill(`function foo() { \tconsole.log("bar"); }`)
          .join("\n")}
        onMount={handleEditorDidMount}
      />
    </div>
  );
}

Visx: Geo Mercator

Visx: Geo Mercator map annotated from the Cord sidebar

...
const Country = ({ feature, path }: { feature: any; path: any }) => {
  const classes = useStyles();

  const location = {
    element: 'worldmap',
    country: feature.properties.name,
  };

  const [highlighted, setHighlighted] = useState(false);

  const countryRef = useCordAnnotationTargetRef<HTMLElement & SVGPathElement>(
    location,
  );

  const isMountedRef = useRef(false);

  useEffect(() => {
    isMountedRef.current = true;

    return () => {
      isMountedRef.current = false;
    };
  }, []);

  const highlightCountry = useCallback(() => {
    setHighlighted(true);
    setTimeout(() => {
      if (isMountedRef.current === true) {
        setHighlighted(false);
      }
    }, 1500);
  }, []);

  useCordAnnotationCaptureHandler(location, () => {
    if (!countryRef.current) {
      return;
    }

    highlightCountry();
    return {
      label: feature.properties.name,
    };
  });

  useCordAnnotationClickHandler(location, () => {
    if (countryRef.current) {
      highlightCountry();
    }
  });

  useCordAnnotationRenderer(location, () => {
    if (!countryRef.current) {
      return;
    }
    // Adjusting the default pin location
    if (feature.properties.name === 'Russia') {
      return {
        coordinates: { x: '77%', y: '65%' },
        element: countryRef.current,
      };
    }
    if (feature.properties.name === 'United States') {
      return {
        coordinates: { x: '22%', y: '72%' },
        element: countryRef.current,
      };
    }
    return { element: countryRef.current };
  });

  return (
    <path
      className={cx(classes.default, {
        [classes.highlightedCountry]: highlighted,
      })}
      ref={countryRef}
      d={path || ''}
      fill={color(feature.geometry.coordinates.length)}
      stroke={BACKGROUND_COLOR}
      strokeWidth={0.5}
    />
  );
};

export const WorldMap = ({ width, height }: GeoMercatorProps) => {
  const classes = useStyles();
  const centerX = width / 2;
  const centerY = height / 2;
  const scale = (width / 630) * 100;

  return (
    <svg width={'100%'} className={classes.svgContainer}>
      <rect
        x={0}
        y={0}
        width={'100%'}
        height={'100%'}
        fill={BACKGROUND_COLOR}
        rx={14}
      />
      <Mercator
        data={world.features}
        scale={scale}
        translate={[centerX, centerY + 50]}
      >
        {(mercator) => (
          <g>
            <Graticule
              graticule={(g) => mercator.path(g) || ''}
              stroke="rgba(33,33,33,0.05)"
            />
            {mercator.features.map(({ feature, path }, i) => {
              return <Country key={i} feature={feature} path={path} />;
            })}
          </g>
        )}
      </Mercator>
    </svg>
  );
};

AG Grid

import React, { useRef } from "react";
import { AgGridReact } from "ag-grid-react";

type GridLocation = {
  section: string;
  row: string;
  column: string;
};

export const GridApp = () => {
  const location = { section: "grid" };

  const containerRef = useCordAnnotationTargetRef(location);

  useCordAnnotationCaptureHandler(location, (_position, element) => {
    const colId = element.closest("[role=gridcell]")?.getAttribute("col-id");
    const rowId = element.closest("[role=row]")?.getAttribute("row-id");

    if (rowId && colId) {
      return {
        extraLocation: {
          row: rowId,
          column: colId,
        },
      };
    }
  });

  useCordAnnotationRenderer<GridLocation>(location, (annotation) => {
    const { row, column } = annotation.location;
    return containerRef.current?.querySelector(
      `*[role='row'][row-id='${row}'] *[role='gridcell'][col-id='${column}']`
    );
  });

  const gridRef = useRef<AgGridReact>(null);

  return (
    <div ref={containerRef}>
      <AgGridReact ref={gridRef} rowData={ROW_DATA} columnDefs={COLUMNS} />
    </div>
  );
};

Learn more