block by emeeks b57f4cc89dacd38fcdcd

circularbrush.filter

Full Screen

The earlier example of this brushable radial chart led to some feedback to make the filtering better. I introduced a #circularBrush.filter(array,accessor) that takes an array of data and and accessor for that data and returns to you the data that falls into the area of the brush. The functionality of this chart is no different than the other, but if you take a look at the code, it’s much more efficient.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <link href='https://fonts.googleapis.com/css?family=Lato:300' rel='stylesheet' type='text/css'>

    <style>
     body {
        background-color: whitesmoke;
     }

    .extent {
      fill-opacity: .1;
      fill: rgb(205,130,42);
      cursor: move;
    }

    .e {
      fill: rgb(111,111,111);
      cursor: move;
    }

    .w {
      fill: rgb(169,169,169);
      cursor: move;
    }

     .freeze {
        fill: #A8DFE4;
     }

     .rain {
        fill: #209CD3;
     }

     .clear {
      fill: white
     }

     .scattered {
      fill: lightgray;
     }

     .cloudy {
      fill: #a1a1a1;
     }

     .overcast {
      fill: #616161;
     }


     svg {
        background-color: white;
        font-family: 'Lato';
     }

     .axis {
        stroke: white;
        opacity: .8;
     }

     text {
      pointer-events: none;
     }

     text.title {
        font-size: 26px;
     }

     text.months, text.temp {
      text-anchor: middle;
      font-size: 12px;
      fill: #39837B;
     }

     circle.axis {
        stroke: white;
        stroke-width: 1px;
        fill: none;
     }

     circle.axis.record {
        stroke: #bae0d6;
        stroke-width: 1.2px;
        opacity: 1;
     }

     line.record, line.avg, line.yearLow, line.yearHigh{
      stroke-width: 2px;
     }

     line.record {
        stroke: #bae0d6;
     }

     line.avg {
        stroke: #3FA39E;
        opacity: .5;
     }

     line.year {
       stroke: #006358;

     }

     line.yearLow, line.yearHigh{
        stroke: #F97F5A;
     }

     .avg {
        stroke: #3FA39E;
        fill: #3FA39E;
     }

     .record {
        stroke: #bae0d6;
        fill: #bae0d6;
        .opacity: .5;
     }

     .year {
        stroke: #F97F5A;
        fill: #F97F5A;
     }
     .beyond {
        stroke: #445E5B;
        fill: #445E5B;
     }
    </style>
</head>
<body>
    <svg width=960 height=500></svg>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.3.0/d3-legend.min.js"></script>
    <script src="d3.svg.circularbrush.js" charset="utf-8" type="text/javascript"></script>
    <script>
    var width = 960,
    margin = 20,
    height = 500,
    svg = d3.select('svg'),
    origin = svg.append('g')
        .attr('transform', 'translate(' + width*3/5 + ',' + height/2 + ')'),
    rScale = d3.scale.linear()
        .domain([-10, 110])
        .range([0, height/2 - margin]),
    yScale = function (day, temp) {return -Math.cos(angleScale(day)*Math.PI/180)*rScale(parseInt(temp))},
    xScale = function (day, temp) {return Math.sin(angleScale(day)*Math.PI/180)*rScale(parseInt(temp))},
    angleScale = d3.scale.linear()
        .range([0, 360]);

    var drawRadial = function(chart, cl, data, low, high){
      chart.selectAll('line.' + cl)
        .data(data)
        .enter().append('line')
        .attr('x1', function (d) {return xScale(d.index, d[low])})
        .attr('x2', function (d) {return xScale(d.index, d[high])})
        .attr('y1', function (d) {return yScale(d.index, d[low])})
        .attr('y2', function (d) {return yScale(d.index, d[high])})
        .attr('class', cl);
    };

    d3.json('ny.json', function(err, json){

      rawData = json;

        angleScale.domain([0, json.values.length - 1]);

        var min = d3.min(json.values, function (d) {return parseInt(d.recLow)}),
        max = d3.max(json.values, function (d) {return parseInt(d.recHigh)});

        var months = [];
        //find index for months based on data
        json.values.forEach(function (d, i) {
            var month = d.date.split('-')[1],
            prevDaysMonth = ( i === 0 ) ? undefined : json.values[i - 1].date.split('-')[1];
            if (i === 0 || month != prevDaysMonth){
                months.push({
                    month: month,
                    index: i
                });
            }
        })

        //circle axis
        origin.selectAll('circle.axis-green')
            .data([40, 60, 80, 100])
            .enter().append('circle')
            .attr('r', function (d) {return rScale(d)})
            .attr('class', 'axis record')

        //record low and high
        drawRadial(origin, 'record', json.values, 'recLow', 'recHigh')

        //avg low and high
        drawRadial(origin, 'avg', json.values, 'avgLow', 'avgHigh')

        //this year's temperature
        var thisYear = json.values.filter(function (d) {return d.min });

        drawRadial(origin, 'year', thisYear, 'min', 'max')

        var lowLower = json.values.filter(function (d) {return d.min && parseInt(d.min) < parseInt(d.avgLow)});
        drawRadial(origin, 'yearLow', lowLower, 'min', 'avgLow')

        var highHigher = json.values.filter(function (d) {return d.min && parseInt(d.max) > parseInt(d.avgHigh)});
        drawRadial(origin, 'yearHigh', highHigher, 'max', 'avgHigh')

        var circleAxis = [0, 32, 60, 80, 100]
        circleAxis = circleAxis.map( function (d) {return {temp: d, index: 320}})

        //temperature axis
        origin.selectAll('circle.axis-white')
            .data(circleAxis)
            .enter().append('circle')
            .attr('r', function (d) {return rScale(d.temp)})
            .attr('class', 'axis')

        //temperature axis labels
        origin.selectAll('text.temp')
            .data(circleAxis)
            .enter().append('text')
            .attr('x', function (d) {
              return xScale(d.index, d.temp)})
            .attr('y', function (d) {return yScale(d.index, d.temp)})
            .text(function (d) {return d.temp + '°'})
            .attr('class', 'temp');

        //axis lines
        var axis = origin.append('g');

        axis.selectAll('line.axis')
          .data(months)
          .enter().append('line')
          .attr('x2', function (d) {
            return xScale(d.index, 120)})
          .attr('y2', function (d) {return -yScale(d.index, 120)})
          .attr('class', 'axis');

        var monthLabels = months.filter( function (d,i) {return i%3 === 0})
        //month labels
        axis.selectAll('text.months')
          .data(monthLabels)
          .enter().append('text')
          .attr('x', function (d) {
            return xScale(d.index, 110)})
          .attr('y', function (d) {return yScale(d.index, 110)})
          .text(function (d) {return d.month})
          .attr('class', 'months');

        //center for reference
        axis.append('circle')
            .attr('r', 3)
            .attr('class', 'avg')

        //title
        svg.append('text')
            .attr('x', 30)
            .attr('y', 60)
            .text(json.name)
            .attr('class', 'title')

        //subtitle
        svg.append('text')
            .attr('x', 30)
            .attr('y', 100)
            .text('Historical Weather Data')

        //create legend
        var legendScale = d3.scale.ordinal()
            .domain(['Record', 'Average', 'This Year - within avg', 'This Year - beyond avg', 'Freezing', 'Precipitation', 'Scattered Clouds', "Cloudy", "Overcast"])
            .range(['record', 'avg', 'beyond', 'year', 'freeze', 'rain', 'scattered', 'cloudy', 'overcast'])

        //d3-legend
        var legend = d3.legend.color()
            .shapePadding(5)
            .useClass(true)
            .scale(legendScale);

        svg.append('g')
            .attr('transform', 'translate(30,120)')
            .call(legend);

        d3.json("cloud_rain_freeze.json", loadBars);


    });


function loadBars(data) {
  var freezeBars = [];
  var rainBars = [];
  var cloudBars = [];
  var freeze = {};
  var cloud = {start: data[0].date, category: data[0].cloud};
  var rain = {};

  data.forEach(function (d, i) {
    if (d.cloud !== cloud.category) {
      cloud.end = d.date;
      cloudBars.push(cloud);
      cloud = {start: d.date, category: d.cloud};
    }

    if (freeze.start && !d.freeze) {
      freeze.end = d.date;
      freezeBars.push(freeze);
      freeze = {};
    }
    else if (d.freeze && !freeze.start) {
      freeze.start = d.date;
    }
    if (rain.start && !d.rain) {
      rain.end = d.date;
      rainBars.push(rain);
      rain = {};
    }
    else if (d.rain && !rain.start) {
      rain.start = d.date;
    }

  });

  drawBars(rainBars, "rain", 205);
  drawBars(freezeBars, "freeze", 200);
  drawBars(cloudBars, "freeze", 210);
}

function loadBars(data) {
  freezeBars = [];
  rainBars = [];
  cloudBars = [];
  var freeze = {};
  var cloud = {start: data[0].date, category: data[0].cloud};
  var rain = {};
  var dateScale = d3.time.scale().domain([new Date("01/01/2015"), new Date("12/31/2015")]).range([1,366]);

  data.forEach(function (d, i) {

    if (d.cloud !== cloud.category) {
      cloud.end = d.date;
      cloud.endInt = dateScale(new Date(d.date));
      cloudBars.push(cloud);
      cloud = {start: d.date, startInt: dateScale(new Date(d.date)), category: d.cloud};
    }

    if (freeze.start && !d.freeze) {
      freeze.end = d.date;
      freeze.endInt = dateScale(new Date(d.date));
      freezeBars.push(freeze);
      freeze = {};
    }
    else if (d.freeze && !freeze.start) {
      freeze.start = d.date;
      freeze.startInt = dateScale(new Date(d.date));
    }
    if (rain.start && !d.rain) {
      rain.end = d.date;
      rain.endInt = dateScale(new Date(d.date));
      rainBars.push(rain);
      rain = {};
    }
    else if (d.rain && !rain.start) {
      rain.start = d.date;
      rain.startInt = dateScale(new Date(d.date));
    }

  });

  drawBars(rainBars, "rain", 205);
  drawBars(freezeBars, "freeze", 200);
  drawBars(cloudBars, "cloud", 210);

  drawBrush();
}

function drawBrush() {
    brush = d3.svg.circularbrush()
      .range([1,366])
      .innerRadius(10)
      .outerRadius(218)
      .handleSize(0.1)
      .extent([30,120])
      .on("brush", brushed);

  d3.select("svg")
  .append("g")
  .attr("class", "brush")
  .attr("transform", "translate(576,250)")
  .call(brush);

  d3.select("svg").append("g")
  .attr("class", "linear")
  .attr("transform", "translate(40,350)");

  brushed();

  function brushed() {
    filteredData = brush.filter(rawData.values, function (d) {return d.index});

    var dates = filteredData.map(function (d) {return d.date});
    var dateScale = d3.scale.ordinal()
      .domain(dates)
      .range(dates.map(function (d, i) {return i}));

    filteredRainbars = brush.filter(rainBars, function (d) {return d.startInt});
    filteredCloudbars = brush.filter(cloudBars, function (d) {return d.startInt});
    filteredFreezebars = brush.filter(freezeBars, function (d) {return d.startInt});

    var yScale = d3.scale.linear().domain([-10,110]).range([100,0]).clamp(true);
    var xScale = d3.scale.linear().domain([0, filteredData.length]).range([0,250]);
    var lineWidth = 250 / filteredData.length;

    d3.select("g.linear")
    .selectAll("*")
    .remove();

    d3.select("g.linear")
    .append("g")
    .attr("class", "weatherbars")
    .selectAll("g")
    .data([{class: "rain", data: filteredRainbars}, {class: "cloud", data: filteredCloudbars},  {class: "freeze", data: filteredFreezebars}])
    .enter()
    .append("g")
    .each(function (g, gi) {
      console.log(this,g)
      d3.select(this)
      .selectAll("rect")
      .data(g.data)
      .enter()
      .append("rect")
      .attr("class", function (d) {return g.class + " " + d.category})
      .attr("y", gi * -10)
      .attr("x", function (d) {return xScale(dateScale(toDate(d))) })
      .attr("width", function (d) {return (d.endInt - d.startInt) * lineWidth })
      .attr("height", "5px")
    })

    d3.select("g.linear")
    .selectAll("g.linearBars")
    .data(filteredData, function (d) {return d.date})
    .enter()
    .insert("g", "g.weatherbars")
    .attr("class", "linearBars")
    .each(function (d, i) {
      if (i === 0 || i === filteredData.length - 1) {
        d3.select(this).append("text")
        .text(d.date)
        .attr("y", -30)
        .style("text-anchor", "middle");
      }
     d3.select(this).append("line").style("stroke-width", lineWidth).attr("class", "record")
     d3.select(this).append("line").style("stroke-width", lineWidth).attr("class", "avg")
     d3.select(this).append("line").style("stroke-width", lineWidth).attr("class", "yearLow")
     d3.select(this).append("line").style("stroke-width", lineWidth).attr("class", "yearHigh")
     d3.select(this).append("line").style("stroke-width", lineWidth).attr("class", "year")
    });

    d3.selectAll("g.linearBars")
    .attr("transform", function (d,i) {return "translate(" + xScale(dateScale(d.date)) +",0)" });

    d3.selectAll("g.linearBars")
    .each(function (d) {
      d3.select(this).select("line.record")
        .attr("y1", yScale(parseInt(d.recHigh)))
        .attr("y2", yScale(parseInt(d.recLow)));
      d3.select(this).select("line.avg")
        .attr("y1", yScale(parseInt(d.avgHigh)))
        .attr("y2", yScale(parseInt(d.avgLow)));
      if (d.max != null) {
        if (d.min < parseInt(d.avgLow)) {
        d3.select(this).select("line.yearLow")
          .attr("y1", yScale(parseInt(d.min)))
          .attr("y2", yScale(parseInt(d.avgLow)));
        }
        if (d.max > parseInt(d.avgHigh)) {
          d3.select(this).select("line.yearHigh")
            .attr("y1", yScale(parseInt(d.max)))
            .attr("y2", yScale(parseInt(d.avgHigh)));
        }
        if (!(d.min > parseInt(d.avgHigh) || d.max < parseInt(d.avgLow))) {
        d3.select(this).select("line.year")
          .attr("y1", yScale(Math.max(d.min,parseInt(d.avgLow))))
          .attr("y2", yScale(Math.min(d.max,parseInt(d.avgHigh))));
        }
      }
    })

  }

}

function toDate(d) {
  var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
  ];
  var date = new Date(d.start);
  return date.getDate() + "-" + monthNames[date.getMonth()];
}

function drawBars(data, type, offset) {

  dateScale = d3.scale.linear()
  .domain([1,366])
  .range([0,(2 * Math.PI)]);

  var arc = d3.svg.arc().innerRadius(offset).outerRadius(offset + 5);

  d3.select("svg").append("g")
  .attr("class", type + "bars")
  .attr("transform", "translate(576,250)")
  .selectAll("path")
  .data(data)
  .enter()
  .append("path")
  .attr("d", drawArc)
  .attr("class", function (d) {return type + " " + d.category})

  function drawArc(d) {
    projected = {startAngle: dateScale(d.startInt), endAngle: dateScale(d.endInt) };
    return arc(projected);
  }
}


    </script>
</body>
</html>

d3.svg.circularbrush.js

d3.svg.circularbrush = function() {
	var _extent = [0,Math.PI * 2];
    var _circularbrushDispatch = d3.dispatch('brushstart', 'brushend', 'brush');
	var _arc = d3.svg.arc().innerRadius(50).outerRadius(100);
	var _brushData = [
		{startAngle: _extent[0], endAngle: _extent[1], class: "extent"},
		{startAngle: _extent[0] - .2, endAngle:  _extent[0], class: "resize e"},
		{startAngle: _extent[1], endAngle: _extent[1] + .2, class: "resize w"}
		];
	var _newBrushData = [];
	var d3_window = d3.select(window);
	var _origin;
	var _brushG;
	var _handleSize = .2;
	var _scale = d3.scale.linear().domain(_extent).range(_extent);
	var _tolerance = 0.00001;

	function _circularbrush(_container) {

		updateBrushData();

		_brushG = _container
		.append("g")
		.attr("class", "circularbrush");

		_brushG
		.selectAll("path.circularbrush")
		.data(_brushData)
		.enter()
		.insert("path", "path.resize")
		.attr("d", _arc)
		.attr("class", function(d) {return d.class + " circularbrush"})

		_brushG.select("path.extent")
		.on("mousedown.brush", resizeDown)

		_brushG.selectAll("path.resize")
		.on("mousedown.brush", resizeDown)

		return _circularbrush;
	}

	_circularbrush.extent = function(_value) {
		var _d = _scale.domain();
		var _r = _scale.range();

		var _actualScale = d3.scale.linear()
		.domain([-_d[1],_d[0],_d[0],_d[1]])
		.range([_r[0],_r[1],_r[0],_r[1]])

		if (!arguments.length) return [_actualScale(_extent[0]),_actualScale(_extent[1])];

		_extent = [_scale.invert(_value[0]),_scale.invert(_value[1])];

		return this
	}

	_circularbrush.handleSize = function(_value) {
		if (!arguments.length) return _handleSize;

		_handleSize = _value;
		_brushData = [
		{startAngle: _extent[0], endAngle: _extent[1], class: "extent"},
		{startAngle: _extent[0] - _handleSize, endAngle:  _extent[0], class: "resize e"},
		{startAngle: _extent[1], endAngle: _extent[1] + _handleSize, class: "resize w"}
		];
		return this
	}

	_circularbrush.innerRadius = function(_value) {
		if (!arguments.length) return _arc.innerRadius();

		_arc.innerRadius(_value);
		return this
	}

	_circularbrush.outerRadius = function(_value) {
		if (!arguments.length) return _arc.outerRadius();

		_arc.outerRadius(_value);
		return this
	}

	_circularbrush.range = function(_value) {
		if (!arguments.length) return _scale.range();

		_scale.range(_value);
		return this
	}

	_circularbrush.arc = function(_value) {
		if (!arguments.length) return _arc;

		_arc = _value;
		return this

	}

	_circularbrush.tolerance = function(_value) {
		if (!arguments.length) return _tolerance;

		_tolerance = _value;
		return this
	}

	_circularbrush.filter = function(_array, _accessor) {
		var data = _array.map(_accessor);

		var extent = _circularbrush.extent();
		var start = extent[0];
		var end = extent[1];
		var firstPoint = _scale.range()[0];
		var lastPoint = _scale.range()[1];
		var filteredArray = [];
		var firstHalf = [];
		var secondHalf = [];

		if (Math.abs(start - end) < _tolerance) {
			return _array;
		}

	    if (start < end) {
	    	filteredArray = _array.filter(function (d) {
	    		return _accessor(d) >= start && _accessor(d) <= end;
	    	});
	    }
	    else {
	      var firstHalf = _array.filter(function (d) {
	         return (_accessor(d) >= start && _accessor(d) <= lastPoint);
	      });
	      var secondHalf = _array.filter(function (d) {
	         return (_accessor(d) <= end && _accessor(d) >= firstPoint);
	      });
		  filteredArray = firstHalf.concat(secondHalf);
	    }

		return filteredArray;

	}

    d3.rebind(_circularbrush, _circularbrushDispatch, "on");

	return _circularbrush;

	function resizeDown(d) {
		var _mouse = d3.mouse(_brushG.node());

		_originalBrushData = {startAngle: _brushData[0].startAngle, endAngle: _brushData[0].endAngle};

		_origin = _mouse;

		if (d.class == "resize e") {
			d3_window
			.on("mousemove.brush", function() {resizeMove("e")})
			.on("mouseup.brush", extentUp);
		}
		else if (d.class == "resize w") {
			d3_window
			.on("mousemove.brush", function() {resizeMove("w")})
			.on("mouseup.brush", extentUp);
		}
		else {
			d3_window
			.on("mousemove.brush", function() {resizeMove("extent")})
			.on("mouseup.brush", extentUp);
		}

		_circularbrushDispatch.brushstart();

	}

	function resizeMove(_resize) {
		var _mouse = d3.mouse(_brushG.node());
		var _current = Math.atan2(_mouse[1],_mouse[0]);
		var _start = Math.atan2(_origin[1],_origin[0]);

		if (_resize == "e") {
			var clampedAngle = Math.max(Math.min(_originalBrushData.startAngle + (_current - _start), _originalBrushData.endAngle), _originalBrushData.endAngle - (2 * Math.PI));

			if (_originalBrushData.startAngle + (_current - _start) > _originalBrushData.endAngle) {
				clampedAngle = _originalBrushData.startAngle + (_current - _start) - (Math.PI * 2);
			}
			else if (_originalBrushData.startAngle + (_current - _start) < _originalBrushData.endAngle - (Math.PI * 2)) {
				clampedAngle = _originalBrushData.startAngle + (_current - _start) + (Math.PI * 2);
			}

			var _newStartAngle = clampedAngle;
			var _newEndAngle = _originalBrushData.endAngle;
		}
		else if (_resize == "w") {
			var clampedAngle = Math.min(Math.max(_originalBrushData.endAngle + (_current - _start), _originalBrushData.startAngle), _originalBrushData.startAngle + (2 * Math.PI))

			if (_originalBrushData.endAngle + (_current - _start) < _originalBrushData.startAngle) {
				clampedAngle = _originalBrushData.endAngle + (_current - _start) + (Math.PI * 2);
			}
			else if (_originalBrushData.endAngle + (_current - _start) > _originalBrushData.startAngle + (Math.PI * 2)) {
				clampedAngle = _originalBrushData.endAngle + (_current - _start) - (Math.PI * 2);
			}

			var _newStartAngle = _originalBrushData.startAngle;
			var _newEndAngle = clampedAngle;
		}
		else {
			var _newStartAngle = _originalBrushData.startAngle + (_current - _start * 1);
			var _newEndAngle = _originalBrushData.endAngle + (_current - _start * 1);
		}


		_newBrushData = [
		{startAngle: _newStartAngle, endAngle: _newEndAngle, class: "extent"},
		{startAngle: _newStartAngle - _handleSize, endAngle: _newStartAngle, class: "resize e"},
		{startAngle: _newEndAngle, endAngle: _newEndAngle + _handleSize, class: "resize w"}
		]

		brushRefresh();

		if (_newStartAngle > (Math.PI * 2)) {
			_newStartAngle = (_newStartAngle - (Math.PI * 2));
		}
		else if (_newStartAngle < -(Math.PI * 2)) {
			_newStartAngle = (_newStartAngle + (Math.PI * 2));
		}

		if (_newEndAngle > (Math.PI * 2)) {
			_newEndAngle = (_newEndAngle - (Math.PI * 2));
		}
		else if (_newEndAngle < -(Math.PI * 2)) {
			_newEndAngle = (_newEndAngle + (Math.PI * 2));
		}

		_extent = ([_newStartAngle,_newEndAngle]);

		_circularbrushDispatch.brush();

	}

	function brushRefresh() {
		_brushG
			.selectAll("path.circularbrush")
			.data(_newBrushData)
			.attr("d", _arc)
	}


	function extentUp() {

		_brushData = _newBrushData;
		d3_window.on("mousemove.brush", null).on("mouseup.brush", null);

		_circularbrushDispatch.brushend();
	}

	function updateBrushData() {
		_brushData = [
		{startAngle: _extent[0], endAngle: _extent[1], class: "extent"},
		{startAngle: _extent[0] - _handleSize, endAngle:  _extent[0], class: "resize e"},
		{startAngle: _extent[1], endAngle: _extent[1] + _handleSize, class: "resize w"}
		];
	}


}