<commit_before>// When comparing groups' names we do not know what was initial sort order (and we would like to keep it fixed)
// Furthermore, we also do not know which table's (if there are multiple tables on the page) groups we are at
// So we make a list of all groups from all tables joined together and use this to compare groups' names
// There should be no be equally named groups in different tables
var allGroups = [];

function testIP(data) {
	return /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/.test(data);
}

function extractFirstElementText(data) {
	return $(data).eq(0).text();
}

function cleanDate(data) {
	if (/^\s*never\s*/.test(data)) {
		return "01 Jan 1970 00:00:00";
	}
	return data.replace(/\//g, " ").replace(/,/g, "");
}

jQuery.fn.dataTableExt.aTypes.push(
	function (sData) {
		if (testIP(sData)) {
			return 'ip-address';
		}
		return null;
	},
	function (sData) {
		// We use valid HTML data where this characters should be escaped in text so we can find tags this way
		if (/[<>]/.test(sData)) {
			if (testIP(extractFirstElementText(sData))) {
				return 'html-ip-address';
			}
			else if (/^\s*$/.test(sData.replace(/<.*?>/g, ""))) {
				// Nothing except HTML tags are in data, we will try to use alt attribute
				return 'html-alt';
			}
			else {
				// Will use text between HTML tags
				return 'html';
			}
		}
		return null;
	},
	function (sData) {
		// This code is duplicated in reverse in conversion.py file. Keep them in sync.
		if (/^\d+(\.\d+)?\s+(KB|MB|GB)$/.test(sData)) {
			return 'byte-size';
		}
		return null;
	},
	function (sData) {
		window.console.log(sData);
		var parse = Date.parse(cleanDate(sData));
		if (!isNaN(parse)) {
			return 'date-clean';
		}
		return null;
	}
);

function compare(a, b) {
	return ((a < b) ? -1 : ((a > b) ? 1 : 0));
}

function getAlt(el) {
	var el = $(el);
	var alt = el.attr('alt') || el.find('[alt]').attr('alt') || "";
	return alt.toLowerCase();
}

jQuery.fn.dataTableExt.oSort['html-alt-asc'] = function (a, b) {
	return compare(getAlt(a), getAlt(b));
};

jQuery.fn.dataTableExt.oSort['html-alt-desc'] = function (a, b) {
	return jQuery.fn.dataTableExt.oSort['html-alt-asc'](b, a);
};

// This code is duplicated in reverse in conversion.py file. Keep them in sync.
function toKbytes(n) {
	var v = parseFloat(n);
	if (/MB/.test(n)) {
		v *= 1024.0;
	}
	else if (/GB/.test(n)) {
		v *= 1048576.0;
	}
	return v;
}

jQuery.fn.dataTableExt.oSort['byte-size-asc'] = function (a, b) {
	return compare(toKbytes(a), toKbytes(b));
};

jQuery.fn.dataTableExt.oSort['byte-size-desc'] = function (a, b) {
	return jQuery.fn.dataTableExt.oSort['byte-size-asc'](b, a);
};

jQuery.fn.dataTableExt.oSort['ip-address-asc'] = function (a, b) {
	var as = a.split(/[./]/);
	var bs = b.split(/[./]/);
	for (var i = 0; i < as.length; i++) {
		if (i >= bs.length) {
			return 1;
		}
		var comp = as[i] - bs[i];
		if (comp != 0) {
			return comp;
		}
	}
	if (i < bs.length) {
		return -1;
	}
	return 0;
};

jQuery.fn.dataTableExt.oSort['ip-address-desc'] = function (a, b) {
	return jQuery.fn.dataTableExt.oSort['ip-address-asc'](b, a);
};

jQuery.fn.dataTableExt.oSort['html-ip-address-asc'] = function (a, b) {
	return jQuery.fn.dataTableExt.oSort['ip-address-asc'](extractFirstElementText(a), extractFirstElementText(b));
}

jQuery.fn.dataTableExt.oSort['html-ip-address-desc'] = function (a, b) {
	return jQuery.fn.dataTableExt.oSort['html-ip-address-asc'](b, a);
};

jQuery.fn.dataTableExt.oSort['group-sort-asc'] = function (a, b) {
	return compare($.inArray(a, allGroups), $.inArray(b, allGroups));
};

jQuery.fn.dataTableExt.oSort['group-sort-desc'] = function (a, b) {
	return jQuery.fn.dataTableExt.oSort['group-sort-asc'](b, a);
};

function dateCompare(a, b) {
	var x = Date.parse(cleanDate(a));
	var y = Date.parse(cleanDate(b));

	if (isNaN(x)) {
		x = 0;
	}
	if (isNaN(y)) {
		y = 0;
	}

	return x - y;
}

jQuery.fn.dataTableExt.oSort['date-clean-asc'] = function (a, b) {
	return dateCompare(a, b);
};

jQuery.fn.dataTableExt.oSort['date-clean-desc'] = function (a, b) {
	return dateCompare(b, a);
};

function groupDrawCallback(table) {
	return function (oSettings) {
		if (oSettings.aiDisplay.length == 0) {
			return;
		}

		var trs = $(table).find('tbody tr');
		var colspan = trs.eq(0).find('td').length;
		var lastGroup = "";
		trs.each(function (i) {
			var displayIndex = oSettings._iDisplayStart + i;
			var group = oSettings.aoData[oSettings.aiDisplay[displayIndex]]._aData[0];
			if (group != lastGroup) {
				$("<tr />").addClass("section_title").append(
					$("<td />").attr("colspan", colspan).html(group)
				).insertBefore($(this));
				lastGroup = group;
			}
		});
	};
}

$(document).ready(function() {
	if (typeof tablesConfig == "undefined") {
		tablesConfig = [];
	}
	$('table.listing:has(thead)').each(function (t) {
		var sections = [];
		var config = $.extend({
			"entryName": "entries",
			"sortColumn": []
		}, tablesConfig[t]);
		var entryName = config.entryName;
		var sortColumn = config.sortColumn;

		$(this).find('tbody tr').each(function (i) {
			if ($(this).is('.section_title')) {
				var content = $(this).find('td').html();
				sections.push(content);
				allGroups.push(content);
				$(this).remove();
			}
			else if (sections.length != 0) {
				$(this).prepend($("<td />").html(sections[sections.length - 1]));
			}
		});
		if (sections.length != 0) {
			$(this).find('thead tr').prepend($("<th />"));
		}

		var columns = [];
		$(this).find('thead th').each(function (i) {
			if ((i == 0) && (sections.length != 0)) {
				columns.push({
					"bVisible": false,
					"bSearchable": true,
					"bSortable": true,
					"sType": 'group-sort'
				});
			}
			else if (/^\s*$/.test($(this).text())) {
				// We ignore columns with empty header
				columns.push({
					"bVisible": true,
					"bSearchable": false,
					"bSortable": false
				});
			}
			else {
				columns.push({});
			}
		});

		$.each(sortColumn, function (i, val) {
			if (sections.length != 0) {
				val[0]++;
			}
			if (val[1] == "desc") {
				columns[val[0]].asSorting = ["desc", "asc"];
			}
		});

		if (($(this).find('tbody td:not([colspan])').length > 0) || ($('table.listing').find('tbody td[colspan=1]').length > 0)) {
			$(this).dataTable({
				"bPaginate": false,
				"bLengthChange": true,
				"bFilter": true,
				"bSort": true,
				"oSearch": {
					"sSearch": "",
					"bEscapeRegex": false
				},
				"bInfo": true,
				"bAutoWidth": false,
				"bProcessing": false,
				"bSortClasses": false,
				"bStateSave": false,
				"bServerSide": false,
				"aoColumns": columns,
				"aaSortingFixed": (sections.length != 0 ? [[ 0, 'asc' ]] : null),
				"aaSorting": sortColumn,
				"fnDrawCallback": (sections.length != 0 ? groupDrawCallback(this) : null),
				"sDom": 'ift<"clear">',
				"oLanguage": {
					"sZeroRecords": "No matching " + entryName + " found",
					"sInfo": "_TOTAL_ " + entryName + " shown",
					"sInfoEmpty": "0 " + entryName + " shown",
					"sInfoFiltered": "(from _MAX_ all " + entryName + ")",
					"sInfoPostFix": "",
					"sSearch": "Filter:"
				}
			});
		}
	});
});
<commit_msg>Removed debug print.<commit_after>// When comparing groups' names we do not know what was initial sort order (and we would like to keep it fixed)
// Furthermore, we also do not know which table's (if there are multiple tables on the page) groups we are at
// So we make a list of all groups from all tables joined together and use this to compare groups' names
// There should be no be equally named groups in different tables
var allGroups = [];

function testIP(data) {
	return /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/.test(data);
}

function extractFirstElementText(data) {
	return $(data).eq(0).text();
}

function cleanDate(data) {
	if (/^\s*never\s*/.test(data)) {
		return "01 Jan 1970 00:00:00";
	}
	return data.replace(/\//g, " ").replace(/,/g, "");
}

jQuery.fn.dataTableExt.aTypes.push(
	function (sData) {
		if (testIP(sData)) {
			return 'ip-address';
		}
		return null;
	},
	function (sData) {
		// We use valid HTML data where this characters should be escaped in text so we can find tags this way
		if (/[<>]/.test(sData)) {
			if (testIP(extractFirstElementText(sData))) {
				return 'html-ip-address';
			}
			else if (/^\s*$/.test(sData.replace(/<.*?>/g, ""))) {
				// Nothing except HTML tags are in data, we will try to use alt attribute
				return 'html-alt';
			}
			else {
				// Will use text between HTML tags
				return 'html';
			}
		}
		return null;
	},
	function (sData) {
		// This code is duplicated in reverse in conversion.py file. Keep them in sync.
		if (/^\d+(\.\d+)?\s+(KB|MB|GB)$/.test(sData)) {
			return 'byte-size';
		}
		return null;
	},
	function (sData) {
		var parse = Date.parse(cleanDate(sData));
		if (!isNaN(parse)) {
			return 'date-clean';
		}
		return null;
	}
);

function compare(a, b) {
	return ((a < b) ? -1 : ((a > b) ? 1 : 0));
}

function getAlt(el) {
	var el = $(el);
	var alt = el.attr('alt') || el.find('[alt]').attr('alt') || "";
	return alt.toLowerCase();
}

jQuery.fn.dataTableExt.oSort['html-alt-asc'] = function (a, b) {
	return compare(getAlt(a), getAlt(b));
};

jQuery.fn.dataTableExt.oSort['html-alt-desc'] = function (a, b) {
	return jQuery.fn.dataTableExt.oSort['html-alt-asc'](b, a);
};

// This code is duplicated in reverse in conversion.py file. Keep them in sync.
function toKbytes(n) {
	var v = parseFloat(n);
	if (/MB/.test(n)) {
		v *= 1024.0;
	}
	else if (/GB/.test(n)) {
		v *= 1048576.0;
	}
	return v;
}

jQuery.fn.dataTableExt.oSort['byte-size-asc'] = function (a, b) {
	return compare(toKbytes(a), toKbytes(b));
};

jQuery.fn.dataTableExt.oSort['byte-size-desc'] = function (a, b) {
	return jQuery.fn.dataTableExt.oSort['byte-size-asc'](b, a);
};

jQuery.fn.dataTableExt.oSort['ip-address-asc'] = function (a, b) {
	var as = a.split(/[./]/);
	var bs = b.split(/[./]/);
	for (var i = 0; i < as.length; i++) {
		if (i >= bs.length) {
			return 1;
		}
		var comp = as[i] - bs[i];
		if (comp != 0) {
			return comp;
		}
	}
	if (i < bs.length) {
		return -1;
	}
	return 0;
};

jQuery.fn.dataTableExt.oSort['ip-address-desc'] = function (a, b) {
	return jQuery.fn.dataTableExt.oSort['ip-address-asc'](b, a);
};

jQuery.fn.dataTableExt.oSort['html-ip-address-asc'] = function (a, b) {
	return jQuery.fn.dataTableExt.oSort['ip-address-asc'](extractFirstElementText(a), extractFirstElementText(b));
}

jQuery.fn.dataTableExt.oSort['html-ip-address-desc'] = function (a, b) {
	return jQuery.fn.dataTableExt.oSort['html-ip-address-asc'](b, a);
};

jQuery.fn.dataTableExt.oSort['group-sort-asc'] = function (a, b) {
	return compare($.inArray(a, allGroups), $.inArray(b, allGroups));
};

jQuery.fn.dataTableExt.oSort['group-sort-desc'] = function (a, b) {
	return jQuery.fn.dataTableExt.oSort['group-sort-asc'](b, a);
};

function dateCompare(a, b) {
	var x = Date.parse(cleanDate(a));
	var y = Date.parse(cleanDate(b));

	if (isNaN(x)) {
		x = 0;
	}
	if (isNaN(y)) {
		y = 0;
	}

	return x - y;
}

jQuery.fn.dataTableExt.oSort['date-clean-asc'] = function (a, b) {
	return dateCompare(a, b);
};

jQuery.fn.dataTableExt.oSort['date-clean-desc'] = function (a, b) {
	return dateCompare(b, a);
};

function groupDrawCallback(table) {
	return function (oSettings) {
		if (oSettings.aiDisplay.length == 0) {
			return;
		}

		var trs = $(table).find('tbody tr');
		var colspan = trs.eq(0).find('td').length;
		var lastGroup = "";
		trs.each(function (i) {
			var displayIndex = oSettings._iDisplayStart + i;
			var group = oSettings.aoData[oSettings.aiDisplay[displayIndex]]._aData[0];
			if (group != lastGroup) {
				$("<tr />").addClass("section_title").append(
					$("<td />").attr("colspan", colspan).html(group)
				).insertBefore($(this));
				lastGroup = group;
			}
		});
	};
}

$(document).ready(function() {
	if (typeof tablesConfig == "undefined") {
		tablesConfig = [];
	}
	$('table.listing:has(thead)').each(function (t) {
		var sections = [];
		var config = $.extend({
			"entryName": "entries",
			"sortColumn": []
		}, tablesConfig[t]);
		var entryName = config.entryName;
		var sortColumn = config.sortColumn;

		$(this).find('tbody tr').each(function (i) {
			if ($(this).is('.section_title')) {
				var content = $(this).find('td').html();
				sections.push(content);
				allGroups.push(content);
				$(this).remove();
			}
			else if (sections.length != 0) {
				$(this).prepend($("<td />").html(sections[sections.length - 1]));
			}
		});
		if (sections.length != 0) {
			$(this).find('thead tr').prepend($("<th />"));
		}

		var columns = [];
		$(this).find('thead th').each(function (i) {
			if ((i == 0) && (sections.length != 0)) {
				columns.push({
					"bVisible": false,
					"bSearchable": true,
					"bSortable": true,
					"sType": 'group-sort'
				});
			}
			else if (/^\s*$/.test($(this).text())) {
				// We ignore columns with empty header
				columns.push({
					"bVisible": true,
					"bSearchable": false,
					"bSortable": false
				});
			}
			else {
				columns.push({});
			}
		});

		$.each(sortColumn, function (i, val) {
			if (sections.length != 0) {
				val[0]++;
			}
			if (val[1] == "desc") {
				columns[val[0]].asSorting = ["desc", "asc"];
			}
		});

		if (($(this).find('tbody td:not([colspan])').length > 0) || ($('table.listing').find('tbody td[colspan=1]').length > 0)) {
			$(this).dataTable({
				"bPaginate": false,
				"bLengthChange": true,
				"bFilter": true,
				"bSort": true,
				"oSearch": {
					"sSearch": "",
					"bEscapeRegex": false
				},
				"bInfo": true,
				"bAutoWidth": false,
				"bProcessing": false,
				"bSortClasses": false,
				"bStateSave": false,
				"bServerSide": false,
				"aoColumns": columns,
				"aaSortingFixed": (sections.length != 0 ? [[ 0, 'asc' ]] : null),
				"aaSorting": sortColumn,
				"fnDrawCallback": (sections.length != 0 ? groupDrawCallback(this) : null),
				"sDom": 'ift<"clear">',
				"oLanguage": {
					"sZeroRecords": "No matching " + entryName + " found",
					"sInfo": "_TOTAL_ " + entryName + " shown",
					"sInfoEmpty": "0 " + entryName + " shown",
					"sInfoFiltered": "(from _MAX_ all " + entryName + ")",
					"sInfoPostFix": "",
					"sSearch": "Filter:"
				}
			});
		}
	});
});
<|endoftext|>


<commit_before>/*
  Based on:
  http://mbostock.github.com/d3/ex/stack.html
*/

function buildTableData($table){
  // build data
  var $headings = $table.find('thead th').map(function(){ return $(this).text(); });
  var $rows = $table.find('tbody > tr');
  var data = $headings.slice(1).toArray().map(function(){ return []; });
  $rows.each(function(_, row){
    var $cells = $(row).children();
    var xVal = +$cells.eq(0).text();
    $cells.slice(1).each(function(idx, cell){
      data[idx].push({
        x: xVal,
        y: $(cell).data('value'),
        title: "" + $headings[idx + 1] + " " + xVal + " " + $.trim($(cell).text()) + ""
      });
    });
  });
  return data;
}

var stackedBarChart = function(el, data){
  var color = d3.scale.category10();
  var width = 940;
  var height = 300;
  var margin = [10, 50, 10, 50];
  var enable_axis_x = true;
  var enable_axis_y = true;
  var x_axis_height = enable_axis_x ? 30 : 0;
  var left_width = enable_axis_y ? 40 : 0;

  // transform data, pre-calculate y0 bar stack offset
  data = d3.layout.stack()(data);

  // insert DOM
  var $canvas = $('
').insertAfter(el);

  // setup d3
  var svg = d3.select($canvas[0])
            .append("svg:svg")
            .attr("width", "100%")
            .attr("height", "100%")
            .attr("viewBox", [0, 0, width, height].join(" "))
            .attr("preserveAspectRatio", "xMinYMin meet");

  w = width - margin[1] - margin[3] - left_width;
  h = height - margin[0] - margin[2] - x_axis_height;
  var vis = svg
            .append("g")
            .attr("class", "vis")
            .attr("width", w)
            .attr("height", h);

  // continue d3 configuration
  var len_series = data.length;
  var len_x = data[0].length,
      min_x = data[0][0].x,
      max_x = data[0][len_x - 1].x,
      bar_width = w / len_x * 0.9,
      max_totaly = d3.max(data, function(d) {
        return d3.max(d, function(d) {
          return d.y0 + d.y;
        });
      }),
      // max_y = d3.max(data, function(d) {
      //   return d3.max(d, function(d) {
      //     return d.y;
      //   });
      // }),
      // map x value
      x_scale = d3.scale.linear()
                  .domain([min_x, max_x])
                  .range([0, w]),
      x = function(d) { return x_scale(d.x); },
      height_scale_stack,  // scaler for mapping height
      y_scale_stack,  // scaler for mapping y position
      y_stack;  // scaler for mapping y position considering stacked offset

  // sets global height and scales
  function rescale(new_height){
    max_totaly = new_height;
    height_scale_stack = d3.scale.linear()
                      .domain([0, max_totaly])
                      .range([0, h]);
    y_scale_stack = d3.scale.linear()
                      .domain([0, max_totaly])
                      .range([h, 0]);
    y_stack = function(d) { return y_scale_stack(d.y + d.y0); };
  }

  rescale(max_totaly);

  vis.attr("transform", "translate(" + (margin[3] + left_width) + "," + margin[0] + ")");

  // set up a layer for each series
  var layers = vis.selectAll("g.layer")
    .data(data)
    .enter().append("g")
      .attr("class", "layer")
      .style("fill", function(d, i) { return color(i / (len_series - 1)); });

  // setup a bar for each point in a series
  var bars = layers.selectAll("rect.bar")
    .data(function(d) { return d; })
    .enter().append("rect")
      .attr("class", "bar")
      .attr("width", bar_width)
      .attr("x", x)
      .attr("y", h)
      .attr("height", 0)
      .attr("transform", "translate(" + (-bar_width/2) + ",0)")
      .transition()
        .delay(function(d, i) { return i * 10; })
        // .attr("y", function(d) { return height_scale_stack(d.y0); })  // inverse
        .attr("y", function(d) { return y_scale_stack(d.y + d.y0); })
        .attr("height", function(d) { return height_scale_stack(d.y); });

  $('rect.bar', svg.$elem).tooltip({title: function(){
    return this.__data__.title;
  }});

  if (enable_axis_x) {
    x_axis = d3.svg.axis()
             .scale(x_scale)
             .tickSize(6, 1, 1)
             .tickFormat(function(a){ return a; });
    svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(" + (margin[3] + left_width) + "," + (height - margin[2] - x_axis_height) + ")")
        .call(x_axis);
  }

  if (enable_axis_y) {
    y_axis = d3.svg.axis()
             .scale(y_scale_stack)
             .orient("left");
    svg.append("g")
        .attr("class", "y axis")
        .attr("transform", "translate(" + margin[3] + "," + (margin[0]) + ")")
        .call(y_axis);
  }

  function set_data(new_data){
    // process add stack offsets
    data = d3.layout.stack()(new_data);

    // reset height ceiling
    ceiling = d3.max(data, function(d) {
      return d3.max(d, function(d) {
        return d.y0 + d.y;
      });
    });
    // if (ceiling > max_totaly) {
    //   rescale(ceiling);
    // }
    rescale(ceiling);

    // update layers data
    layers.data(data);
    // update bars data :(
    layers.selectAll("rect.bar")
      .data(function(d) { return d; })
      .transition()
        .attr("y", function(d) { return y_scale_stack(d.y + d.y0); })
        .attr("height", function(d) { return height_scale_stack(d.y); });
    return layers;
  }

  return {
    setData: set_data
  };
};
<commit_msg>clean up variable names<commit_after>/*
  Based on:
  http://mbostock.github.com/d3/ex/stack.html
*/

function buildTableData($table){
  // build data
  var $headings = $table.find('thead th').map(function(){ return $(this).text(); });
  var $rows = $table.find('tbody > tr');
  var data = $headings.slice(1).toArray().map(function(){ return []; });
  $rows.each(function(_, row){
    var $cells = $(row).children();
    var xVal = +$cells.eq(0).text();
    $cells.slice(1).each(function(idx, cell){
      data[idx].push({
        x: xVal,
        y: $(cell).data('value'),
        title: "" + $headings[idx + 1] + " " + xVal + " " + $.trim($(cell).text()) + ""
      });
    });
  });
  return data;
}

var stackedBarChart = function(el, data){
  var color = d3.scale.category10();
  var width = 940;
  var height = 300;
  var margin = [10, 50, 10, 50];
  var enable_axis_x = true;
  var enable_axis_y = true;
  var bottom_axis_height = enable_axis_x ? 30 : 0;
  var left_width = enable_axis_y ? 40 : 0;

  // configure plot area
  var plot = {
    w: width - margin[1] - margin[3] - left_width,
    h: height - margin[0] - margin[2] - bottom_axis_height
  };

  // transform data, pre-calculate y0 bar stack offset
  data = d3.layout.stack()(data);

  // continue d3 configuration
  var len_series = data.length;
  var len_x = data[0].length,
      min_x = data[0][0].x,
      max_x = data[0][len_x - 1].x,
      bar_width = plot.w / len_x * 0.9,
      max_totaly = d3.max(data, function(d) {
        return d3.max(d, function(d) {
          return d.y0 + d.y;
        });
      }),
      // max_y = d3.max(data, function(d) {
      //   return d3.max(d, function(d) {
      //     return d.y;
      //   });
      // }),
      // map x value
      x_scale = d3.scale.linear()
                  .domain([min_x, max_x])
                  .range([0, plot.w]),
      x = function(d) { return x_scale(d.x); },
      height_scale_stack,  // scaler for mapping height
      y_scale_stack,  // scaler for mapping y position
      y_stack;  // scaler for mapping y position considering stacked offset

  // sets global height and scales
  function rescale(new_height){
    height_scale_stack = d3.scale.linear()
                      .domain([0, new_height])
                      .range([0, plot.h]);
    y_scale_stack = d3.scale.linear()
                      .domain([0, new_height])
                      .range([plot.h, 0]);
    y_stack = function(d) { return y_scale_stack(d.y + d.y0); };
  }
  rescale(max_totaly);

  // insert DOM
  var $canvas = $('
').insertAfter(el);

  // setup d3 canvas
  var svg = d3.select($canvas[0])
            .append("svg:svg")
            .attr("width", "100%")
            .attr("height", "100%")
            .attr("viewBox", [0, 0, width, height].join(" "))
            .attr("preserveAspectRatio", "xMinYMin meet");

  // setup d3 plot area
  var vis = svg
            .append("g")
            .attr("class", "vis")
            .attr("width", plot.w)
            .attr("height", plot.h)
            .attr("transform", "translate(" + (margin[3] + left_width) + "," + margin[0] + ")");

  // set up a layer for each series
  var layers = vis.selectAll("g.layer")
    .data(data)
    .enter().append("g")
      .attr("class", "layer")
      .style("fill", function(d, i) { return color(i / (len_series - 1)); });

  // setup a bar for each point in a series
  var bars = layers.selectAll("rect.bar")
    .data(function(d) { return d; })
    .enter().append("rect")
      .attr("class", "bar")
      .attr("width", bar_width)
      .attr("x", x)
      .attr("y", plot.h)
      .attr("height", 0)
      .attr("transform", "translate(" + (-bar_width/2) + ",0)")
      .transition()
        .delay(function(d, i) { return i * 10; })
        // .attr("y", function(d) { return height_scale_stack(d.y0); })  // inverse
        .attr("y", function(d) { return y_scale_stack(d.y + d.y0); })
        .attr("height", function(d) { return height_scale_stack(d.y); });

  // tooltip
  $('rect.bar', svg.$elem).tooltip({title: function(){ return this.__data__.title; }});

  if (enable_axis_x) {
    x_axis = d3.svg.axis()
             .scale(x_scale)
             .tickSize(6, 1, 1)
             .tickFormat(function(a){ return a; });
    svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(" + (margin[3] + left_width) + "," + (height - margin[2] - bottom_axis_height) + ")")
        .call(x_axis);
  }

  if (enable_axis_y) {
    y_axis = d3.svg.axis()
             .scale(y_scale_stack)
             .orient("left");
    svg.append("g")
        .attr("class", "y axis")
        .attr("transform", "translate(" + margin[3] + "," + margin[0] + ")")
        .call(y_axis);
  }

  // interaction, new data api
  function set_data(new_data){
    // process add stack offsets
    data = d3.layout.stack()(new_data);

    // reset height ceiling
    ceiling = d3.max(data, function(d) {
      return d3.max(d, function(d) {
        return d.y0 + d.y;
      });
    });
    // if (ceiling > max_totaly) {
    //   rescale(ceiling);
    // }
    rescale(ceiling);

    // update layers data
    layers.data(data);
    // update bars data :(
    layers.selectAll("rect.bar")
      .data(function(d) { return d; })
      .transition()
        .attr("y", function(d) { return y_scale_stack(d.y + d.y0); })
        .attr("height", function(d) { return height_scale_stack(d.y); });
    return layers;
  }

  return {
    setData: set_data
  };
};
<|endoftext|>


<commit_before>import { UrlUtils } from '/static/vesper/util/url-utils.js';

const _NONZERO_COUNT_CIRCLE_COLOR = 'orange';
const _ZERO_COUNT_CIRCLE_COLOR = '#A0A0A0';
const _HIGHLIGHTED_CIRCLE_COLOR = '#00AA00';
const _MIN_RADIUS = 12.5;
const _RADIUS_SCALE_FACTOR = 7.5;

let circles = null;
let highlightedCircle = null;

// Module-level state, set via `init` function.
let state = null;

export function init(state_) {

    // Set module-level state.
    state = state_

	// Install event handlers.
    window.onload = onLoad;

}

function onLoad() {
	setTitle();
    addButtonEventListeners();
	setCalendarPeriods();
}

function setTitle() {

	const title =
		`${state.stationMicName} / ${state.detectorName} / ` +
		`${state.classification} Clips`;

	const titleElement = document.getElementById('title');
	titleElement.textContent = title;

	document.title = `Clip Calendar - ${title}`;

}

function addButtonEventListeners() {

    // filter clips button
    const filterButton = document.getElementById('filter-clips-button');
    filterButton.addEventListener(
        'click', e => $('#filter-clips-modal').modal('show'));

    // filter clips modal OK button
    const okButton = document.getElementById('filter-clips-modal-ok-button');
    okButton.onclick = onFilterClipsModalOkButtonClick;

}

function onFilterClipsModalOkButtonClick(event) {

    // TODO: Only set URL if query has changed.

    const encode = UrlUtils.encodeQueryParameterValue;

    const stationMic = encode(document.getElementById(
        'filter-clips-modal-station-mic-select').value);

    const detector = encode(document.getElementById(
        'filter-clips-modal-detector-select').value);

    const classification = encode(document.getElementById(
        'filter-clips-modal-classification-select').value);

    const url =
        `/clip-calendar/?` +
        `station_mic=${stationMic}&` +
        `detector=${detector}&` +
        `classification=${classification}`;

    window.location.href = url;

}

function setCalendarPeriods() {

	const periodsDiv = document.getElementById('periods');

	if (state.stationMicName === 'None')
		periodsDiv.innerHTML = 'There are no stations in the archive.';

	else if (state.detectorName === 'None')
		periodsDiv.innerHTML = 'There are no detectors in the archive.';

	else if (state.periods.length === 0)
		periodsDiv.innerHTML = 'There are no such clips in the archive.';

	else {

		// The server provides us with a Javascript array called `periods`,
		// each element of which describes a calendar period. It also provides
		// us with an empty <div> element with ID "periods" where the calendar
		// periods should go. We populate the <div> according to the contents
		// of the `periods` array.

		for (const period of state.periods)
			addCalendarPeriod(period, periodsDiv);

		circles = document.querySelectorAll('.circle');

		// Install mouse motion handler on calendar.
		const calendar = document.getElementById('calendar');
		calendar.addEventListener('mousemove', onMouseMove);
		calendar.addEventListener('click', onMouseClick);

	}

}

function addCalendarPeriod(period, periodsDiv) {

	const periodDiv = document.createElement('div');
	periodDiv.className = 'period';

	// Add period name.
//	const nameHeading = document.createElement('h2');
//	nameHeading.className = 'period-name';
//	nameHeading.textContent = period.name;
//	periodDiv.appendChild(nameHeading);

	// Add horizontal rule.
	const rule = document.createElement('hr');
	rule.className = 'period-rule';
	periodDiv.appendChild(rule);

	// Add period rows.
	const rows = getPeriodRows(period);
	for (const row of rows)
		addPeriodRow(row, periodDiv);

	periodsDiv.appendChild(periodDiv);

}

function addPeriodRow(row, periodDiv) {

	const rowDiv = document.createElement('div');
	rowDiv.className = 'row';

	for (const month of row)
		addRowMonth(month, rowDiv);

	periodDiv.appendChild(rowDiv);

}

function addRowMonth(month, rowDiv) {

	const monthDiv = document.createElement('div');
	monthDiv.className = 'col-sm-4 month';

	if (month !== null) {

		// Add month name.
		if (month.name !== null) {
		    const nameHeading = document.createElement('h3');
		    nameHeading.className = 'month-name';
		    nameHeading.textContent = month.name;
		    monthDiv.appendChild(nameHeading)
		}

		// Add month days.
		const daysDiv = document.createElement('div');
		daysDiv.className = 'month-days';
		for (const day of month.days)
			addMonthDay(day, daysDiv)
		monthDiv.appendChild(daysDiv);

	}

	rowDiv.appendChild(monthDiv);

}

function addMonthDay(day, daysDiv) {

	const dayDiv = document.createElement('div');
	dayDiv.className = 'day';

	if (day === null) {

		// Add empty div for layout.
		const empty = document.createElement('div');
		dayDiv.appendChild(empty);

	} else {

		// Get circle URL.

	    const encode = UrlUtils.encodeQueryParameterValue;
	    const stationMic = encode(state.stationMicName);
	    const detector = encode(state.detectorName);
	    const classification = encode(state.classification);
		const date = formatDate(day.date);

		const url =
		    `/night?` +
		    `station_mic=${stationMic}&` +
	        `detector=${detector}&` +
			`classification=${classification}&` +
			`date=${date}`;

		// Add day number.

		let num = null;

		if (day.count === undefined) {
			// no recordings for this day

			num = document.createElement('span');

		} else {
			// one or more recordings for this day

		    num = document.createElement('a');
	        num.href = url;

		}

		num.textContent = day.date.getDate();
		num.className = 'day-num';
		dayDiv.appendChild(num);

		// Add count circle.

		if (day.count !== undefined) {
			// one or more recordings for this day

			const circleDiv = document.createElement('div');
			circleDiv.className = 'circle';

			// Set circle attributes.
			circleDiv.setAttribute('data-url', url);
			circleDiv.setAttribute('data-count', day.count.toString());
			const radius = getCircleRadius(day.count).toFixed(1);
			circleDiv.setAttribute('data-radius', radius);

			// Set circle size.
			const diameter = 2 * radius;
			const size = `${diameter}px`;
			circleDiv.style.width = size;
			circleDiv.style.height = size;

			// Set circle color.
			circleDiv.style.background = getCircleColor(circleDiv);
			circleDiv.style.opacity = .7;

			dayDiv.appendChild(circleDiv);

		}

	}

	daysDiv.appendChild(dayDiv);

}

function getCircleColor(circle) {
	const count = Number.parseInt(circle.getAttribute('data-count'));
	return count === 0 ? _ZERO_COUNT_CIRCLE_COLOR : _NONZERO_COUNT_CIRCLE_COLOR;
}

function formatDate(d) {

	// Get four-digit year.
	const yyyy = d.getFullYear().toString();

	// Get two-digit month.
	let mm = (d.getMonth() + 1).toString();
	if (mm.length === 1)
		mm = '0' + mm;

	// Get two-digit day.
	let dd = d.getDate().toString();
	if (dd.length === 1)
		dd = '0' + dd;

	return yyyy + '-' + mm + '-' + dd;

}

function getPeriodRows(period) {

	let months = period.months;
	const numMonths = months.length;

	let initialMonths;
	let finalMonths;

	if (numMonths > 3) {

		const numInitialEmptyMonths = (months[0].month - 1) % 3;
		initialMonths = Array(numInitialEmptyMonths).fill(null);

		const m = (numInitialEmptyMonths + numMonths) % 3;
		const numFinalEmptyMonths = m === 0 ? 0 : 3 - m;
		finalMonths = Array(numFinalEmptyMonths).fill(null);

	} else {
		// three or fewer months

		initialMonths = [];
		finalMonths = Array(3 - numMonths).fill(null);

	}

	months = [].concat(initialMonths, months, finalMonths);

	const rows = [];
	for (let i = 0; i < months.length; i += 3) {
		const rowMonths = months.slice(i, i + 3);
		const monthInfos = rowMonths.map(getMonthInfo);
		rows.push(monthInfos);
	}

//    if (numMonths === 1) {
//        // only one month in this period
//
//        // Suppress display of month name since it's same as period name.
//        rows[0][0].name = null;
//
//    }

	return rows;

}

function getMonthInfo(month) {

	if (month === null) {

		return null;

	} else {

	    const name = getMonthName(month.month) + ' ' + month.year;
	    const numInitialEmptyDays = getWeekdayNumOfFirstMonthDay(month);
	    const length = getMonthLength(month);

	    // Create `dayCounts` object that maps day numbers (as strings)
	    // to counts.
	    const dayCounts = {}
	    for (const dayCount of month.dayCounts) {
	    	dayCounts[dayCount[0].toString()] = dayCount[1];
	    }

	    const days = Array(numInitialEmptyDays + length).fill(null);

	    for (let dayNum = 1; dayNum <= length; dayNum++) {
	    	const date = new Date(month.year, month.month - 1, dayNum);
	        const count = dayCounts[dayNum.toString()]
	        days[numInitialEmptyDays + dayNum - 1] =
	            { 'date': date, 'count': count }
	    }

	    return { 'name': name, 'days': days }

	}

}

const monthNames = [
    null, 'January', 'February', 'March', 'April', 'May', 'June',
    'July', 'August', 'September', 'October', 'November', 'December'];

/* This really isn't satisfactory since it doesn't support localization.
 * Perhaps we could use the recent ECMAScript Internationalization API on
 * browsers that support it, and fall back on the current implementation
 * on browsers that don't (e.g. Safari)?
 *
 * Relevant links:
 *     https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
 *         Global_Objects/DateTimeFormat
 *     http://stackoverflow.com/questions/1643320/get-month-name-from-date
 */
function getMonthName(month) {
	return monthNames[month];
}

function getWeekdayNumOfFirstMonthDay(month) {
	return (new Date(month.year, month.month - 1, 1)).getDay();
}

function getMonthLength(month) {
	return (new Date(month.year, month.month, 0)).getDate();
}

function getCircleRadius(count) {
	if (count === 0)
		return _MIN_RADIUS;
	else
		return _MIN_RADIUS + _RADIUS_SCALE_FACTOR * Math.log10(count);
}

function onMouseMove(event) {

	const x = event.pageX;
	const y = event.pageY;

	const circle = getCircleUnderneath(x, y);

	if (circle !== highlightedCircle) {
		// highlighted circle will change

		// Unhighlight old highlighted circle, if any.
		if (highlightedCircle !== null) {
		    highlightedCircle.style.backgroundColor =
		    	getCircleColor(highlightedCircle);
		    highlightedCircle.style.zIndex = '0';
		}

		highlightedCircle = circle;

		// Highlight new highlighted circle, if any.
		if (highlightedCircle !== null) {
		    highlightedCircle.style.backgroundColor = _HIGHLIGHTED_CIRCLE_COLOR;
		    highlightedCircle.style.zIndex = '1';
		}

	}

}

function getCircleUnderneath(x, y) {

	let circleUnderneath = null;
	let distanceUnderneath = null;

	for (const circle of circles) {

		const rect = circle.getBoundingClientRect();
		const left = rect.left + window.scrollX;
		const top = rect.top + window.scrollY;

		const radius = parseInt(circle.getAttribute('data-radius'));

		const centerX = left + radius;
		const centerY = top + radius;

		const distance = getDistance(x, y, centerX, centerY);

		if (distance <= radius &&
		        (distanceUnderneath === null || distance < distanceUnderneath)) {
			    // circle is underneath mouse, and its center is closer
			    // to the mouse than the center of any other circle yet
			    // encountered

			circleUnderneath = circle;
			distanceUnderneath = distance;

		}

	}

	return circleUnderneath;

}

function getDistance(xa, ya, xb, yb) {
	const dx = xa - xb;
	const dy = ya - yb;
	return Math.sqrt(dx * dx + dy * dy);
}

function onMouseClick(event) {

	const x = event.pageX;
	const y = event.pageY;

	const circle = getCircleUnderneath(x, y);

	if (circle !== null) {

		const url = circle.getAttribute('data-url');

		if (url !== '')
		    window.location.href = url;

	}

}
<commit_msg>Tweaked clip calendar title.<commit_after>import { UrlUtils } from '/static/vesper/util/url-utils.js';

const _NONZERO_COUNT_CIRCLE_COLOR = 'orange';
const _ZERO_COUNT_CIRCLE_COLOR = '#A0A0A0';
const _HIGHLIGHTED_CIRCLE_COLOR = '#00AA00';
const _MIN_RADIUS = 12.5;
const _RADIUS_SCALE_FACTOR = 7.5;

let circles = null;
let highlightedCircle = null;

// Module-level state, set via `init` function.
let state = null;

export function init(state_) {

    // Set module-level state.
    state = state_

	// Install event handlers.
    window.onload = onLoad;

}

function onLoad() {
	setTitle();
    addButtonEventListeners();
	setCalendarPeriods();
}

function setTitle() {

	const title =
		`${state.stationMicName} / ${state.detectorName} / ` +
		`${state.classification}`;

	const titleElement = document.getElementById('title');
	titleElement.textContent = `${title} Clips`;

	document.title = `Clip Calendar - ${title}`;

}

function addButtonEventListeners() {

    // filter clips button
    const filterButton = document.getElementById('filter-clips-button');
    filterButton.addEventListener(
        'click', e => $('#filter-clips-modal').modal('show'));

    // filter clips modal OK button
    const okButton = document.getElementById('filter-clips-modal-ok-button');
    okButton.onclick = onFilterClipsModalOkButtonClick;

}

function onFilterClipsModalOkButtonClick(event) {

    // TODO: Only set URL if query has changed.

    const encode = UrlUtils.encodeQueryParameterValue;

    const stationMic = encode(document.getElementById(
        'filter-clips-modal-station-mic-select').value);

    const detector = encode(document.getElementById(
        'filter-clips-modal-detector-select').value);

    const classification = encode(document.getElementById(
        'filter-clips-modal-classification-select').value);

    const url =
        `/clip-calendar/?` +
        `station_mic=${stationMic}&` +
        `detector=${detector}&` +
        `classification=${classification}`;

    window.location.href = url;

}

function setCalendarPeriods() {

	const periodsDiv = document.getElementById('periods');

	if (state.stationMicName === 'None')
		periodsDiv.innerHTML = 'There are no stations in the archive.';

	else if (state.detectorName === 'None')
		periodsDiv.innerHTML = 'There are no detectors in the archive.';

	else if (state.periods.length === 0)
		periodsDiv.innerHTML = 'There are no such clips in the archive.';

	else {

		// The server provides us with a Javascript array called `periods`,
		// each element of which describes a calendar period. It also provides
		// us with an empty <div> element with ID "periods" where the calendar
		// periods should go. We populate the <div> according to the contents
		// of the `periods` array.

		for (const period of state.periods)
			addCalendarPeriod(period, periodsDiv);

		circles = document.querySelectorAll('.circle');

		// Install mouse motion handler on calendar.
		const calendar = document.getElementById('calendar');
		calendar.addEventListener('mousemove', onMouseMove);
		calendar.addEventListener('click', onMouseClick);

	}

}

function addCalendarPeriod(period, periodsDiv) {

	const periodDiv = document.createElement('div');
	periodDiv.className = 'period';

	// Add period name.
//	const nameHeading = document.createElement('h2');
//	nameHeading.className = 'period-name';
//	nameHeading.textContent = period.name;
//	periodDiv.appendChild(nameHeading);

	// Add horizontal rule.
	const rule = document.createElement('hr');
	rule.className = 'period-rule';
	periodDiv.appendChild(rule);

	// Add period rows.
	const rows = getPeriodRows(period);
	for (const row of rows)
		addPeriodRow(row, periodDiv);

	periodsDiv.appendChild(periodDiv);

}

function addPeriodRow(row, periodDiv) {

	const rowDiv = document.createElement('div');
	rowDiv.className = 'row';

	for (const month of row)
		addRowMonth(month, rowDiv);

	periodDiv.appendChild(rowDiv);

}

function addRowMonth(month, rowDiv) {

	const monthDiv = document.createElement('div');
	monthDiv.className = 'col-sm-4 month';

	if (month !== null) {

		// Add month name.
		if (month.name !== null) {
		    const nameHeading = document.createElement('h3');
		    nameHeading.className = 'month-name';
		    nameHeading.textContent = month.name;
		    monthDiv.appendChild(nameHeading)
		}

		// Add month days.
		const daysDiv = document.createElement('div');
		daysDiv.className = 'month-days';
		for (const day of month.days)
			addMonthDay(day, daysDiv)
		monthDiv.appendChild(daysDiv);

	}

	rowDiv.appendChild(monthDiv);

}

function addMonthDay(day, daysDiv) {

	const dayDiv = document.createElement('div');
	dayDiv.className = 'day';

	if (day === null) {

		// Add empty div for layout.
		const empty = document.createElement('div');
		dayDiv.appendChild(empty);

	} else {

		// Get circle URL.

	    const encode = UrlUtils.encodeQueryParameterValue;
	    const stationMic = encode(state.stationMicName);
	    const detector = encode(state.detectorName);
	    const classification = encode(state.classification);
		const date = formatDate(day.date);

		const url =
		    `/night?` +
		    `station_mic=${stationMic}&` +
	        `detector=${detector}&` +
			`classification=${classification}&` +
			`date=${date}`;

		// Add day number.

		let num = null;

		if (day.count === undefined) {
			// no recordings for this day

			num = document.createElement('span');

		} else {
			// one or more recordings for this day

		    num = document.createElement('a');
	        num.href = url;

		}

		num.textContent = day.date.getDate();
		num.className = 'day-num';
		dayDiv.appendChild(num);

		// Add count circle.

		if (day.count !== undefined) {
			// one or more recordings for this day

			const circleDiv = document.createElement('div');
			circleDiv.className = 'circle';

			// Set circle attributes.
			circleDiv.setAttribute('data-url', url);
			circleDiv.setAttribute('data-count', day.count.toString());
			const radius = getCircleRadius(day.count).toFixed(1);
			circleDiv.setAttribute('data-radius', radius);

			// Set circle size.
			const diameter = 2 * radius;
			const size = `${diameter}px`;
			circleDiv.style.width = size;
			circleDiv.style.height = size;

			// Set circle color.
			circleDiv.style.background = getCircleColor(circleDiv);
			circleDiv.style.opacity = .7;

			dayDiv.appendChild(circleDiv);

		}

	}

	daysDiv.appendChild(dayDiv);

}

function getCircleColor(circle) {
	const count = Number.parseInt(circle.getAttribute('data-count'));
	return count === 0 ? _ZERO_COUNT_CIRCLE_COLOR : _NONZERO_COUNT_CIRCLE_COLOR;
}

function formatDate(d) {

	// Get four-digit year.
	const yyyy = d.getFullYear().toString();

	// Get two-digit month.
	let mm = (d.getMonth() + 1).toString();
	if (mm.length === 1)
		mm = '0' + mm;

	// Get two-digit day.
	let dd = d.getDate().toString();
	if (dd.length === 1)
		dd = '0' + dd;

	return yyyy + '-' + mm + '-' + dd;

}

function getPeriodRows(period) {

	let months = period.months;
	const numMonths = months.length;

	let initialMonths;
	let finalMonths;

	if (numMonths > 3) {

		const numInitialEmptyMonths = (months[0].month - 1) % 3;
		initialMonths = Array(numInitialEmptyMonths).fill(null);

		const m = (numInitialEmptyMonths + numMonths) % 3;
		const numFinalEmptyMonths = m === 0 ? 0 : 3 - m;
		finalMonths = Array(numFinalEmptyMonths).fill(null);

	} else {
		// three or fewer months

		initialMonths = [];
		finalMonths = Array(3 - numMonths).fill(null);

	}

	months = [].concat(initialMonths, months, finalMonths);

	const rows = [];
	for (let i = 0; i < months.length; i += 3) {
		const rowMonths = months.slice(i, i + 3);
		const monthInfos = rowMonths.map(getMonthInfo);
		rows.push(monthInfos);
	}

//    if (numMonths === 1) {
//        // only one month in this period
//
//        // Suppress display of month name since it's same as period name.
//        rows[0][0].name = null;
//
//    }

	return rows;

}

function getMonthInfo(month) {

	if (month === null) {

		return null;

	} else {

	    const name = getMonthName(month.month) + ' ' + month.year;
	    const numInitialEmptyDays = getWeekdayNumOfFirstMonthDay(month);
	    const length = getMonthLength(month);

	    // Create `dayCounts` object that maps day numbers (as strings)
	    // to counts.
	    const dayCounts = {}
	    for (const dayCount of month.dayCounts) {
	    	dayCounts[dayCount[0].toString()] = dayCount[1];
	    }

	    const days = Array(numInitialEmptyDays + length).fill(null);

	    for (let dayNum = 1; dayNum <= length; dayNum++) {
	    	const date = new Date(month.year, month.month - 1, dayNum);
	        const count = dayCounts[dayNum.toString()]
	        days[numInitialEmptyDays + dayNum - 1] =
	            { 'date': date, 'count': count }
	    }

	    return { 'name': name, 'days': days }

	}

}

const monthNames = [
    null, 'January', 'February', 'March', 'April', 'May', 'June',
    'July', 'August', 'September', 'October', 'November', 'December'];

/* This really isn't satisfactory since it doesn't support localization.
 * Perhaps we could use the recent ECMAScript Internationalization API on
 * browsers that support it, and fall back on the current implementation
 * on browsers that don't (e.g. Safari)?
 *
 * Relevant links:
 *     https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
 *         Global_Objects/DateTimeFormat
 *     http://stackoverflow.com/questions/1643320/get-month-name-from-date
 */
function getMonthName(month) {
	return monthNames[month];
}

function getWeekdayNumOfFirstMonthDay(month) {
	return (new Date(month.year, month.month - 1, 1)).getDay();
}

function getMonthLength(month) {
	return (new Date(month.year, month.month, 0)).getDate();
}

function getCircleRadius(count) {
	if (count === 0)
		return _MIN_RADIUS;
	else
		return _MIN_RADIUS + _RADIUS_SCALE_FACTOR * Math.log10(count);
}

function onMouseMove(event) {

	const x = event.pageX;
	const y = event.pageY;

	const circle = getCircleUnderneath(x, y);

	if (circle !== highlightedCircle) {
		// highlighted circle will change

		// Unhighlight old highlighted circle, if any.
		if (highlightedCircle !== null) {
		    highlightedCircle.style.backgroundColor =
		    	getCircleColor(highlightedCircle);
		    highlightedCircle.style.zIndex = '0';
		}

		highlightedCircle = circle;

		// Highlight new highlighted circle, if any.
		if (highlightedCircle !== null) {
		    highlightedCircle.style.backgroundColor = _HIGHLIGHTED_CIRCLE_COLOR;
		    highlightedCircle.style.zIndex = '1';
		}

	}

}

function getCircleUnderneath(x, y) {

	let circleUnderneath = null;
	let distanceUnderneath = null;

	for (const circle of circles) {

		const rect = circle.getBoundingClientRect();
		const left = rect.left + window.scrollX;
		const top = rect.top + window.scrollY;

		const radius = parseInt(circle.getAttribute('data-radius'));

		const centerX = left + radius;
		const centerY = top + radius;

		const distance = getDistance(x, y, centerX, centerY);

		if (distance <= radius &&
		        (distanceUnderneath === null || distance < distanceUnderneath)) {
			    // circle is underneath mouse, and its center is closer
			    // to the mouse than the center of any other circle yet
			    // encountered

			circleUnderneath = circle;
			distanceUnderneath = distance;

		}

	}

	return circleUnderneath;

}

function getDistance(xa, ya, xb, yb) {
	const dx = xa - xb;
	const dy = ya - yb;
	return Math.sqrt(dx * dx + dy * dy);
}

function onMouseClick(event) {

	const x = event.pageX;
	const y = event.pageY;

	const circle = getCircleUnderneath(x, y);

	if (circle !== null) {

		const url = circle.getAttribute('data-url');

		if (url !== '')
		    window.location.href = url;

	}

}
<|endoftext|>


<commit_before>/**
 * Utility class to align elements relatively to another.
 *
 * @author	Alexander Ebert
 * @copyright	2001-2015 WoltLab GmbH
 * @license	GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @module	WoltLab/WCF/Ui/Alignment
 */
define(['Core', 'Language', 'Dom/Traverse', 'Dom/Util'], function(Core, Language, DomTraverse, DomUtil) {
	"use strict";

	/**
	 * @exports	WoltLab/WCF/Ui/Alignment
	 */
	return {
		/**
		 * Sets the alignment for target element relatively to the reference element.
		 *
		 * @param	{Element}		el		target element
		 * @param	{Element}		ref		reference element
		 * @param	{Object<string, *>}	options		list of options to alter the behavior
		 */
		set: function(el, ref, options) {
			options = Core.extend({
				// offset to reference element
				verticalOffset: 10,

				// align the pointer element, expects .elementPointer as a direct child of given element
				pointer: false,

				// offset from/left side, ignored for center alignment
				pointerOffset: 4,

				// use static pointer positions, expects two items: class to move it to the bottom and the second to move it to the right
				pointerClassNames: [],

				// alternate element used to calculate dimensions
				refDimensionsElement: null,

				// preferred alignment, possible values: left/right/center and top/bottom
				horizontal: 'left',
				vertical: 'bottom',

				// allow flipping over axis, possible values: both, horizontal, vertical and none
				allowFlip: 'both'
			}, options);

			if (!Array.isArray(options.pointerClassNames) || options.pointerClassNames.length !== (options.pointer ? 1 : 2)) options.pointerClassNames = [];
			if (['left', 'right', 'center'].indexOf(options.horizontal) === -1) options.horizontal = 'left';
			if (options.vertical !== 'bottom') options.vertical = 'top';
			if (['both', 'horizontal', 'vertical', 'none'].indexOf(options.allowFlip) === -1) options.allowFlip = 'both';

			// place element in the upper left corner to prevent calculation issues due to possible scrollbars
			DomUtil.setStyles(el, {
				bottom: 'auto !important',
				left: '0 !important',
				right: 'auto !important',
				top: '0 !important',
				visibility: 'hidden !important'
			});

			var elDimensions = DomUtil.outerDimensions(el);
			var refDimensions = DomUtil.outerDimensions((options.refDimensionsElement instanceof Element ? options.refDimensionsElement : ref));
			var refOffsets = DomUtil.offset(ref);
			var windowHeight = window.innerHeight;
			var windowWidth = document.body.clientWidth;

			var horizontal = { result: null };
			var alignCenter = false;
			if (options.horizontal === 'center') {
				alignCenter = true;
				horizontal = this._tryAlignmentHorizontal(options.horizontal, elDimensions, refDimensions, refOffsets, windowWidth);

				if (!horizontal.result) {
					if (options.allowFlip === 'both' || options.allowFlip === 'horizontal') {
						options.horizontal = 'left';
					}
					else {
						horizontal.result = true;
					}
				}
			}

			// in rtl languages we simply swap the value for 'horizontal'
			if (Language.get('wcf.global.pageDirection') === 'rtl') {
				options.horizontal = (options.horizontal === 'left') ? 'right' : 'left';
			}

			if (!horizontal.result) {
				var horizontalCenter = horizontal;
				horizontal = this._tryAlignmentHorizontal(options.horizontal, elDimensions, refDimensions, refOffsets, windowWidth);
				if (!horizontal.result && (options.allowFlip === 'both' || options.allowFlip === 'horizontal')) {
					var horizontalFlipped = this._tryAlignmentHorizontal((options.horizontal === 'left' ? 'right' : 'left'), elDimensions, refDimensions, refOffsets, windowWidth);
					// only use these results if it fits into the boundaries, otherwise both directions exceed and we honor the demanded direction
					if (horizontalFlipped.result) {
						horizontal = horizontalFlipped;
					}
					else if (alignCenter) {
						horizontal = horizontalCenter;
					}
				}
			}

			var left = horizontal.left;
			var right = horizontal.right;

			var vertical = this._tryAlignmentVertical(options.vertical, elDimensions, refDimensions, refOffsets, windowHeight, options.verticalOffset);
			if (!vertical.result && (options.allowFlip === 'both' || options.allowFlip === 'vertical')) {
				var verticalFlipped = this._tryAlignmentVertical((options.vertical === 'top' ? 'bottom' : 'top'), elDimensions, refDimensions, refOffsets, windowHeight, options.verticalOffset);
				// only use these results if it fits into the boundaries, otherwise both directions exceed and we honor the demanded direction
				if (verticalFlipped.result) {
					vertical = verticalFlipped;
				}
			}

			var bottom = vertical.bottom;
			var top = vertical.top;

			// set pointer position
			if (options.pointer) {
				var pointer = DomTraverse.childrenByClass(el, 'elementPointer');
				pointer = pointer[0] || null;
				if (pointer === null) {
					throw new Error("Expected the .elementPointer element to be a direct children.");
				}

				if (horizontal.align === 'center') {
					pointer.classList.add('center');

					pointer.classList.remove('left');
					pointer.classList.remove('right');
				}
				else {
					pointer.classList.add(horizontal.align);

					pointer.classList.remove('center');
					pointer.classList.remove(horizontal.align === 'left' ? 'right' : 'left');
				}

				if (vertical.align === 'top') {
					pointer.classList.add('flipVertical');
				}
				else {
					pointer.classList.remove('flipVertical');
				}
			}
			else if (options.pointerClassNames.length === 2) {
				var pointerBottom = 0;
				var pointerRight = 1;

				el.classList[(top === 'auto' ? 'add' : 'remove')](options.pointerClassNames[pointerBottom]);
				el.classList[(left === 'auto' ? 'add' : 'remove')](options.pointerClassNames[pointerRight]);
			}

			DomUtil.setStyles(el, {
				bottom: bottom + (bottom !== 'auto' ? 'px' : ''),
				left: left + (left !== 'auto' ? 'px' : ''),
				right: right + (right !== 'auto' ? 'px' : ''),
				top: top + (top !== 'auto' ? 'px' : '')
			});

			elShow(el);
			el.style.removeProperty('visibility');
		},

		/**
		 * Calculates left/right position and verifies if the element would be still within the page's boundaries.
		 *
		 * @param	{string}		align		align to this side of the reference element
		 * @param	{Object<string, int>}	elDimensions	element dimensions
		 * @param	{Object<string, int>}	refDimensions	reference element dimensions
		 * @param	{Object<string, int>}	refOffsets	position of reference element relative to the document
		 * @param	{int}			windowWidth	window width
		 * @returns	{Object<string, *>}	calculation results
		 */
		_tryAlignmentHorizontal: function(align, elDimensions, refDimensions, refOffsets, windowWidth) {
			var left = 'auto';
			var right = 'auto';
			var result = true;

			if (align === 'left') {
				left = refOffsets.left;
				if (left + elDimensions.width > windowWidth) {
					result = false;
				}
			}
			else if (align === 'right') {
				right = windowWidth - (refOffsets.left + refDimensions.width);
				if (right < 0) {
					result = false;
				}
			}
			else {
				left = refOffsets.left + (refDimensions.width / 2) - (elDimensions.width / 2);
				left = ~~left;

				if (left < 0 || left + elDimensions.width > windowWidth) {
					result = false;
				}
			}

			return {
				align: align,
				left: left,
				right: right,
				result: result
			};
		},

		/**
		 * Calculates top/bottom position and verifys if the element would be still within the page's boundaries.
		 *
		 * @param	{string}		align		align to this side of the reference element
		 * @param	{Object<string, int>}	elDimensions	element dimensions
		 * @param	{Object<string, int>}	refDimensions	reference element dimensions
		 * @param	{Object<string, int>}	refOffsets	position of reference element relative to the document
		 * @param	{int}			windowHeight	window height
		 * @param	{int}			verticalOffset	desired gap between element and reference element
		 * @returns	{object<string, *>}	calculation results
		 */
		_tryAlignmentVertical: function(align, elDimensions, refDimensions, refOffsets, windowHeight, verticalOffset) {
			var bottom = 'auto';
			var top = 'auto';
			var result = true;

			if (align === 'top') {
				var bodyHeight = document.body.clientHeight;
				bottom = (bodyHeight - refOffsets.top) + verticalOffset;
				if (bodyHeight - (bottom + elDimensions.height) < window.scrollY) {
					result = false;
				}
			}
			else {
				top = refOffsets.top + refDimensions.height + verticalOffset;
				if (top + elDimensions.height - window.scrollY > windowHeight) {
					result = false;
				}
			}

			return {
				align: align,
				bottom: bottom,
				top: top,
				result: result
			};
		}
	};
});
<commit_msg>Avoid rounding errors when positioning elements<commit_after>/**
 * Utility class to align elements relatively to another.
 *
 * @author	Alexander Ebert
 * @copyright	2001-2015 WoltLab GmbH
 * @license	GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 * @module	WoltLab/WCF/Ui/Alignment
 */
define(['Core', 'Language', 'Dom/Traverse', 'Dom/Util'], function(Core, Language, DomTraverse, DomUtil) {
	"use strict";

	/**
	 * @exports	WoltLab/WCF/Ui/Alignment
	 */
	return {
		/**
		 * Sets the alignment for target element relatively to the reference element.
		 *
		 * @param	{Element}		el		target element
		 * @param	{Element}		ref		reference element
		 * @param	{Object<string, *>}	options		list of options to alter the behavior
		 */
		set: function(el, ref, options) {
			options = Core.extend({
				// offset to reference element
				verticalOffset: 10,

				// align the pointer element, expects .elementPointer as a direct child of given element
				pointer: false,

				// offset from/left side, ignored for center alignment
				pointerOffset: 4,

				// use static pointer positions, expects two items: class to move it to the bottom and the second to move it to the right
				pointerClassNames: [],

				// alternate element used to calculate dimensions
				refDimensionsElement: null,

				// preferred alignment, possible values: left/right/center and top/bottom
				horizontal: 'left',
				vertical: 'bottom',

				// allow flipping over axis, possible values: both, horizontal, vertical and none
				allowFlip: 'both'
			}, options);

			if (!Array.isArray(options.pointerClassNames) || options.pointerClassNames.length !== (options.pointer ? 1 : 2)) options.pointerClassNames = [];
			if (['left', 'right', 'center'].indexOf(options.horizontal) === -1) options.horizontal = 'left';
			if (options.vertical !== 'bottom') options.vertical = 'top';
			if (['both', 'horizontal', 'vertical', 'none'].indexOf(options.allowFlip) === -1) options.allowFlip = 'both';

			// place element in the upper left corner to prevent calculation issues due to possible scrollbars
			DomUtil.setStyles(el, {
				bottom: 'auto !important',
				left: '0 !important',
				right: 'auto !important',
				top: '0 !important',
				visibility: 'hidden !important'
			});

			var elDimensions = DomUtil.outerDimensions(el);
			var refDimensions = DomUtil.outerDimensions((options.refDimensionsElement instanceof Element ? options.refDimensionsElement : ref));
			var refOffsets = DomUtil.offset(ref);
			var windowHeight = window.innerHeight;
			var windowWidth = document.body.clientWidth;

			var horizontal = { result: null };
			var alignCenter = false;
			if (options.horizontal === 'center') {
				alignCenter = true;
				horizontal = this._tryAlignmentHorizontal(options.horizontal, elDimensions, refDimensions, refOffsets, windowWidth);

				if (!horizontal.result) {
					if (options.allowFlip === 'both' || options.allowFlip === 'horizontal') {
						options.horizontal = 'left';
					}
					else {
						horizontal.result = true;
					}
				}
			}

			// in rtl languages we simply swap the value for 'horizontal'
			if (Language.get('wcf.global.pageDirection') === 'rtl') {
				options.horizontal = (options.horizontal === 'left') ? 'right' : 'left';
			}

			if (!horizontal.result) {
				var horizontalCenter = horizontal;
				horizontal = this._tryAlignmentHorizontal(options.horizontal, elDimensions, refDimensions, refOffsets, windowWidth);
				if (!horizontal.result && (options.allowFlip === 'both' || options.allowFlip === 'horizontal')) {
					var horizontalFlipped = this._tryAlignmentHorizontal((options.horizontal === 'left' ? 'right' : 'left'), elDimensions, refDimensions, refOffsets, windowWidth);
					// only use these results if it fits into the boundaries, otherwise both directions exceed and we honor the demanded direction
					if (horizontalFlipped.result) {
						horizontal = horizontalFlipped;
					}
					else if (alignCenter) {
						horizontal = horizontalCenter;
					}
				}
			}

			var left = horizontal.left;
			var right = horizontal.right;

			var vertical = this._tryAlignmentVertical(options.vertical, elDimensions, refDimensions, refOffsets, windowHeight, options.verticalOffset);
			if (!vertical.result && (options.allowFlip === 'both' || options.allowFlip === 'vertical')) {
				var verticalFlipped = this._tryAlignmentVertical((options.vertical === 'top' ? 'bottom' : 'top'), elDimensions, refDimensions, refOffsets, windowHeight, options.verticalOffset);
				// only use these results if it fits into the boundaries, otherwise both directions exceed and we honor the demanded direction
				if (verticalFlipped.result) {
					vertical = verticalFlipped;
				}
			}

			var bottom = vertical.bottom;
			var top = vertical.top;

			// set pointer position
			if (options.pointer) {
				var pointer = DomTraverse.childrenByClass(el, 'elementPointer');
				pointer = pointer[0] || null;
				if (pointer === null) {
					throw new Error("Expected the .elementPointer element to be a direct children.");
				}

				if (horizontal.align === 'center') {
					pointer.classList.add('center');

					pointer.classList.remove('left');
					pointer.classList.remove('right');
				}
				else {
					pointer.classList.add(horizontal.align);

					pointer.classList.remove('center');
					pointer.classList.remove(horizontal.align === 'left' ? 'right' : 'left');
				}

				if (vertical.align === 'top') {
					pointer.classList.add('flipVertical');
				}
				else {
					pointer.classList.remove('flipVertical');
				}
			}
			else if (options.pointerClassNames.length === 2) {
				var pointerBottom = 0;
				var pointerRight = 1;

				el.classList[(top === 'auto' ? 'add' : 'remove')](options.pointerClassNames[pointerBottom]);
				el.classList[(left === 'auto' ? 'add' : 'remove')](options.pointerClassNames[pointerRight]);
			}

			if (bottom !== 'auto') bottom = Math.round(bottom) + 'px';
			if (left !== 'auto') left = Math.ceil(left) + 'px';
			if (right !== 'auto') right = Math.floor(right) + 'px';
			if (top !== 'auto') top = Math.round(top) + 'px';

			DomUtil.setStyles(el, {
				bottom: bottom,
				left: left,
				right: right,
				top: top
			});

			elShow(el);
			el.style.removeProperty('visibility');
		},

		/**
		 * Calculates left/right position and verifies if the element would be still within the page's boundaries.
		 *
		 * @param	{string}		align		align to this side of the reference element
		 * @param	{Object<string, int>}	elDimensions	element dimensions
		 * @param	{Object<string, int>}	refDimensions	reference element dimensions
		 * @param	{Object<string, int>}	refOffsets	position of reference element relative to the document
		 * @param	{int}			windowWidth	window width
		 * @returns	{Object<string, *>}	calculation results
		 */
		_tryAlignmentHorizontal: function(align, elDimensions, refDimensions, refOffsets, windowWidth) {
			var left = 'auto';
			var right = 'auto';
			var result = true;

			if (align === 'left') {
				left = refOffsets.left;
				if (left + elDimensions.width > windowWidth) {
					result = false;
				}
			}
			else if (align === 'right') {
				right = windowWidth - (refOffsets.left + refDimensions.width);
				if (right < 0) {
					result = false;
				}
			}
			else {
				left = refOffsets.left + (refDimensions.width / 2) - (elDimensions.width / 2);
				left = ~~left;

				if (left < 0 || left + elDimensions.width > windowWidth) {
					result = false;
				}
			}

			return {
				align: align,
				left: left,
				right: right,
				result: result
			};
		},

		/**
		 * Calculates top/bottom position and verifys if the element would be still within the page's boundaries.
		 *
		 * @param	{string}		align		align to this side of the reference element
		 * @param	{Object<string, int>}	elDimensions	element dimensions
		 * @param	{Object<string, int>}	refDimensions	reference element dimensions
		 * @param	{Object<string, int>}	refOffsets	position of reference element relative to the document
		 * @param	{int}			windowHeight	window height
		 * @param	{int}			verticalOffset	desired gap between element and reference element
		 * @returns	{object<string, *>}	calculation results
		 */
		_tryAlignmentVertical: function(align, elDimensions, refDimensions, refOffsets, windowHeight, verticalOffset) {
			var bottom = 'auto';
			var top = 'auto';
			var result = true;

			if (align === 'top') {
				var bodyHeight = document.body.clientHeight;
				bottom = (bodyHeight - refOffsets.top) + verticalOffset;
				if (bodyHeight - (bottom + elDimensions.height) < window.scrollY) {
					result = false;
				}
			}
			else {
				top = refOffsets.top + refDimensions.height + verticalOffset;
				if (top + elDimensions.height - window.scrollY > windowHeight) {
					result = false;
				}
			}

			return {
				align: align,
				bottom: bottom,
				top: top,
				result: result
			};
		}
	};
});
<|endoftext|>


<commit_before>import contains from 'lodash/collection/contains';
import { camelizeKeys } from 'humps';
import * as api from 'app/utils/apiCallCreators';
import execute from 'app/utils/executeApiCall';
import * as ActionTypes from 'app/ActionTypes';
import { normalize } from 'app/utils/normalizer';

const TYPING_TIME = 10 * 1000;

export function loadConversations() {
  return {
    types: [
      ActionTypes.REQUEST_CONVERSATIONS,
      ActionTypes.RECEIVE_CONVERSATIONS,
      ActionTypes.FAILURE_CONVERSATIONS,
    ],
    callApi: api.getConversations(),
    getPagination: state => state.pagination.conversations,
    condition: state => {
      return !state.pagination.conversations.isLoading
              && (!state.pagination.conversations.ids
                || state.pagination.conversations.nextCursor);
    },
  };
}

export function loadConversation(conversationId) {
  return {
    types: [
      ActionTypes.REQUEST_CONVERSATION,
      ActionTypes.RECEIVE_CONVERSATION,
      ActionTypes.FAILURE_CONVERSATION,
    ],
    payload: { conversationId },
    callApi: api.getConversation(conversationId),
    condition: state => {
      const conversation = state.entities.conversations[conversationId];
      return !conversation || !conversation.members || !conversation.messages;
    },
  };
}

export function loadMessages(conversationId) {
  return {
    types: [
      ActionTypes.REQUEST_MESSAGES,
      ActionTypes.RECEIVE_MESSAGES,
      ActionTypes.FAILURE_MESSAGES,
    ],
    payload: { conversationId },
    callApi: api.getMessages(conversationId),
    getPagination: state => state.pagination.messagesByConversation[conversationId],
    condition: ({ pagination: { messagesByConversation } }) => {
      const messagePagination = messagesByConversation[conversationId];
      return !messagePagination
              || (!messagePagination.isLoading
                && (!messagePagination.ids || messagePagination.nextCursor));
    },
  };
}

export function sendTyping(conversationId) { // TODO: refactor
  return (dispatch, getState) => {
    const { user, entities: { conversations } } = getState();
    const conversation = conversations[conversationId];
    if (contains(conversation.typingUserIds, user.id)) {
      return;
    }
    dispatch({
      type: ActionTypes.SEND_TYPING,
      payload: { conversationId },
    });
    execute(api.sendTyping(conversationId));
  };
}

function stopTyping({ conversationId, userId }) {
  return {
    type: ActionTypes.STOP_TYPING,
    payload: { conversationId, userId },
  };
}

function startTyping({ conversationId, userId }) {
  return {
    type: ActionTypes.START_TYPING,
    payload: { conversationId, userId },
  };
}

export function receiveTyping({ conversationId, userId }) {
  return dispatch => {
    dispatch(startTyping({ conversationId, userId }));
    setTimeout(() => {
      // We would like to clear this timeout in `stopTyping`, but then we
      // would have to manage a (conversationId, userId) -> timeout collection.
      dispatch(stopTyping({ conversationId, userId }));
    }, TYPING_TIME);
  };
}

export function sendMessage({ conversationId, text }) {
  return (dispatch, getState) => {
    const { user: { id: userId } } = getState();
    dispatch(stopTyping({ conversationId, userId }));
    dispatch({
      type: ActionTypes.SEND_MESSAGE,
      payload: { conversationId, text },
    });
    execute(api.sendMessage({ conversationId, text }));
  };
}

export function receiveMessage({ conversationId, message }) {
  return dispatch => {
    dispatch(stopTyping({ conversationId, userId: message.author }));
    dispatch({
      type: ActionTypes.RECEIVE_MESSAGE,
      payload: { conversationId },
      // TODO: normalize somewhere else
      // TODO: better name
      response: normalize(camelizeKeys(message), api.message),
    });
  };
}
<commit_msg>client: fix sending messages<commit_after>import contains from 'lodash/collection/contains';
import { camelizeKeys } from 'humps';
import * as api from 'app/utils/apiCallCreators';
import execute from 'app/utils/executeApiCall';
import * as ActionTypes from 'app/ActionTypes';
import { normalize } from 'app/utils/normalizer';

const TYPING_TIME = 10 * 1000;

export function loadConversations() {
  return {
    types: [
      ActionTypes.REQUEST_CONVERSATIONS,
      ActionTypes.RECEIVE_CONVERSATIONS,
      ActionTypes.FAILURE_CONVERSATIONS,
    ],
    callApi: api.getConversations(),
    getPagination: state => state.pagination.conversations,
    condition: state => {
      return !state.pagination.conversations.isLoading
              && (!state.pagination.conversations.ids
                || state.pagination.conversations.nextCursor);
    },
  };
}

export function loadConversation(conversationId) {
  return {
    types: [
      ActionTypes.REQUEST_CONVERSATION,
      ActionTypes.RECEIVE_CONVERSATION,
      ActionTypes.FAILURE_CONVERSATION,
    ],
    payload: { conversationId },
    callApi: api.getConversation(conversationId),
    condition: state => {
      const conversation = state.entities.conversations[conversationId];
      return !conversation || !conversation.members || !conversation.messages;
    },
  };
}

export function loadMessages(conversationId) {
  return {
    types: [
      ActionTypes.REQUEST_MESSAGES,
      ActionTypes.RECEIVE_MESSAGES,
      ActionTypes.FAILURE_MESSAGES,
    ],
    payload: { conversationId },
    callApi: api.getMessages(conversationId),
    getPagination: state => state.pagination.messagesByConversation[conversationId],
    condition: ({ pagination: { messagesByConversation } }) => {
      const messagePagination = messagesByConversation[conversationId];
      return !messagePagination
              || (!messagePagination.isLoading
                && (!messagePagination.ids || messagePagination.nextCursor));
    },
  };
}

export function sendTyping(conversationId) { // TODO: refactor
  return (dispatch, getState) => {
    const { userId, entities: { conversations } } = getState();
    const conversation = conversations[conversationId];
    if (contains(conversation.typingUserIds, userId)) {
      return;
    }
    dispatch({
      type: ActionTypes.SEND_TYPING,
      payload: { conversationId },
    });
    execute(api.sendTyping(conversationId));
  };
}

function stopTyping({ conversationId, userId }) {
  return {
    type: ActionTypes.STOP_TYPING,
    payload: { conversationId, userId },
  };
}

function startTyping({ conversationId, userId }) {
  return {
    type: ActionTypes.START_TYPING,
    payload: { conversationId, userId },
  };
}

export function receiveTyping({ conversationId, userId }) {
  return dispatch => {
    dispatch(startTyping({ conversationId, userId }));
    setTimeout(() => {
      // We would like to clear this timeout in `stopTyping`, but then we
      // would have to manage a (conversationId, userId) -> timeout collection.
      dispatch(stopTyping({ conversationId, userId }));
    }, TYPING_TIME);
  };
}

export function sendMessage({ conversationId, text }) {
  return (dispatch, getState) => {
    const { userId } = getState();
    dispatch(stopTyping({ conversationId, userId }));
    dispatch({
      type: ActionTypes.SEND_MESSAGE,
      payload: { conversationId, text },
    });
    execute(api.sendMessage({ conversationId, text }));
  };
}

export function receiveMessage({ conversationId, message }) {
  return dispatch => {
    dispatch(stopTyping({ conversationId, userId: message.author }));
    dispatch({
      type: ActionTypes.RECEIVE_MESSAGE,
      payload: { conversationId },
      // TODO: normalize somewhere else
      // TODO: better name
      response: normalize(camelizeKeys(message), api.message),
    });
  };
}
<|endoftext|>


<commit_before>$.fn.parallaxize = (function (global, $) {
    'use strict';

    function Parallax() {
        this.init.apply(this, Array.prototype.slice.call(arguments));
    }

    Parallax.prototype = {
        init: function (parent, settings) {
            this.initializeConfig(settings);
            this.config('element', parent);
            this.config('width', 1);
            this.config('height', 1);

            this.initializeDOM();
            this.initializeBindings();
        },

        initializeConfig: function (settings) {
            var config = {};
            settings = $.fn.extend({variation: 20}, settings);

            var getGetter = function (object) {
                return function (key, value) {
                    if (typeof key === 'undefined') {
                        return object;
                    }
                    if (typeof value === 'undefined') {
                        return object[key];
                    }
                    return object[key] = value;
                }
            };

            this.config = getGetter(config);
            this.setting = getGetter(settings);
        },

        initializeDOM: function () {
            var elem = this.config('element'),
                image = new Image(),
                image_elem = $(image),
                me = this;
            elem.prepend(image_elem);
            image_elem.hide();

            var synchronous = true;
            image.onload = function () {
                me.config('height', image_elem.height());
                me.config('width', image_elem.width());
                me.translate(false);

                if (synchronous) {
                    image_elem.show();
                } else {
                    image_elem.fadeIn(250);
                }
            };
            image.src = elem.data('background-image');
            setTimeout(function () {
                synchronous = false;
            }, 150);
            this.config('image', image_elem);

            elem.addClass('parallaxic-container');
            image_elem.addClass('parallaxic-image');

        },

        initializeBindings: function () {
            var me = this;
            $(global).scroll(function () {
                me.translate();
            });

            $(global).resize(function () {
                me.translate(false);
            });
        },

        translate: function (cache) {
            var transform = [
                this.resizeTranslation(cache),
                this.positionTranslation()
            ].join(' ');

            this.config('image').css({
                transform: transform,
                '-webkit-transform': transform,
                '-moz-transform': transform,
                '-o-transform': transform
            });
        },

        resizeTranslation: function (cached) {
            var scale;
            if (cached !== false) {
                scale = this.config('scale');
                if (scale) {
                    return 'scale(' + scale + ')';
                }
            }

            var variation = this.setting('variation'),
                elem = this.config('element'),
                image = this.config('image'),
                height = elem.height(),
                image_height = this.config('height'), image_width = this.config('width'),
                potential_height = height + variation * 2,
                new_width = elem.width(), new_height;

            new_height = (image_height / image_width) * new_width;

            if (new_height < potential_height) {
                new_height = potential_height;
                new_width = (image_width / image_height) * new_height;
            }

            scale = Math.ceil(new_height / image_height * 1000) / 1000;

            //if (Modernizr && !Modernizr.csstransforms) {
            //    image.css({
            //        width: new_width,
            //        height: new_height
            //    });
            //    return '';
            //}

            this.config('scale', scale);
            return 'scale(' + scale + ')';
        },

        positionTranslation: function () {

            var variation = this.setting('variation'),
                elem = this.config('element'),
                image = this.config('image'),
                height = elem.height(), width = elem.width(),
                image_height = this.config('height'), image_width = this.config('width'),
                top = (height - image_height) / 2,
                left = (width - image_width) / 2,
                scroll_percentage = this.getScrollPercentage();

            top += (variation * 2 * scroll_percentage) - variation;

            // First assume translate
            //var method_start = 'translate(', method_end = ')';
            //
            //if (Modernizr) {
            //    if (Modernizr.csstransforms3d) {
            //        method_start = 'translate3d(';
            //        method_end = ', 0px)';
            //    } else if (!Modernizr.csstransforms) {
            //        return '';
            //    }
            //}
            //return method_start + Math.round(left) + 'px, ' + Math.round(top) + 'px' + method_end;

            return ['translate3d(', Math.round(left), 'px, ', Math.round(top), 'px, ', '0px)'].join('');
        },

        getScrollPercentage: function () {
            var elem = this.config('element'),
                image = this.config('image'),
                main = $(global),
                min = elem.offset().top - main.height(),
                max = elem.offset().top + elem.height() - min,
                scroll = main.scrollTop() - min;

            return Math.max(0, Math.min(1, scroll / max));
        }

    };

    return function (settings) {
        this.data('parallax', new Parallax(this, settings))
    };

}(window, $));
<commit_msg>:bug: fix(browser): Adjust the parallax JS to respect scale when calculating left shift Scale was not being taken into account (since I assumed that scaling would adjust the images initial x/y coordinate, [this doesn't look to be the case](http://jsfiddle.net/rhj0kL8L/) as browsers keep the same bounding box and simply scale at the rendering level. To fix this, we simply adjust the offset by the inverse scale.<commit_after>$.fn.parallaxize = (function (global, $) {
    'use strict';

    function Parallax() {
        this.init.apply(this, Array.prototype.slice.call(arguments));
    }

    Parallax.prototype = {
        init: function (parent, settings) {
            this.initializeConfig(settings);
            this.config('element', parent);
            this.config('width', 1);
            this.config('height', 1);

            this.initializeDOM();
            this.initializeBindings();
        },

        initializeConfig: function (settings) {
            var config = {};
            settings = $.fn.extend({variation: 20}, settings);

            var getGetter = function (object) {
                return function (key, value) {
                    if (typeof key === 'undefined') {
                        return object;
                    }
                    if (typeof value === 'undefined') {
                        return object[key];
                    }
                    return object[key] = value;
                }
            };

            this.config = getGetter(config);
            this.setting = getGetter(settings);
        },

        initializeDOM: function () {
            var elem = this.config('element'),
                image = new Image(),
                image_elem = $(image),
                me = this;
            elem.prepend(image_elem);
            image_elem.hide();

            var synchronous = true;
            image.onload = function () {
                me.config('height', image_elem.height());
                me.config('width', image_elem.width());
                me.translate(false);

                if (synchronous) {
                    image_elem.show();
                } else {
                    image_elem.fadeIn(250);
                }
            };
            image.src = elem.data('background-image');
            setTimeout(function () {
                synchronous = false;
            }, 150);
            this.config('image', image_elem);

            elem.addClass('parallaxic-container');
            image_elem.addClass('parallaxic-image');

        },

        initializeBindings: function () {
            var me = this;
            $(global).scroll(function () {
                me.translate();
            });

            $(global).resize(function () {
                me.translate(false);
            });
        },

        translate: function (cache) {
            var transform = [
                this.resizeTranslation(cache),
                this.positionTranslation()
            ].join(' ');

            this.config('image').css({
                transform: transform,
                '-webkit-transform': transform,
                '-moz-transform': transform,
                '-o-transform': transform
            });
        },

        resizeTranslation: function (cached) {
            var scale;
            if (cached !== false) {
                scale = this.config('scale');
                if (scale) {
                    return 'scale(' + scale + ')';
                }
            }

            var variation = this.setting('variation'),
                elem = this.config('element'),
                image = this.config('image'),
                height = elem.height(),
                image_height = this.config('height'), image_width = this.config('width'),
                potential_height = height + variation * 2,
                new_width = elem.width(), new_height;

            new_height = (image_height / image_width) * new_width;

            if (new_height < potential_height) {
                new_height = potential_height;
                new_width = (image_width / image_height) * new_height;
            }

            scale = Math.ceil(new_height / image_height * 1000) / 1000;

            //if (Modernizr && !Modernizr.csstransforms) {
            //    image.css({
            //        width: new_width,
            //        height: new_height
            //    });
            //    return '';
            //}

            this.config('scale', scale);
            return 'scale(' + scale + ')';
        },

        positionTranslation: function () {

            var variation = this.setting('variation'),
                scale = this.config('scale'),
                elem = this.config('element'),
                image = this.config('image'),
                height = elem.height(), width = elem.width(),
                image_height = this.config('height'), image_width = this.config('width'),
                top = (height - image_height) / 2,
                left = (width - image_width) / 2,
                scroll_percentage = this.getScrollPercentage();

            top += (variation * 2 * scroll_percentage) - variation;

            // First assume translate
            //var method_start = 'translate(', method_end = ')';
            //
            //if (Modernizr) {
            //    if (Modernizr.csstransforms3d) {
            //        method_start = 'translate3d(';
            //        method_end = ', 0px)';
            //    } else if (!Modernizr.csstransforms) {
            //        return '';
            //    }
            //}
            //return method_start + Math.round(left) + 'px, ' + Math.round(top) + 'px' + method_end;

            return ['translate3d(', Math.round(left / scale), 'px, ', Math.round(top), 'px, ', '0px)'].join('');
        },

        getScrollPercentage: function () {
            var elem = this.config('element'),
                image = this.config('image'),
                main = $(global),
                min = elem.offset().top - main.height(),
                max = elem.offset().top + elem.height() - min,
                scroll = main.scrollTop() - min;

            return Math.max(0, Math.min(1, scroll / max));
        }

    };

    return function (settings) {
        this.data('parallax', new Parallax(this, settings))
    };

}(window, $));
<|endoftext|>


<commit_before>import React, { useState } from 'react';
import PropTypes from 'prop-types';

import { generateUID } from '../../helpers/component';
import useInput from '../input/useInput';
import ErrorZone from '../text/ErrorZone';

const Select = ({ options, error, size = 1, className = '', multiple = false, ...rest }) => {
    const { handlers, statusClasses, status } = useInput({ ...rest });
    const [uid] = useState(generateUID('select'));

    return (
        <>
            <select
                className={`pm-field w100 ${className} ${statusClasses}`}
                size={size}
                multiple={multiple}
                {...rest}
                {...handlers}
            >
                {options.map(({ text, ...rest }, index) => (
                    <option key={index.toString()} {...rest}>
                        {text}
                    </option>
                ))}
            </select>
            <ErrorZone id={uid}>{error && status.isDirty ? error : ''}</ErrorZone>
        </>
    );
};

Select.propTypes = {
    error: PropTypes.string,
    disabled: PropTypes.bool,
    size: PropTypes.number.isRequired,
    onChange: PropTypes.func,
    onBlur: PropTypes.func,
    onFocus: PropTypes.func,
    options: PropTypes.arrayOf(PropTypes.object).isRequired,
    multiple: PropTypes.bool,
    className: PropTypes.string
};

export default Select;
<commit_msg>Hotfix - remove isRequired on Select<commit_after>import React, { useState } from 'react';
import PropTypes from 'prop-types';

import { generateUID } from '../../helpers/component';
import useInput from '../input/useInput';
import ErrorZone from '../text/ErrorZone';

const Select = ({ options, error, size = 1, className = '', multiple = false, ...rest }) => {
    const { handlers, statusClasses, status } = useInput({ ...rest });
    const [uid] = useState(generateUID('select'));

    return (
        <>
            <select
                className={`pm-field w100 ${className} ${statusClasses}`}
                size={size}
                multiple={multiple}
                {...rest}
                {...handlers}
            >
                {options.map(({ text, ...rest }, index) => (
                    <option key={index.toString()} {...rest}>
                        {text}
                    </option>
                ))}
            </select>
            <ErrorZone id={uid}>{error && status.isDirty ? error : ''}</ErrorZone>
        </>
    );
};

Select.propTypes = {
    error: PropTypes.string,
    disabled: PropTypes.bool,
    size: PropTypes.number,
    onChange: PropTypes.func,
    onBlur: PropTypes.func,
    onFocus: PropTypes.func,
    options: PropTypes.arrayOf(PropTypes.object),
    multiple: PropTypes.bool,
    className: PropTypes.string
};

export default Select;
<|endoftext|>


<commit_before>/** @license MIT License (c) copyright 2010-2017 original author or authors */

import RelativeSink from '../sink/RelativeSink'
import { schedulerRelativeTo } from '@most/scheduler'

export const runWithLocalTime = (origin, stream, sink, scheduler) =>
  stream.run(new RelativeSink(origin, sink), schedulerRelativeTo(origin, scheduler))
<commit_msg>feat(runWithLocalTime): avoid nesting RelativeSinks by accumulating offsets instead (#85)<commit_after>/** @license MIT License (c) copyright 2010-2017 original author or authors */

import RelativeSink from '../sink/RelativeSink'
import { schedulerRelativeTo } from '@most/scheduler'

// Run a stream with its own localized clock
// This transforms time from the provided scheduler's clock to a stream-local
// clock (which starts at 0), and then *back* to the scheduler's clock before
// propagating events to sink.  IOW, stream.run will see local times, and sink
// will see scheduler times.
export const runWithLocalTime = (origin, stream, sink, scheduler) =>
  stream.run(relativeSink(origin, sink), schedulerRelativeTo(origin, scheduler))

// Accumulate offsets instead of nesting RelativeSinks, which can happen
// with higher-order stream and combinators like continueWith when they're
// applied recursively.
export const relativeSink = (origin, sink) =>
  sink instanceof RelativeSink
    ? new RelativeSink(origin + sink.offset, sink.sink)
    : new RelativeSink(origin, sink)
<|endoftext|>


<commit_before>(function($, undefined) {
  var form_ext;
  $.form_ext  = form_ext = {
    ajax: function(options) {
      return $.ajax(options);
    },
    handleRemote: function(element) {
      var method = element.attr('method');
      var url = element.attr('action');
      var data = element.serializeArray();
      var options = {
        type: method || 'GET',
        data: data,
        success: function(data, status, xhr) {
          element.trigger("ajax:success", [data, status, xhr]);
        },
        complete: function(xhr, status) {
          element.trigger("ajax:complete", [xhr, status]);
        },
        error: function(xhr, status, error) {
          element.trigger("ajax:error", [xhr, status, error]);
        }
      }
      if(url) { options.url = url; }
      return form_ext.ajax(options)
    },
    CSRFProtection: function(xhr) {
      var token = $.cookie('csrftoken');
      if (token) xhr.setRequestHeader('X-CSRFToken', token);
    },
  }
  $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { form_ext.CSRFProtection(xhr); }});
  $(document).delegate('form', 'submit', function(e) {
    var form = $(this),
    remote = form.data("remote") !== undefined;

    if(remote) {
      form_ext.handleRemote(form);
      return false;
    }

  });
})(jQuery);
<commit_msg>Force dataType to 'text json'<commit_after>(function($, undefined) {
  var form_ext;
  $.form_ext  = form_ext = {
    ajax: function(options) {
      return $.ajax(options);
    },
    handleRemote: function(element) {
      var method = element.attr('method');
      var url = element.attr('action');
      var data = element.serializeArray();
      var options = {
        type: method || 'GET',
        data: data,
        dataType: 'text json',
        success: function(data, status, xhr) {
          element.trigger("ajax:success", [data, status, xhr]);
        },
        complete: function(xhr, status) {
          element.trigger("ajax:complete", [xhr, status]);
        },
        error: function(xhr, status, error) {
          element.trigger("ajax:error", [xhr, status, error]);
        }
      }
      if(url) { options.url = url; }
      return form_ext.ajax(options)
    },
    CSRFProtection: function(xhr) {
      var token = $.cookie('csrftoken');
      if (token) xhr.setRequestHeader('X-CSRFToken', token);
    },
  }
  $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { form_ext.CSRFProtection(xhr); }});
  $(document).delegate('form', 'submit', function(e) {
    var form = $(this),
    remote = form.data("remote") !== undefined;

    if(remote) {
      form_ext.handleRemote(form);
      return false;
    }

  });
})(jQuery);
<|endoftext|>


<commit_before>/*globals psc jQuery */
psc.namespace("schedule.preview");

psc.schedule.preview.Parameters = (function ($) {
  var query = null;
  var pending = false;

  function findPairKey(pair) {
    var found;
    $.each(query.get('segment'), function (prop, val) {
      if (val == pair.segment && query.get('start_date[' + prop + ']') === pair.start_date) {
        found = prop;
        return false;
      }
    });
    return found;
  }

  function nextKey() {
    var intKeys = [];
    $.each(query.get('segment'), function (key, _) {
      if (/^\d+$/.match(key)) {
        intKeys.push(parseInt(key));
      }
    });
    intKeys.sort();
    return (intKeys.pop() || 0) + 1;
  }

  return {
    init: function (hashParams) {
      query = $.query.load("#" + hashParams);
      pending = false;
      $('#schedule').bind('schedule-ready', function () {
        pending = false;
      });
    },

    size: function () {
      var ct = 0;
      $.each(query.get('segment'), function (prop, value) { ct += 1; });
      return ct;
    },

    clear: function () {
      query.EMPTY();
      pending = true;
    },

    add: function (newPair) {
      var newKey = nextKey();
      query.SET('segment[' + newKey + ']', newPair.segment);
      query.SET('start_date[' + newKey + ']', newPair.start_date);
      pending = true;
    },

    remove: function(pair) {
      var key = findPairKey(pair);
      if (key) {
        query.REMOVE('segment[' + key + ']');
        query.REMOVE('start_date[' + key + ']');
      }
      pending = true;
    },

    pending: function() {
      return pending;
    },

    requestedSegments: function () {
      var segments = [];
      $.each(query.get('segment'), function (prop, val) {
        segments.push({ segment: val, start_date: query.get('start_date')[prop] })
      });
      segments.sort(function (a, b) {
        var a_date = a.start_date;
        var b_date = b.start_date;
        if (a_date == b_date) {
          return 0;
        } else if (a_date < b_date) {
          return -1;
        } else {
          return 1;
        }
      });
      return segments;
    },

    toQueryString: function () {
      return query.toString();
    }
  };
}(jQuery));
<commit_msg>Update the if condition before removal of the segment. Condition 'if(key)' was failing when key value is 0 and segment with index value 0 never deleted from query. #889, 1st scenario.<commit_after>/*globals psc jQuery */
psc.namespace("schedule.preview");

psc.schedule.preview.Parameters = (function ($) {
  var query = null;
  var pending = false;

  function findPairKey(pair) {
    var found = null;
    $.each(query.get('segment'), function (prop, val) {
      if (val == pair.segment && query.get('start_date[' + prop + ']') === pair.start_date) {
        found = prop;
        return false;
      }
    });
    return found;
  }

  function nextKey() {
    var intKeys = [];
    $.each(query.get('segment'), function (key, _) {
      if (/^\d+$/.match(key)) {
        intKeys.push(parseInt(key));
      }
    });
    intKeys.sort();
    return (intKeys.pop() || 0) + 1;
  }

  return {
    init: function (hashParams) {
      query = $.query.load("#" + hashParams);
      pending = false;
      $('#schedule').bind('schedule-ready', function () {
        pending = false;
      });
    },

    size: function () {
      var ct = 0;
      $.each(query.get('segment'), function (prop, value) { ct += 1; });
      return ct;
    },

    clear: function () {
      query.EMPTY();
      pending = true;
    },

    add: function (newPair) {
      var newKey = nextKey();
      query.SET('segment[' + newKey + ']', newPair.segment);
      query.SET('start_date[' + newKey + ']', newPair.start_date);
      pending = true;
    },

    remove: function(pair) {
      var key = findPairKey(pair);
      if (key != null) {
        query.REMOVE('segment[' + key + ']');
        query.REMOVE('start_date[' + key + ']');
      }
      pending = true;
    },

    pending: function() {
      return pending;
    },

    requestedSegments: function () {
      var segments = [];
      $.each(query.get('segment'), function (prop, val) {
        segments.push({ segment: val, start_date: query.get('start_date')[prop] })
      });
      segments.sort(function (a, b) {
        var a_date = a.start_date;
        var b_date = b.start_date;
        if (a_date == b_date) {
          return 0;
        } else if (a_date < b_date) {
          return -1;
        } else {
          return 1;
        }
      });
      return segments;
    },

    toQueryString: function () {
      return query.toString();
    }
  };
}(jQuery));
<|endoftext|>