Chartjs-Moving point to clicked point in ChartJS (radarchart)

1👍

Unfortunately, your approach is not a viable solution because you are effectively hard coding regions within your chart. As you pointed out already, when the chart changes (e.g. size increase/decrease, dimensions change, different step size config, different number of datasets, etc.), then this solution will not work. To make this work on any size chart, you must perform some chart introspection to attempt to determine where in the chart the user clicked compared with the scale regions.

Based upon the chart.js API available to us, the only way I can think of how to do this is by comparing the distance between the click event and center point with the distance between each chart scale interval.

However, this won’t produce 100% accurate results and can result in a click causing the point to move differently then what you might have expected. The reason for this is because the scale lines are straight instead of arced but we can only approximate the clicked region by using a circumference. Here is an example of what I mean.

enter image description here

With the approach that I described, the blue circle represents the click region that will cause a point to move to the 2nd region. I’m sure with some tricky math and some brute force you could overcome this restriction. But after playing with it for quite sometime it was the best I could come up with.

Here is a working example to demonstrate the behavior. If you click near any of the major scale lines it works perfectly. But if you click closer to the middle between scale lines it doesn’t work so well (again, because of the circle example above).

Here is the relevant code from the example that determines how far to move the point.

$("#radarChart").click(function (evt) {
  var eventLocation = getEventLocation(this,evt); 
  var activePoints = myRadarChart.getPointsAtEvent(evt);
  var eventLocDistToCenter = pointDistance({x: myRadarChart.scale.xCenter, y: myRadarChart.scale.yCenter}, eventLocation);
  var scale = myRadarChart.scale;
  var scaleIntervalDistance = scale.calculateCenterOffset(scale.min + (1 * scale.stepValue));  

  activePoints[0].value = parseInt(eventLocDistToCenter / scaleIntervalDistance);  
  myRadarChart.update();
});

0👍

This is the closest I have come to producing the wanted result, but this is very buggy, and very unfriendly to touch devices.

var RadarChart = {
  draw: function(id, d, options){
    var cfg = {
      radius: 6, 
      w: 200,
      h: 200,
      factor: 1, 
      factorLegend: .85,
      levels: 5,    
      maxValue: 10,
      radians: 2 * Math.PI,
      opacityArea: 0.5,
      color: d3.rgb("#659CEF")
    };
    if('undefined' !== typeof options){
      for(var i in options){
        if('undefined' !== typeof options[i]){
          cfg[i] = options[i];
        }
      }
    }

    cfg.maxValue = Math.max(cfg.maxValue, d3.max(d.map(function(o){return o.value}))); 
    var allAxis = (d.map(function(i, j){return i.axis}));
    var total = allAxis.length;    
    var radius = cfg.factor*Math.min(cfg.w/2, cfg.h/2);

    d3.select(id).select("svg").remove();
    var g = d3.select(id).append("svg").attr("width", cfg.w).attr("height", cfg.h).append("g");

    var tooltip;

    drawFrame();
    var maxAxisValues = []; 
    drawAxis();
    var dataValues = [];
    reCalculatePoints();

    var areagg = initPolygon();
    drawPoly();

    drawnode();

    function drawFrame(){
      for(var j=0; j<cfg.levels; j++){
        var levelFactor = cfg.factor*radius*((j+1)/cfg.levels);
        g.selectAll(".levels").data(allAxis).enter().append("svg:line")
         .attr("x1", function(d, i){return levelFactor*(1-cfg.factor*Math.sin(i*cfg.radians/total));})
         .attr("y1", function(d, i){return levelFactor*(1-cfg.factor*Math.cos(i*cfg.radians/total));})
         .attr("x2", function(d, i){return levelFactor*(1-cfg.factor*Math.sin((i+1)*cfg.radians/total));})
         .attr("y2", function(d, i){return levelFactor*(1-cfg.factor*Math.cos((i+1)*cfg.radians/total));})
         .attr("class", "line").style("stroke", "grey").style("stroke-width", "0.5px").attr("transform", "translate(" + (cfg.w/2-levelFactor) + ", " + (cfg.h/2-levelFactor) + ")");;
      }
    }

    function drawAxis(){
      var axis = g.selectAll(".axis").data(allAxis).enter().append("g").attr("class", "axis");

      axis.append("line")
          .attr("x1", cfg.w/2)
          .attr("y1", cfg.h/2)
          .attr("x2", function(j, i){
            maxAxisValues[i] = {x:cfg.w/2*(1-cfg.factor*Math.sin(i*cfg.radians/total)), y:0};
            return maxAxisValues[i].x;
          })
          .attr("y2", function(j, i){
            maxAxisValues[i].y = cfg.h/2*(1-cfg.factor*Math.cos(i*cfg.radians/total));
            return maxAxisValues[i].y;
          })
          .attr("class", "line").style("stroke", "grey").style("stroke-width", "1px");

      axis.append("text").attr("class", "legend")
          .text(function(d){return d}).style("font-family", "sans-serif").style("font-size", "10px").attr("transform", function(d, i){return "translate(0, -10)";})
          .attr("x", function(d, i){return cfg.w/2*(1-cfg.factorLegend*Math.sin(i*cfg.radians/total))-20*Math.sin(i*cfg.radians/total);})
          .attr("y", function(d, i){return cfg.h/2*(1-Math.cos(i*cfg.radians/total))+20*Math.cos(i*cfg.radians/total);});
    }

    function reCalculatePoints(){
      g.selectAll(".nodes")
        .data(d, function(j, i){
          dataValues[i] =
          [
            cfg.w/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total)),
            cfg.h/2*(1-(parseFloat(Math.max(j.value, 0))/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total)),
          ];
        });
      dataValues[d[0].length] = dataValues[0];
    }

    function initPolygon(){
      return g.selectAll("area").data([dataValues])
                .enter()
                .append("polygon")
                .attr("class", "radar-chart-serie0")
                .style("stroke-width", "2px")
                .style("stroke", cfg.color)
                .on('mouseover', function (d){
                  z = "polygon."+d3.select(this).attr("class");
                  g.selectAll("polygon").transition(200).style("fill-opacity", 0.1); 
                  g.selectAll(z).transition(200).style("fill-opacity", 0.7);
                })
                .on('mouseout', function(){
                  g.selectAll("polygon").transition(200).style("fill-opacity", cfg.opacityArea);
                })
                .style("fill", function(j, i){return cfg.color;})
                .style("fill-opacity", cfg.opacityArea);
    }

    function drawPoly(){
      areagg.attr("points",function(de) {
          var str="";
          for(var pti=0;pti<de.length;pti++){
            str=str+de[pti][0]+","+de[pti][1]+" ";
          }            
          return str;
        });
    }

    function drawnode(){    
      g.selectAll(".nodes")
        .data(d).enter()
        .append("svg:circle").attr("class", "radar-chart-serie0")
        .attr('r', cfg.radius)
        .attr("alt", function(j){return Math.max(j.value, 0);})
        .attr("cx", function(j, i){
          return cfg.w/2*(1-(Math.max(j.value, 0)/cfg.maxValue)*cfg.factor*Math.sin(i*cfg.radians/total));
        })
        .attr("cy", function(j, i){
          return cfg.h/2*(1-(Math.max(j.value, 0)/cfg.maxValue)*cfg.factor*Math.cos(i*cfg.radians/total));
        })
        .attr("data-id", function(j){return j.axis;})
        .style("fill", cfg.color).style("fill-opacity", 0.9)
        .on('mouseover', function (d){
                    newX =  parseFloat(d3.select(this).attr('cx')) - 10;
                    newY =  parseFloat(d3.select(this).attr('cy')) - 5;
                    tooltip.attr('x', newX).attr('y', newY).text(d.value).transition(200).style('opacity', 1);
                    z = "polygon."+d3.select(this).attr("class");
                    g.selectAll("polygon").transition(200).style("fill-opacity", 0.1);
                    g.selectAll(z).transition(200).style("fill-opacity", 0.7);
                  })
        .on('mouseout', function(){
                    tooltip.transition(200).style('opacity', 0);
                    g.selectAll("polygon").transition(200).style("fill-opacity", cfg.opacityArea);
                  })
        .call(d3.behavior.drag().on("drag", move))      // for drag & drop
        .append("svg:title")
        .text(function(j){return Math.max(j.value, 0)});
    }

    //Tooltip
    tooltip = g.append('text').style('opacity', 0).style('font-family', 'sans-serif').style('font-size', 13);


    function move(dobj, i){

      this.parentNode.appendChild(this);
      var dragTarget = d3.select(this);

      var oldData = dragTarget.data()[0];
      var oldX = parseFloat(dragTarget.attr("cx")) - 100;
      var oldY = 100 - parseFloat(dragTarget.attr("cy"));
      var newY = 0, newX = 0, newValue = 0;
      var maxX = maxAxisValues[i].x - 100;
      var maxY = 100 - maxAxisValues[i].y;

      if(oldX == 0) {
        newY = oldY - d3.event.dy;
        if(Math.abs(newY) > Math.abs(maxY)) {
          newY = maxY;
        }
        newValue = (newY/oldY) * oldData.value;
      }
      else
      {
        var slope = oldY / oldX;       
        newX = d3.event.dx + parseFloat(dragTarget.attr("cx")) - 100;

        if(Math.abs(newX) > Math.abs(maxX)) {
          newX = maxX;
        }
        newY = newX * slope;

        var ratio = newX / oldX; 
        newValue = ratio * oldData.value;
      }

      dragTarget
          .attr("cx", function(){return newX + 100 ;})
          .attr("cy", function(){return 100 - newY;});
      d[oldData.order].value=newValue;
      reCalculatePoints();
      drawPoly();
    }

  }
};
var d = [
         {axis: "Communication", value: 3, order:0}, 
         {axis: "Item As Described", value: 3, order:1}, 
         {axis: "Shipping Time", value: 3, order:2},  
         {axis: "Shipping Cost", value: 3, order:3},  
         {axis: "SE", value: 3, order:4},
        ];
RadarChart.draw("#chart", d);
$('#chart').on('touchmove', function (e) {
     e.preventDefault();
});

This uses the D3JS library.

Leaving it here for others to improve on.

Leave a comment