airbnb/caravel

View on GitHub
superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js

Summary

Maintainability
B
4 hrs
Test Coverage
/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/* eslint-disable no-param-reassign, react/sort-prop-types */
import d3 from 'd3';
import PropTypes from 'prop-types';
import {
  getNumberFormatter,
  CategoricalColorNamespace,
} from '@superset-ui/core';

const propTypes = {
  data: PropTypes.shape({
    matrix: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)),
    nodes: PropTypes.arrayOf(PropTypes.string),
  }),
  width: PropTypes.number,
  height: PropTypes.number,
  colorScheme: PropTypes.string,
  numberFormat: PropTypes.string,
};

function Chord(element, props) {
  const { data, width, height, numberFormat, colorScheme, sliceId } = props;

  element.innerHTML = '';

  const div = d3.select(element);
  div.classed('superset-legacy-chart-chord', true);
  const { nodes, matrix } = data;
  const f = getNumberFormatter(numberFormat);
  const colorFn = CategoricalColorNamespace.getScale(colorScheme);

  const outerRadius = Math.min(width, height) / 2 - 10;
  const innerRadius = outerRadius - 24;

  let chord;

  const arc = d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius);

  const layout = d3.layout
    .chord()
    .padding(0.04)
    .sortSubgroups(d3.descending)
    .sortChords(d3.descending);

  const path = d3.svg.chord().radius(innerRadius);

  const svg = div
    .append('svg')
    .attr('width', width)
    .attr('height', height)
    .on('mouseout', () => chord.classed('fade', false))
    .append('g')
    .attr('id', 'circle')
    .attr('transform', `translate(${width / 2}, ${height / 2})`);

  svg.append('circle').attr('r', outerRadius);

  // Compute the chord layout.
  layout.matrix(matrix);

  const group = svg
    .selectAll('.group')
    .data(layout.groups)
    .enter()
    .append('g')
    .attr('class', 'group')
    .on('mouseover', (d, i) => {
      chord.classed('fade', p => p.source.index !== i && p.target.index !== i);
    });

  // Add a mouseover title.
  group.append('title').text((d, i) => `${nodes[i]}: ${f(d.value)}`);

  // Add the group arc.
  const groupPath = group
    .append('path')
    .attr('id', (d, i) => `group${i}`)
    .attr('d', arc)
    .style('fill', (d, i) => colorFn(nodes[i], sliceId, colorScheme));

  // Add a text label.
  const groupText = group.append('text').attr('x', 6).attr('dy', 15);

  groupText
    .append('textPath')
    .attr('xlink:href', (d, i) => `#group${i}`)
    .text((d, i) => nodes[i]);
  // Remove the labels that don't fit. :(
  groupText
    .filter(function filter(d, i) {
      return (
        groupPath[0][i].getTotalLength() / 2 - 16 < this.getComputedTextLength()
      );
    })
    .remove();

  // Add the chords.
  chord = svg
    .selectAll('.chord')
    .data(layout.chords)
    .enter()
    .append('path')
    .attr('class', 'chord')
    .on('mouseover', d => {
      chord.classed('fade', p => p !== d);
    })
    .style('fill', d => colorFn(nodes[d.source.index], sliceId, colorScheme))
    .attr('d', path);

  // Add an elaborate mouseover title for each chord.
  chord
    .append('title')
    .text(
      d =>
        `${nodes[d.source.index]} → ${nodes[d.target.index]}: ${f(
          d.target.value,
        )}\n${nodes[d.target.index]} → ${nodes[d.source.index]}: ${f(
          d.source.value,
        )}`,
    );
}

Chord.displayName = 'Chord';
Chord.propTypes = propTypes;

export default Chord;