<commit_before>import d3 from 'd3';
import { isEmpty } from 'ramda';
import map2tree from 'map2tree';
import deepmerge from 'deepmerge';
import { getTooltipString, toggleChildren, visit, getNodeGroupByDepthCount } from './utils';
import d3tooltip from 'd3tooltip';

const defaultOptions = {
  id: 'd3svg',
  style: {
    node: {
      colors: {
        'default': '#ccc',
        collapsed: 'lightsteelblue',
        parent: 'white'
      },
      radius: 5
    },
    text: {
      colors: {
        'default': 'black',
        hover: 'skyblue'
      }
    },
    link: {
      stroke: '#000',
      fill: 'none'
    }
  },
  size: 500,
  aspectRatio: 1.0,
  margin: {
    top: 10,
    right: 10,
    bottom: 10,
    left: 50
  },
  isSorted: false,
  heightBetweenNodesCoeff: 2,
  widthBetweenNodesCoeff: 1,
  transitionDuration: 750,
  state: undefined,
  rootKeyName: 'state',
  pushMethod: 'push',
  tree: undefined,
  tooltipOptions: {
    disabled: false,
    left: undefined,
    right: undefined,
    offset: {
      left: 0,
      top: 0
    },
    style: undefined
  }
};

export default function(DOMNode, options = {}) {
  const {
    id,
    style,
    size,
    aspectRatio,
    margin,
    isSorted,
    widthBetweenNodesCoeff,
    heightBetweenNodesCoeff,
    transitionDuration,
    state,
    rootKeyName,
    pushMethod,
    tree,
    tooltipOptions
    } = deepmerge(defaultOptions, options);

  const width = size - margin.left - margin.right;
  const height = size * aspectRatio - margin.top - margin.bottom;
  const fullWidth = size;
  const fullHeight = size * aspectRatio;

  const attr = {
    id,
    viewBox: `0 0 ${fullWidth} ${fullHeight}`,
    preserveAspectRatio: 'xMinYMin slice'
  };

  if (!style.width) {
    attr.width = fullWidth;
  }

  const root = d3.select(DOMNode);
  const vis = root
    .append('svg')
    .attr(attr)
    .style(style)
    .append('g')
    .attr({
      transform: `translate(${margin.left + style.node.radius}, ${margin.top})`
    });

  let layout = d3.layout.tree().size([width, height]);
  let data;

  if (isSorted) {
    layout.sort((a, b) => b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1);
  }

  return function renderChart(nextState = tree || state) {
    data = !tree ? map2tree(nextState, {key: rootKeyName, pushMethod}) : nextState;

    if (isEmpty(data) || !data.name) {
      data = { name: 'error', message: 'Please provide a state map or a tree structure'};
    }

    let nodeIndex = 0;
    let maxLabelLength = 0;

    visit(data,
        node => maxLabelLength = Math.max(node.name.length, maxLabelLength),
        node => node.children && node.children.length > 0 ? node.children : null
    );

    data.x0 = height / 2;
    data.y0 = 0;
    /*eslint-disable*/
    update(data);
    /*eslint-enable*/

    function update(source) {
      // path generator for links
      const diagonal = d3.svg.diagonal().projection(d => [d.y, d.x]);
      // set tree dimensions and spacing between branches and nodes
      const maxNodeCountByLevel = Math.max(...getNodeGroupByDepthCount(data));

      layout = layout.size([maxNodeCountByLevel * 25 * heightBetweenNodesCoeff, width]);

      let nodes = layout.nodes(data);
      let links = layout.links(nodes);

      nodes.forEach(node => node.y = node.depth * (maxLabelLength * 7 * widthBetweenNodesCoeff));

      // process the node selection
      let node = vis.selectAll('g.node').data(nodes, d => d.id || (d.id = ++nodeIndex));

      let nodeEnter = node.enter().append('g')
        .attr({
          'class': 'node',
          transform: d => `translate(${source.y0},${source.x0})`
        })
        .style({
          fill: style.text.colors.default,
          cursor: 'pointer'
        })
        .on({
          click: clickedNode => {
            if (d3.event.defaultPrevented) return;
            update(toggleChildren(clickedNode));
          },
          mouseover: function mouseover(d, i) {
            d3.select(this).style({
              fill: style.text.colors.hover
            });
          },
          mouseout: function mouseout(d, i) {
            d3.select(this).style({
              fill: style.text.colors.default
            });
          }
        });

      if (!tooltipOptions.disabled) {
        nodeEnter.call(d3tooltip(d3, 'tooltip', {...tooltipOptions, root})
          .text((d, i) => getTooltipString(d, i, tooltipOptions))
          .style(tooltipOptions.style)
        );
      }

      nodeEnter.append('circle')
        .attr({
          'class': 'nodeCircle'
        });

      nodeEnter.append('text')
        .attr({
          'class': 'nodeText',
          dy: '.35em'
        })
        .style({
          'fill-opacity': 0
        })
        .text(d => d.name);

      // update the text to reflect whether node has children or not
      node.select('text')
        .attr({
          x: d => d.children || d._children ? -(style.node.radius + 10) : style.node.radius + 10,
          'text-anchor': d => d.children || d._children ? 'end' : 'start'
        })
        .text(d => d.name);

      // change the circle fill depending on whether it has children and is collapsed
      node.select('circle.nodeCircle')
        .attr({
          r: style.node.radius
        })<commit_msg>Click on circle to collapse Add onClickText hook<commit_after>import d3 from 'd3';
import { isEmpty } from 'ramda';
import map2tree from 'map2tree';
import deepmerge from 'deepmerge';
import { getTooltipString, toggleChildren, visit, getNodeGroupByDepthCount } from './utils';
import d3tooltip from 'd3tooltip';

const defaultOptions = {
  id: 'd3svg',
  style: {
    node: {
      colors: {
        'default': '#ccc',
        collapsed: 'lightsteelblue',
        parent: 'white'
      },
      radius: 5
    },
    text: {
      colors: {
        'default': 'black',
        hover: 'skyblue'
      }
    },
    link: {
      stroke: '#000',
      fill: 'none'
    }
  },
  size: 500,
  aspectRatio: 1.0,
  margin: {
    top: 10,
    right: 10,
    bottom: 10,
    left: 50
  },
  isSorted: false,
  heightBetweenNodesCoeff: 2,
  widthBetweenNodesCoeff: 1,
  transitionDuration: 750,
  state: undefined,
  rootKeyName: 'state',
  pushMethod: 'push',
  tree: undefined,
  tooltipOptions: {
    disabled: false,
    left: undefined,
    right: undefined,
    offset: {
      left: 0,
      top: 0
    },
    style: undefined
  },
  onClickText: () => {}
};

export default function(DOMNode, options = {}) {
  const {
    id,
    style,
    size,
    aspectRatio,
    margin,
    isSorted,
    widthBetweenNodesCoeff,
    heightBetweenNodesCoeff,
    transitionDuration,
    state,
    rootKeyName,
    pushMethod,
    tree,
    tooltipOptions,
    onClickText
    } = deepmerge(defaultOptions, options);

  const width = size - margin.left - margin.right;
  const height = size * aspectRatio - margin.top - margin.bottom;
  const fullWidth = size;
  const fullHeight = size * aspectRatio;

  const attr = {
    id,
    viewBox: `0 0 ${fullWidth} ${fullHeight}`,
    preserveAspectRatio: 'xMinYMin slice'
  };

  if (!style.width) {
    attr.width = fullWidth;
  }

  const root = d3.select(DOMNode);
  const vis = root
    .append('svg')
    .attr(attr)
    .style(style)
    .append('g')
    .attr({
      transform: `translate(${margin.left + style.node.radius}, ${margin.top})`
    });

  let layout = d3.layout.tree().size([width, height]);
  let data;

  if (isSorted) {
    layout.sort((a, b) => b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1);
  }

  return function renderChart(nextState = tree || state) {
    data = !tree ? map2tree(nextState, {key: rootKeyName, pushMethod}) : nextState;

    if (isEmpty(data) || !data.name) {
      data = { name: 'error', message: 'Please provide a state map or a tree structure'};
    }

    let nodeIndex = 0;
    let maxLabelLength = 0;

    visit(data,
        node => maxLabelLength = Math.max(node.name.length, maxLabelLength),
        node => node.children && node.children.length > 0 ? node.children : null
    );

    data.x0 = height / 2;
    data.y0 = 0;
    /*eslint-disable*/
    update(data);
    /*eslint-enable*/

    function update(source) {
      // path generator for links
      const diagonal = d3.svg.diagonal().projection(d => [d.y, d.x]);
      // set tree dimensions and spacing between branches and nodes
      const maxNodeCountByLevel = Math.max(...getNodeGroupByDepthCount(data));

      layout = layout.size([maxNodeCountByLevel * 25 * heightBetweenNodesCoeff, width]);

      let nodes = layout.nodes(data);
      let links = layout.links(nodes);

      nodes.forEach(node => node.y = node.depth * (maxLabelLength * 7 * widthBetweenNodesCoeff));

      // process the node selection
      let node = vis.selectAll('g.node').data(nodes, d => d.id || (d.id = ++nodeIndex));

      let nodeEnter = node.enter().append('g')
        .attr({
          'class': 'node',
          transform: d => `translate(${source.y0},${source.x0})`
        })
        .style({
          fill: style.text.colors.default,
          cursor: 'pointer'
        })
        .on({
          mouseover: function mouseover(d, i) {
            d3.select(this).style({
              fill: style.text.colors.hover
            });
          },
          mouseout: function mouseout(d, i) {
            d3.select(this).style({
              fill: style.text.colors.default
            });
          }
        });

      if (!tooltipOptions.disabled) {
        nodeEnter.call(d3tooltip(d3, 'tooltip', {...tooltipOptions, root})
          .text((d, i) => getTooltipString(d, i, tooltipOptions))
          .style(tooltipOptions.style)
        );
      }

      nodeEnter.append('circle')
        .attr({
          'class': 'nodeCircle'
        })
        .on({
          click: clickedNode => {
            if (d3.event.defaultPrevented) return;
            update(toggleChildren(clickedNode));
          }
        });

      nodeEnter.append('text')
        .attr({
          'class': 'nodeText',
          dy: '.35em'
        })
        .style({
          'fill-opacity': 0
        })
        .text(d => d.name)
        .on({
          click: onClickText
        });

      // update the text to reflect whether node has children or not
      node.select('text')
        .attr({
          x: d => d.children || d._children ? -(style.node.radius + 10) : style.node.radius + 10,
          'text-anchor': d => d.children || d._children ? 'end' : 'start'
        })
        .text(d => d.name);

      // change the circle fill depending on whether it has children and is collapsed
      node.select('circle.nodeCircle')
        .attr({
          r: style.node.radius
        })
{'fraction_non_alphanumeric': 0.14218852382289618, 'fraction_numerical': 0.011794439764111205, 'mean_word_length': 2.146980854197349, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 32, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5684865', 'n_tokens_mistral': 3452, 'n_tokens_neox': 3335, 'n_words': 795}