<commit_before>"use strict";

let d3 = require('d3');
let colorbrewer = require('colorbrewer');
let Changesets = require('Changesets');
let _ = require('lodash');

// Calculates where points on the circumference of a circle lie.
class Circle {
  constructor(cx, cy, r) {
    this.cx = cx;
    this.cy = cy;
    this.r = r;
  }
  at(frac) {
    let radian = frac * 2 * Math.PI;
    return {
      x: _.round(this.cx + this.r * Math.sin(radian), 2),
      y: _.round(this.cy - this.r * Math.cos(radian), 2),
    };
  }
}

class Markers {
  constructor(defs) {
    this.defs = defs;
    this.added = [];
  }
  has(id) {
    return _.includes(this.added, id);
  }
  append(id) {
    return this.defs.append('marker')
      .attr('id', id);
  }
  get(id) {
    return this.defs.select(`marker#${id}`);
  }
  ref(id) {
    return `url(#${id})`;
  }
}

class Transform {
  constructor() {
    this.operations = [];
  }
  toString() {
    return _.reverse(_.clone(this.operations)).join(' ');
  }
  translate(x, y) {
    this.operations.push(`translate(${x}, ${y})`);
    return this;
  };
  rotate(angle) {
    this.operations.push(`rotate(${angle})`);
    return this;
  }
}

let View = function(controller, svg, module) {

svg = d3.select(svg)
  .classed('raft', true)
  .append('g');


let messageTypes = ['RequestVote', 'AppendEntries'];
let messageColor = d3.scale.category10().domain(messageTypes).range(colorbrewer.Set2[6]);
let termColor = d3.scale.category10().range(colorbrewer.Set2[6]);

let markers = new Markers(svg.append('defs'));

let createArrow = selection =>
  selection
    .attr({
      viewBox: '0 -5 10 10',
      refX: 5,
      refY: 0,
      markerWidth: 4,
      markerHeight: 4,
      orient:'auto',
    })
    .append('path')
      .attr('d', 'M0,-5L10,0L0,5')
      .attr('class', 'arrowHead');

markers.append('arrow')
  .call(createArrow);
messageTypes.forEach(t => {
  markers.append(`arrow-${t}`)
    .call(createArrow)
    .style('stroke', messageColor(t))
    .style('fill', messageColor(t));
});

d3.select('head').append('style')
  .text(`
      .raft {
        font-size: 60;
      }
      .raft circle.ring {
        stroke: black;
        stroke-width: 5;
        fill: none;
      }
      .raft .leader.current text.serverId {
        fill: red;
      }
      .raft .legend {
        font-size: 40;
      }
      .raft .noleader .legend {
        visibility: hidden;
      }

      .raft .server .serverbg {
        stroke: black;
      }
      .raft .server g.votes {
        visibility: hidden;
      }
      .raft .server.candidate g.votes {
        visibility: visible;
        fill: white;
      }
      .raft .server.candidate g.votes .granted {
        fill: black;
      }

      .raft .message {
        stroke-width: 4;
        stroke: black;
        fill: black;
      }
      .raft .message.RequestVote {
        stroke: ${messageColor('RequestVote')};
        fill: ${messageColor('RequestVote')};
      }
      .raft .message.AppendEntries {
        stroke: ${messageColor('AppendEntries')};
        fill: ${messageColor('AppendEntries')};
      }
      .raft .message line.direction {
        stroke-width: 5;
      }
      .playback .raft .message line.direction {
        visibility: hidden;
      }
      .raft .message line.plus {
        visibility: hidden;
      }
      .raft .message.RequestVote.response circle,
      .raft .message.AppendEntries.request.empty circle,
      .raft .message.AppendEntries.response circle {
        fill: none;
      }
      .raft .message.RequestVote.response line.plus.horizontal,
      .raft .message.AppendEntries.response line.plus.horizontal {
        visibility: visible;
      }
      .raft .message.RequestVote.response.granted line.plus.vertical,
      .raft .message.AppendEntries.response.success line.plus.vertical {
        visibility: visible;
      }

      .raft .logs .bg rect {
        fill: #dddddd;
        stroke: gray;
      }
      .raft .logs .entry rect {
        stroke: black;
        stroke-width: 8;
      }
      .raft .logs .entry.uncommitted rect {
        stroke-dasharray: 20, 15;
      }

      .raft line.nextIndex {
        stroke-width: 6;
        stroke: black;
      }

      .raft .noLeader .nextIndex,
      .raft .noLeader .matchIndex,
      .raft .leader.current .nextIndex,
      .raft .leader.current .matchIndex {
        visibility: hidden;
      }
  `);

let model = module.env;

let numServers = model.vars.get('servers').size();
let numIndexes = model.vars.get('servers').index(1).lookup('log').capacity();
let electionTimeout = 100000;
let ringLayout = new Circle(250, 500, 200);
let serverLabelCircle = new Circle(250, 500, 300);
let peersCircle = new Circle(0, 0, 40); // relative to server midpoint

// The current leader is defined as the leader of the largest term, or
// undefined if no leaders. This is its server ID.
let currentLeaderId = undefined;


// Wraps the model's Server Record with additional information for drawing
class Server {
  constructor(serverId, serverVar) {
    this.serverVar = serverVar;
    this.serverId = serverId;
    this.frac = (this.serverId - 1) / numServers;
    this.point = ringLayout.at(this.frac);
    this.labelPoint = serverLabelCircle.at(this.frac);

    this.timeoutArc = d3.svg.arc()
      .innerRadius(50)
      .outerRadius(60)
      .startAngle(0)
      .endAngle(0);
  }

  getVotes() {
    return this.serverVar.lookup('state').match({
      Candidate: cstate => cstate.peers.map(peer => {
        if (peer.lookup('voteGranted').toString() == 'True') {
          return 'granted';
        } else if (peer.lookup('voteResponded').toString() == 'True') {
          return 'denied';
        } else {
          return 'noreply';
        }
      }),
      Follower: model.vars.get('servers').map(s => 'N/A'),
      Leader: model.vars.get('servers').map(s => 'N/A'),
    });
  }

  stateClasses() {
    let classes = [];
    this.serverVar.lookup('state').match({
      Follower: () => {
        classes.push('follower');
      },
      Candidate: () => {
        classes.push('candidate');
      },
      Leader: () => {
        classes.push('leader');
        if (currentLeaderId === this.serverId) {
          classes.push('current');
        } else {
          classes.push('stale');
        }
      },
    });
    return classes.join(' ');
  }

  update(clock) {
    let timeoutFrac = ((this.serverVar.lookup('timeoutAt').value - clock) /
       (electionTimeout * 2));
    this.serverVar.lookup('state').match({
      Leader: () => { timeoutFrac = 1; },
    });
    this.timeoutArc.endAngle(2 * Math.PI * timeoutFrac);
    return this;
  }
}

let serverData = model.vars.get('servers').map((v, id) => new Server(id, v));

class Servers {
  constructor() {
  }

  draw(selection, changes) {
    if (!Changesets.affected(changes, ['clock', 'servers'])) {
      return;
    }

    serverData.forEach(s => s.update(controller.workspace.clock));
    let updateG = serversG
      .selectAll('g.server')
      .data(serverData);

    // Server enter
    let enterG = updateG.enter()
      .append('g');
    enterG.append('text')
      .attr('class', 'serverId')
      .attr('x', s => s.labelPoint.x - s.point.x)
      .attr('y', s => s.labelPoint.y - s.point.y)
      .style('text-anchor', 'middle')
      .style('dominant-baseline', 'middle')
      .text(s => `S${s.serverId}`);
    enterG.append('circle')
      .attr('class', 'serverbg')
      .attr('r', 50);
    enterG.append('path')
      .attr('class', 'timeout')
    enterG.append('text')
      .attr('class', 'term')
      .style({
        'text-anchor': 'middle',
        'dominant-baseline': 'central',
      });
    enterG.append('g')
      .attr('class', 'votes');

    // Server update
    updateG
      .attr('transform', s => new Transform()
        .translate(s.point.x, s.point.y)
        .toString())
      .attr('class', s => 'server ' + s.stateClasses());
    updateG.select('.serverbg')
      .style('fill', s => termColor(s.serverVar.lookup('currentTerm').value));
    updateG.select('text.term')
      .text(s => s.serverVar.lookup('currentTerm').toString());
    if (Changesets.affected(changes, 'clock')) {
      updateG.select('path.timeout')
        .attr('d', s => s.timeoutArc());
    }

    // Votes
    this.drawVotes(updateG.select('g.votes'));
  } // Servers.draw()

  drawVotes(votesSel) {
    let updateSel = votesSel.selectAll('circle')
      .data(s => s.getVotes().map((vote, i) => ({
        server: s,
        vote: vote,
        point: peersCircle.at(i / numServers),
      })));
    let enterSel = updateSel.enter();
    enterSel
      .append('circle')
        .attr('cx', v => v.point.x)
        .attr('cy', v => v.point.y)
        .attr('r', 5);
    updateSel
      .attr('class', v => v.vote);
  } // Servers.drawVotes()

} // class Servers

let radianToAngle = radian => radian / (2 * Math.PI) * 360;

class Message {
  constructor(messageVar) {
    this.messageVar = messageVar;
    this.fromPoint = ringLayout.at((messageVar.lookup('from').value - 1) / numServers);
    this.toPoint = ringLayout.at((messageVar.lookup('to').value - 1) / numServers);
    let rise = this.toPoint.y - this.fromPoint.y;
    let run = this.toPoint.x - this.fromPoint.x;
    this.angle = radianToAngle(Math.atan(rise / run));
    if (run < 0) {
      this.angle += 180;
    }
    this.point = {
      x: 0,
      y: 0,
    };

    this.classes = ['message'];
    this.messageVar.lookup('payload').match({
      RequestVoteRequest: r => {
        this.type = 'RequestVote';
        this.classes.push(
          'RequestVote',
          'request');
      },
      RequestVoteResponse: r => {
        this.type = 'RequestVote';
        this.classes.push(
          'RequestVote',
          'response',
          r.lookup('granted').toString() === 'True' ? 'granted' : 'denied');
      },
      AppendEntriesRequest: r => {
        this.type = 'AppendEntries';
        this.classes.push(
          'AppendEntries',
          'request',
          r.lookup('entries').empty() ? 'empty' : 'nonempty');
      },
      AppendEntriesResponse: r => {
        this.type = 'AppendEntries';
        this.classes.push(<commit_msg>d3: Adjust fromPoint, toPoint to reach edge of server, not center<commit_after>"use strict";

let d3 = require('d3');
let colorbrewer = require('colorbrewer');
let Changesets = require('Changesets');
let _ = require('lodash');

let radianToAngle = radian => radian / Math.PI * 180;
let angleToRadian = angle => angle / 180 * Math.PI;

// Calculates where points on the circumference of a circle lie.
// Angles are measured from the right horizon clockwise.
class Circle {
  constructor(cx, cy, r) {
    this.cx = cx;
    this.cy = cy;
    this.r = r;
  }
  radian(radian) {
    return {
      x: _.round(this.cx + this.r * Math.cos(radian), 2),
      y: _.round(this.cy + this.r * Math.sin(radian), 2),
    };
  }
  angle(angle) {
    return this.radian(angleToRadian(angle));
  }
}

class Markers {
  constructor(defs) {
    this.defs = defs;
    this.added = [];
  }
  has(id) {
    return _.includes(this.added, id);
  }
  append(id) {
    return this.defs.append('marker')
      .attr('id', id);
  }
  get(id) {
    return this.defs.select(`marker#${id}`);
  }
  ref(id) {
    return `url(#${id})`;
  }
}

class Transform {
  constructor() {
    this.operations = [];
  }
  toString() {
    return _.reverse(_.clone(this.operations)).join(' ');
  }
  translate(x, y) {
    this.operations.push(`translate(${x}, ${y})`);
    return this;
  };
  rotate(angle) {
    this.operations.push(`rotate(${angle})`);
    return this;
  }
}

let View = function(controller, svg, module) {

svg = d3.select(svg)
  .classed('raft', true)
  .append('g');


let messageTypes = ['RequestVote', 'AppendEntries'];
let messageColor = d3.scale.category10().domain(messageTypes).range(colorbrewer.Set2[6]);
let termColor = d3.scale.category10().range(colorbrewer.Set2[6]);

let markers = new Markers(svg.append('defs'));

let createArrow = selection =>
  selection
    .attr({
      viewBox: '0 -5 10 10',
      refX: 5,
      refY: 0,
      markerWidth: 4,
      markerHeight: 4,
      orient:'auto',
    })
    .append('path')
      .attr('d', 'M0,-5L10,0L0,5')
      .attr('class', 'arrowHead');

markers.append('arrow')
  .call(createArrow);
messageTypes.forEach(t => {
  markers.append(`arrow-${t}`)
    .call(createArrow)
    .style('stroke', messageColor(t))
    .style('fill', messageColor(t));
});

d3.select('head').append('style')
  .text(`
      .raft {
        font-size: 60;
      }
      .raft circle.ring {
        stroke: black;
        stroke-width: 5;
        fill: none;
      }
      .raft .leader.current text.serverId {
        fill: red;
      }
      .raft .legend {
        font-size: 40;
      }
      .raft .noleader .legend {
        visibility: hidden;
      }

      .raft .server .serverbg {
        stroke: black;
      }
      .raft .server g.votes {
        visibility: hidden;
      }
      .raft .server.candidate g.votes {
        visibility: visible;
        fill: white;
      }
      .raft .server.candidate g.votes .granted {
        fill: black;
      }

      .raft .message {
        stroke-width: 4;
        stroke: black;
        fill: black;
      }
      .raft .message.RequestVote {
        stroke: ${messageColor('RequestVote')};
        fill: ${messageColor('RequestVote')};
      }
      .raft .message.AppendEntries {
        stroke: ${messageColor('AppendEntries')};
        fill: ${messageColor('AppendEntries')};
      }
      .raft .message line.direction {
        stroke-width: 5;
      }
      .playback .raft .message line.direction {
        visibility: hidden;
      }
      .raft .message line.plus {
        visibility: hidden;
      }
      .raft .message.RequestVote.response circle,
      .raft .message.AppendEntries.request.empty circle,
      .raft .message.AppendEntries.response circle {
        fill: none;
      }
      .raft .message.RequestVote.response line.plus.horizontal,
      .raft .message.AppendEntries.response line.plus.horizontal {
        visibility: visible;
      }
      .raft .message.RequestVote.response.granted line.plus.vertical,
      .raft .message.AppendEntries.response.success line.plus.vertical {
        visibility: visible;
      }

      .raft .logs .bg rect {
        fill: #dddddd;
        stroke: gray;
      }
      .raft .logs .entry rect {
        stroke: black;
        stroke-width: 8;
      }
      .raft .logs .entry.uncommitted rect {
        stroke-dasharray: 20, 15;
      }

      .raft line.nextIndex {
        stroke-width: 6;
        stroke: black;
      }

      .raft .noLeader .nextIndex,
      .raft .noLeader .matchIndex,
      .raft .leader.current .nextIndex,
      .raft .leader.current .matchIndex {
        visibility: hidden;
      }
  `);

let model = module.env;

let numServers = model.vars.get('servers').size();
let numIndexes = model.vars.get('servers').index(1).lookup('log').capacity();
let electionTimeout = 100000;
let serverRadius = 50;
let outerServerRadius = serverRadius + 10;
let ringLayout = new Circle(250, 500, 200);
let serverLabelCircle = new Circle(250, 500, 300);
let peersCircle = new Circle(0, 0, serverRadius - 10); // relative to server midpoint

// The current leader is defined as the leader of the largest term, or
// undefined if no leaders. This is its server ID.
let currentLeaderId = undefined;

let serverAngle = serverId => 270 + 360 / numServers * (serverId - 1);

// Wraps the model's Server Record with additional information for drawing
class Server {
  constructor(serverId, serverVar) {
    this.serverVar = serverVar;
    this.serverId = serverId;
    let angle = serverAngle(this.serverId);
    this.point = ringLayout.angle(angle);
    this.labelPoint = serverLabelCircle.angle(angle);

    this.timeoutArc = d3.svg.arc()
      .innerRadius(serverRadius)
      .outerRadius(outerServerRadius)
      .startAngle(0)
      .endAngle(0);
  }

  getVotes() {
    return this.serverVar.lookup('state').match({
      Candidate: cstate => cstate.peers.map(peer => {
        if (peer.lookup('voteGranted').toString() == 'True') {
          return 'granted';
        } else if (peer.lookup('voteResponded').toString() == 'True') {
          return 'denied';
        } else {
          return 'noreply';
        }
      }),
      Follower: model.vars.get('servers').map(s => 'N/A'),
      Leader: model.vars.get('servers').map(s => 'N/A'),
    });
  }

  stateClasses() {
    let classes = [];
    this.serverVar.lookup('state').match({
      Follower: () => {
        classes.push('follower');
      },
      Candidate: () => {
        classes.push('candidate');
      },
      Leader: () => {
        classes.push('leader');
        if (currentLeaderId === this.serverId) {
          classes.push('current');
        } else {
          classes.push('stale');
        }
      },
    });
    return classes.join(' ');
  }

  update(clock) {
    let timeoutFrac = ((this.serverVar.lookup('timeoutAt').value - clock) /
       (electionTimeout * 2));
    this.serverVar.lookup('state').match({
      Leader: () => { timeoutFrac = 1; },
    });
    this.timeoutArc.endAngle(2 * Math.PI * timeoutFrac);
    return this;
  }
}

let serverData = model.vars.get('servers').map((v, id) => new Server(id, v));

class Servers {
  constructor() {
  }

  draw(selection, changes) {
    if (!Changesets.affected(changes, ['clock', 'servers'])) {
      return;
    }

    serverData.forEach(s => s.update(controller.workspace.clock));
    let updateG = serversG
      .selectAll('g.server')
      .data(serverData);

    // Server enter
    let enterG = updateG.enter()
      .append('g');
    enterG.append('text')
      .attr('class', 'serverId')
      .attr('x', s => s.labelPoint.x - s.point.x)
      .attr('y', s => s.labelPoint.y - s.point.y)
      .style('text-anchor', 'middle')
      .style('dominant-baseline', 'middle')
      .text(s => `S${s.serverId}`);
    enterG.append('circle')
      .attr('class', 'serverbg')
      .attr('r', serverRadius);
    enterG.append('path')
      .attr('class', 'timeout')
    enterG.append('text')
      .attr('class', 'term')
      .style({
        'text-anchor': 'middle',
        'dominant-baseline': 'central',
      });
    enterG.append('g')
      .attr('class', 'votes');

    // Server update
    updateG
      .attr('transform', s => new Transform()
        .translate(s.point.x, s.point.y)
        .toString())
      .attr('class', s => 'server ' + s.stateClasses());
    updateG.select('.serverbg')
      .style('fill', s => termColor(s.serverVar.lookup('currentTerm').value));
    updateG.select('text.term')
      .text(s => s.serverVar.lookup('currentTerm').toString());
    if (Changesets.affected(changes, 'clock')) {
      updateG.select('path.timeout')
        .attr('d', s => s.timeoutArc());
    }

    // Votes
    this.drawVotes(updateG.select('g.votes'));
  } // Servers.draw()

  drawVotes(votesSel) {
    let updateSel = votesSel.selectAll('circle')
      .data(s => s.getVotes().map((vote, i) => ({
        server: s,
        vote: vote,
        point: peersCircle.angle(serverAngle(i + 1)),
      })));
    let enterSel = updateSel.enter();
    enterSel
      .append('circle')
        .attr('cx', v => v.point.x)
        .attr('cy', v => v.point.y)
        .attr('r', 5);
    updateSel
      .attr('class', v => v.vote);
  } // Servers.drawVotes()

} // class Servers

class Message {
  constructor(messageVar) {
    this.messageVar = messageVar;
    this.fromPoint = ringLayout.angle(serverAngle(messageVar.lookup('from').value));
    this.toPoint = ringLayout.angle(serverAngle(messageVar.lookup('to').value));
    let rise = this.toPoint.y - this.fromPoint.y;
    let run = this.toPoint.x - this.fromPoint.x;
    this.angle = radianToAngle(Math.atan2(rise, run));
    // Adjust fromPoint, toPoint to reach edge of server, not center
    this.fromPoint = new Circle(this.fromPoint.x, this.fromPoint.y, serverRadius)
      .angle(this.angle);
    this.toPoint = new Circle(this.toPoint.x, this.toPoint.y, serverRadius)
      .angle(this.angle + 180);

    this.point = {
      x: 0,
      y: 0,
    };

    this.classes = ['message'];
    this.messageVar.lookup('payload').match({
      RequestVoteRequest: r => {
        this.type = 'RequestVote';
        this.classes.push(
          'RequestVote',
          'request');
      },
      RequestVoteResponse: r => {
        this.type = 'RequestVote';
        this.classes.push(
          'RequestVote',
          'response',
          r.lookup('granted').toString() === 'True' ? 'granted' : 'denied');
      },
      AppendEntriesRequest: r => {
        this.type = 'AppendEntries';
        this.classes.push(
          'AppendEntries',
          'request',
          r.lookup('entries').empty() ? 'empty' : 'nonempty');
      },
      AppendEntriesResponse: r => {
        this.type = 'AppendEntries';
        this.classes.push(
{'fraction_non_alphanumeric': 0.165275857136045, 'fraction_numerical': 0.009966143722283154, 'mean_word_length': 2.3273044581945106, 'pattern_counts': {'":': 0, '<': 4, '<?xml version=': 0, '>': 65, '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': '2412829', 'n_tokens_mistral': 7016, 'n_tokens_neox': 6806, 'n_words': 1371}