site,id,event,status,date
A,1,1,Complete,2017-05-02
A,1,2,Complete,2017-06-06
A,1,3,Unverified,2018-03-21
A,1,4,Incomplete,2019-02-24
A,1,5,Complete,2019-06-18
A,2,1,Complete,2019-01-23
A,2,2,Unverified,2019-03-10
A,2,3,Complete,2019-05-30
A,3,1,Incomplete,2017-07-07
A,3,2,Complete,2018-03-11
A,3,3,Complete,2019-02-21
A,3,4,Unverified,2019-04-05
A,4,1,Unverified,2019-05-13
A,4,2,Complete,2019-08-09
A,5,1,Complete,2017-11-23
A,5,2,Complete,2018-05-17
A,5,3,Complete,2018-05-26
A,5,4,Complete,2019-02-05
B,1,1,Complete,2017-04-16
B,1,2,Complete,2017-11-26
B,1,3,Unverified,2018-03-22
B,1,4,Complete,2018-07-15
B,1,5,Complete,2019-11-05
B,2,1,Complete,2018-09-11
B,2,2,Complete,2018-10-16
B,2,3,Complete,2019-03-08
B,2,3,Unverified,2019-06-22
B,2,4,Complete,2019-10-19
B,3,1,Complete,2017-10-16
B,3,2,Incomplete,2018-09-06
B,3,3,Complete,2019-07-27
B,3,4,Complete,2019-10-02
B,4,1,Unverified,2017-04-21
B,4,2,Complete,2017-11-21
B,4,3,Complete,2018-03-12
B,4,4,Complete,2018-04-04
B,5,1,Unverified,2017-09-23
B,5,2,Complete,2018-01-19
B,5,3,Complete,2018-02-11
B,5,4,Complete,2019-06-26
C,1,1,Complete,2017-04-12
C,1,2,Complete,2018-08-20
C,1,3,Complete,2018-10-02
C,1,4,Unverified,2018-11-16
C,1,5,Complete,2019-06-16
C,2,1,Complete,2018-07-22
C,2,2,Complete,2019-06-13
C,2,3,Complete,2019-11-10
C,2,4,Incomplete,2019-11-12
C,3,1,Complete,2017-03-20
C,3,2,Complete,2018-01-12
C,3,3,Complete,2018-03-08
C,3,4,Complete,2019-09-24
C,4,1,Complete,2017-03-09
C,4,2,Complete,2017-07-13
C,4,3,Complete,2019-10-30
C,5,1,Complete,2018-07-29
C,5,2,Complete,2018-09-20
C,5,3,Unverified,2018-12-25
C,5,4,Complete,2019-05-26
C,5,5,Incomplete,2019-11-17

Study Participant Monitoring POC


I've used this format to monitor study participant progress in clinical trial settings. Each dot represents a participation phase like intake questionnaires, treatment, followup surveys etc. Color encoding can represent the status of each phase; for example, a red dot may mean that the study participant did not complete a milestone. Yellow may represent incomplete data. Of course, you are welcome to customize to suit your needs!

  var margin = { top: 20, right: 20, bottom: 20, left: 20 };
  var width = 500 - margin.left - margin.right;
  var height = 150
  var x = d3.scaleLinear()
      .range([0, width - 90])
      .domain([0.5, 5.5]);
  var y = d3.scaleLinear().range([height - 40, 0]).domain([0, 6]);
  var xAxis = d3.axisBottom(x).ticks(5);
  var yAxis = d3.axisLeft(y).ticks(5);
  var color = d3.scaleOrdinal(d3.schemeCategory10);
  var mydata = d3.csvParse(d3.select("pre#data").text());
  var parseTime = d3.timeParse("%Y-%m-%d");
  var formatDate = d3.timeFormat("%Y-%m-%d");
  var startDate = parseTime("2017-01-01");
  var endDate = parseTime("2019-12-30");
  var xSlider = d3.scaleTime()
    .domain([startDate, endDate])
    .range([0, 300])
    .clamp(true);

  mydata.forEach(function (d) {
    d.id = +d.id;
    d.event = +d.event;
    d.date = parseTime(d.date);
  });

  ///////////////////////////////////////////////////////////////
  ///////////////////////////////////////////////////////////////
  var slider = d3.select("#slider")
    .append("svg")
    .attr("class", "slider")
    .attr("width", 370)
    .attr("height", 60)
    .attr("transform", "translate(" + 50 + "," + 580 + ")");

  slider.append("line")
    .attr("class", "track")
    .attr("x1", xSlider.range()[0])
    .attr("x2", xSlider.range()[1])
    .select(function () {
      return this.parentNode.appendChild(this.cloneNode(true));
    })
    .attr("class", "track-inset")
    .select(function () {
      return this.parentNode.appendChild(this.cloneNode(true));
    })
    .attr("class", "track-overlay")
    .call(d3.drag()
      .on("start.interrupt", function () { slider.interrupt(); })
      .on("start drag", function () {
        hue(xSlider.invert(d3.event.x));
      }));

  var label = slider.append("text")
    .attr("class", "label")
    .attr("text-anchor", "middle")
    .attr("font-size", 12)
    .text(formatDate(startDate))
    .attr("transform", "translate(0," + (-15) + ")")

  var handle = slider.insert("circle", ".track-overlay")
    .attr("class", "handle")
    .attr("r", 9);

  ////////////////////////////////////////////////////////////////
  ////////////////////////////////////////////////////////////////
  var container = d3.select('#figurecontainer')

  var createGraph = function () {

    var panelData = d3.nest()
      .key(function (d) { return d.site; })
      .entries(mydata);

    var myGraph = container.selectAll(".facetClass")
      .data(panelData)
      .enter().append("svg")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      .attr("class", "facetClass")
      .attr("width", width - margin.left - margin.right)
      .attr("height", height)

      .each(function (d) {

        var facet = d3.select(this)
          .append("g");

        facet.append("rect")
          .attr("x", margin.left)
          .attr("y", margin.top)
          .attr("width", width - 40)
          .attr("height", height - margin.top - margin.bottom)
          .style('opacity', 0.7)
          .style('fill', '#FFFFFF');

        facet.append("rect")
          .attr("x", width - 30 - margin.left - margin.right)
          .attr("width", 30)
          .attr("y", margin.top)
          .attr("height", height - margin.top - margin.bottom)
          .style('opacity', 0.1)
          .style('fill', "black");

        facet.append("g")
          .attr("transform", "translate(" + margin.left + "," +
            margin.bottom + ")")
          .call(yAxis);

        facet.append("g")
          .attr("transform", "translate(" + margin.left + "," +
            130 + ")")
          .call(xAxis);

        facet.append("text")
          .attr("x", height / 2)
          .attr("y", 60 - width)
          .attr("transform", function (d) { return "rotate(90)" })
          .style("text-anchor", "end")
          .text(function (d) { return d.key; });
      });
  };

  var update = function (whichDate) {
    var filteredData = mydata.filter(function (d) {
      return d.date <= whichDate
    });

    var panelDotsData = d3.nest()
      .key(function (d) { return d.site; })
      .entries(filteredData);

    var myFacets = d3.selectAll(".facetClass")
      .data(panelDotsData);

    var myDots = myFacets.selectAll(".dot")
      .data(function (d) { return d.values });

    var myDotsEnter = myDots.enter()
      .append("circle")
      .attr("class", "dot");

    myDots.exit().remove();

    var myDotsMerge = myDotsEnter.merge(myDots);

    myDotsMerge.attr("r", 8)
      .attr("cx", function (d) { return x(d.id); })
      .attr("cy", function (d) { return y(d.event); })
      .attr("transform", "translate(" + margin.left + "," +
        margin.bottom + ")")
      .style("stroke", "black")
      .style("fill", function (d) {
        if (d.status === "Complete") { return "darkgreen" }
        if (d.status === "Unverified") { return "gold" }
        if (d.status === "Incomplete") { return "red" }
      });
  };

  /////////////////////////////////////////////////////////////////
  /////////////////////////////////////////////////////////////////
  createGraph();
  handle.attr("cx", xSlider(endDate));
  label.attr("x", xSlider(endDate)).text(formatDate(endDate));
  update(endDate);

  function hue(h) {
    handle.attr("cx", xSlider(h));
    label.attr("x", xSlider(h)).text(formatDate(h));
    console.log(h);
    update(h);
  }