<commit_before>import React from 'react';
import Status from './status';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { ScrollContainer } from 'react-router-scroll';
import PropTypes from 'prop-types';
import StatusContainer from '../containers/status_container';
import LoadMore from './load_more';
import ImmutablePureComponent from 'react-immutable-pure-component';

class StatusList extends ImmutablePureComponent {

  static propTypes = {
    scrollKey: PropTypes.string.isRequired,
    statusIds: ImmutablePropTypes.list.isRequired,
    onScrollToBottom: PropTypes.func,
    onScrollToTop: PropTypes.func,
    onScroll: PropTypes.func,
    shouldUpdateScroll: PropTypes.func,
    isLoading: PropTypes.bool,
    isUnread: PropTypes.bool,
    hasMore: PropTypes.bool,
    prepend: PropTypes.node,
    emptyMessage: PropTypes.node,
  };

  static defaultProps = {
    trackScroll: true,
  };

  handleScroll = (e) => {
    const { scrollTop, scrollHeight, clientHeight } = e.target;
    const offset = scrollHeight - scrollTop - clientHeight;
    this._oldScrollPosition = scrollHeight - scrollTop;

    if (250 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
      this.props.onScrollToBottom();
    } else if (scrollTop < 100 && this.props.onScrollToTop) {
      this.props.onScrollToTop();
    } else if (this.props.onScroll) {
      this.props.onScroll();
    }
  }

  componentDidMount () {
    this.attachScrollListener();
  }

  componentDidUpdate (prevProps) {
    if ((prevProps.statusIds.size < this.props.statusIds.size && prevProps.statusIds.first() !== this.props.statusIds.first() && !!this._oldScrollPosition) && this.node.scrollTop > 0) {
      this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition;
    }
  }

  componentWillUnmount () {
    this.detachScrollListener();
  }

  attachScrollListener () {
    this.node.addEventListener('scroll', this.handleScroll);
  }

  detachScrollListener () {
    this.node.removeEventListener('scroll', this.handleScroll);
  }

  setRef = (c) => {
    this.node = c;
  }

  handleLoadMore = (e) => {
    e.preventDefault();
    this.props.onScrollToBottom();
  }

  render () {
    const { statusIds, onScrollToBottom, scrollKey, shouldUpdateScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props;

    let loadMore       = '';
    let scrollableArea = '';
    let unread         = '';

    if (!isLoading && statusIds.size > 0 && hasMore) {
      loadMore = <LoadMore onClick={this.handleLoadMore} />;
    }

    if (isUnread) {
      unread =
;
    }

    if (isLoading || statusIds.size > 0 || !emptyMessage) {
      scrollableArea = (

          {unread}

            {prepend}

            {statusIds.map((statusId) => {
              return <StatusContainer key={statusId} id={statusId} />;
            })}

            {loadMore}

      );
    } else {
      scrollableArea = (

          {emptyMessage}

      );
    }

    return (
      <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
        {scrollableArea}
      </ScrollContainer>
    );
  }

}

export default StatusList;
<commit_msg>Refactoring. Removed useless import. (#3254)<commit_after>import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { ScrollContainer } from 'react-router-scroll';
import PropTypes from 'prop-types';
import StatusContainer from '../containers/status_container';
import LoadMore from './load_more';
import ImmutablePureComponent from 'react-immutable-pure-component';

class StatusList extends ImmutablePureComponent {

  static propTypes = {
    scrollKey: PropTypes.string.isRequired,
    statusIds: ImmutablePropTypes.list.isRequired,
    onScrollToBottom: PropTypes.func,
    onScrollToTop: PropTypes.func,
    onScroll: PropTypes.func,
    shouldUpdateScroll: PropTypes.func,
    isLoading: PropTypes.bool,
    isUnread: PropTypes.bool,
    hasMore: PropTypes.bool,
    prepend: PropTypes.node,
    emptyMessage: PropTypes.node,
  };

  static defaultProps = {
    trackScroll: true,
  };

  handleScroll = (e) => {
    const { scrollTop, scrollHeight, clientHeight } = e.target;
    const offset = scrollHeight - scrollTop - clientHeight;
    this._oldScrollPosition = scrollHeight - scrollTop;

    if (250 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
      this.props.onScrollToBottom();
    } else if (scrollTop < 100 && this.props.onScrollToTop) {
      this.props.onScrollToTop();
    } else if (this.props.onScroll) {
      this.props.onScroll();
    }
  }

  componentDidMount () {
    this.attachScrollListener();
  }

  componentDidUpdate (prevProps) {
    if ((prevProps.statusIds.size < this.props.statusIds.size && prevProps.statusIds.first() !== this.props.statusIds.first() && !!this._oldScrollPosition) && this.node.scrollTop > 0) {
      this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition;
    }
  }

  componentWillUnmount () {
    this.detachScrollListener();
  }

  attachScrollListener () {
    this.node.addEventListener('scroll', this.handleScroll);
  }

  detachScrollListener () {
    this.node.removeEventListener('scroll', this.handleScroll);
  }

  setRef = (c) => {
    this.node = c;
  }

  handleLoadMore = (e) => {
    e.preventDefault();
    this.props.onScrollToBottom();
  }

  render () {
    const { statusIds, onScrollToBottom, scrollKey, shouldUpdateScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props;

    let loadMore       = '';
    let scrollableArea = '';
    let unread         = '';

    if (!isLoading && statusIds.size > 0 && hasMore) {
      loadMore = <LoadMore onClick={this.handleLoadMore} />;
    }

    if (isUnread) {
      unread =
;
    }

    if (isLoading || statusIds.size > 0 || !emptyMessage) {
      scrollableArea = (

          {unread}

            {prepend}

            {statusIds.map((statusId) => {
              return <StatusContainer key={statusId} id={statusId} />;
            })}

            {loadMore}

      );
    } else {
      scrollableArea = (

          {emptyMessage}

      );
    }

    return (
      <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
        {scrollableArea}
      </ScrollContainer>
    );
  }

}

export default StatusList;
<|endoftext|>


<commit_before>function DBCore(sheet, headerRowPosition, startColumn, width) {
    var t = this;
    t._sheet = sheet;
    t._startColumn = startColumn || 1;
    t._width = width || null;
    t._headerRowPos = headerRowPosition;
    t._header = Lib.util.getRange(
        t._sheet,
        [t._headerRowPos, t._startColumn], [1, t._width]
    ).getValues()[0];

    t._initActualRange();
}

DBCore.prototype.getBaseRow = function () {
    return this._headerRowPos + 1;
};

DBCore.prototype._initActualRange = function () {
    var t = this;
    t._range = Lib.util.getRange(
        t._sheet,
        [t._headerRowPos + 1, t._startColumn], [null, t._width]
    );
    t._height = t._range.getHeight();
    t._width = t._range.getWidth(); //actual width if not specified
};

DBCore.prototype._checkConstraints = function (data) {
    var t = this;
    if (data[0].length !== t._width) throw Error("Data width != range width");
};

DBCore.prototype._subset = function (startOffset, rowNumber) {
    var t = this;
    if (!startOffset && !rowNumber) return t._range; //optimization
    return t._range.offset(startOffset || 0, 0, rowNumber || t._height, t._width);
};

DBCore.prototype.getRows = function (optStartRow, optRowsNumber) {
    var t = this;
    var res = t._range.getValues();
    if (optStartRow || optRowsNumber){
        optStartIndex = (optStartRow || 1) - 1;
        optRowsNumber = optRowsNumber || t._height;
        res = res.slice(optStartIndex, optStartIndex + optRowsNumber);
    }
    return res;
};

DBCore.prototype.getHeader = function () {
    return this._header;
};

DBCore.prototype.updateRows = function (startOffset, data) {
    var t = this;
    t._checkConstraints(data);
    t._subset(startOffset, data.length).setValues(data);
    return true;
};
/**
 * Rewrites existing data with new
 * @param data
 * @returns {boolean}
 */
DBCore.prototype.rewriteAll = function (data) {
    var t = this;
    t._checkConstraints(data);
    t._range.clearContent();
    return t._appendRows(data);
};

/**
 * Appends to existing records
 * @param data
 * @returns {boolean}
 */
DBCore.prototype.appendRows = function (data) {
    var t = this;
    t._checkConstraints(data);
    return t._appendRows(data);
};

DBCore.prototype._appendRows = function (data) {
    var t = this;
    var res = Lib.util.writeRows(t._sheet, data, t._startColumn);
    if (res.inserted){
        //need to update original range, because new column where inserted at the bottom
        t._initActualRange();
    }
    //SpreadsheetApp.flush();
    return true
};

/**
 *
 * @param criteria - {index: value, index2: value2...}
 * @param startOffset
 * @param limit
 */
/*DBManager.prototype.selectRows = function (criteria, startOffset, limit) {
    var t = this,
        rows = [],
        matches = 0;

    limit = limit || 1e+20;
    startOffset = startOffset || 0;

    var values = t._range.getValues();
    var criteriaNumber = Object.keys(criteria).length;

    for (var i = 0; i < values.length; i++) {
        if (criteriaNumber) {
            var matched = true;
            for (var c in criteria) {
                if (values[i][c].toString().indexOf(criteria[c]) === -1) {
                    matched = false;
                    break;
                }
            }
            if (! matched) continue;
        }
        matches++;
        if (matches >= startOffset) {
            rows.push(values[i]);
            if (rows.length >= limit) break;
        }
    }
    return rows;
}*/

Lib.tool.DBCore = DBCore;<commit_msg>improvements<commit_after>function DBCore(sheet, headerRowPosition, startColumn, width) {
    var t = this;
    t._sheet = sheet;
    t._startColumn = startColumn || 1;
    t._width = width || null;
    t._headerRowPos = headerRowPosition;
    t._header = Lib.util.getRange(
        t._sheet,
        [t._headerRowPos, t._startColumn], [1, t._width]
    ).getValues()[0];

    t._initActualRange();
}

DBCore.prototype.getBaseRow = function () {
    return this._headerRowPos + 1;
};

DBCore.prototype._initActualRange = function () {
    var t = this;
    t._range = Lib.util.getRange(
        t._sheet,
        [t._headerRowPos + 1, t._startColumn], [null, t._width]
    );
    t._height = t._range.getHeight();
    t._width = t._range.getWidth(); //actual width if not specified
};

DBCore.prototype._checkConstraints = function (data) {
    var t = this;
    if (data[0].length !== t._width) throw Error("Data width != range width");
};

DBCore.prototype._subset = function (startOffset, rowNumber) {
    var t = this;
    if (!startOffset && !rowNumber) return t._range; //optimization
    return t._range.offset(startOffset || 0, 0, rowNumber || t._height, t._width);
};

DBCore.prototype.get = function (optStartRowPos, optRowsNumber) {
    var t = this;
    var res = t._range.getValues();
    if (optStartRowPos || optRowsNumber){
        optStartIndex = (optStartRowPos || 1) - 1;
        optRowsNumber = optRowsNumber || t._height;
        res = res.slice(optStartIndex, optStartIndex + optRowsNumber);
    }
    return res;
};

DBCore.prototype.getOne = function (optRowPosition, optRowsNumber) {
    return this.get(optRowPosition, 1);
};

DBCore.prototype.getHeader = function () {
    return this._header;
};

DBCore.prototype.update = function (startRowNumber, data) {
    var t = this;
    t._checkConstraints(data);
    t._subset(startRowNumber - 1, data.length).setValues(data);
    return true;
};

DBCore.prototype.updateOne = function (rowNumber, row) {
    "use strict";
    return this.update(rowNumber, [row])
};

/**
 * Rewrites existing data with new
 * @param data
 * @returns {boolean}
 */
DBCore.prototype.rewriteAll = function (data) {
    var t = this;
    t._checkConstraints(data);
    t._range.clearContent();
    return t._appendRows(data);
};

/**
 * Appends to existing records
 * @param row
 * @returns {boolean}
 */
DBCore.prototype.appendOne = function (row) {
    return this.append([row]);
};

DBCore.prototype.append = function (data) {
    var t = this;
    t._checkConstraints(data);
    return t._appendRows(data);
};

DBCore.prototype._appendRows = function (data) {
    var t = this;
    var res = Lib.util.writeRows(t._sheet, data, t._startColumn);
    if (res.inserted){
        //need to update original range, because new column where inserted at the bottom
        t._initActualRange();
    }
    //SpreadsheetApp.flush();
    return true
};

/**
 *
 * @param criteria - {index: value, index2: value2...}
 * @param startOffset
 * @param limit
 */
/*DBManager.prototype.selectRows = function (criteria, startOffset, limit) {
    var t = this,
        rows = [],
        matches = 0;

    limit = limit || 1e+20;
    startOffset = startOffset || 0;

    var values = t._range.getValues();
    var criteriaNumber = Object.keys(criteria).length;

    for (var i = 0; i < values.length; i++) {
        if (criteriaNumber) {
            var matched = true;
            for (var c in criteria) {
                if (values[i][c].toString().indexOf(criteria[c]) === -1) {
                    matched = false;
                    break;
                }
            }
            if (! matched) continue;
        }
        matches++;
        if (matches >= startOffset) {
            rows.push(values[i]);
            if (rows.length >= limit) break;
        }
    }
    return rows;
}*/

Lib.tool.DBCore = DBCore;<|endoftext|>


<commit_before>

import React, { Component } from "react";
import PropTypes from "prop-types";
import { line as d3Line } from "d3-shape";

import GenericChartComponent from "../GenericChartComponent";
import { getAxisCanvas, getMouseCanvas } from "../GenericComponent";

import {
	isDefined,
	getClosestItemIndexes,
	strokeDashTypes,
	getStrokeDasharray,
	hexToRGBA,
} from "../utils";

class LineSeries extends Component {
	constructor(props) {
		super(props);
		this.renderSVG = this.renderSVG.bind(this);
		this.drawOnCanvas = this.drawOnCanvas.bind(this);
		this.isHover = this.isHover.bind(this);
	}
	isHover(moreProps) {
		// console.log("HERE")
		const { highlightOnHover, yAccessor, hoverTolerance } = this.props;

		if (!highlightOnHover) return false;

		const { mouseXY, currentItem, xScale, plotData } = moreProps;
		const { chartConfig: { yScale, origin } } = moreProps;

		const { xAccessor } = moreProps;

		const [x, y] = mouseXY;
		const radius = hoverTolerance;

		const { left, right } = getClosestItemIndexes(plotData, xScale.invert(x), xAccessor);
		if (left === right) {
			const cy = yScale(yAccessor(currentItem)) + origin[1];
			const cx = xScale(xAccessor(currentItem)) + origin[0];

			const hovering1 = Math.pow(x - cx, 2) + Math.pow(y - cy, 2) < Math.pow(radius, 2);

			return hovering1;
		} else {
			const l = plotData[left];
			const r = plotData[right];
			const x1 = xScale(xAccessor(l)) + origin[0];
			const y1 = yScale(yAccessor(l)) + origin[1];
			const x2 = xScale(xAccessor(r)) + origin[0];
			const y2 = yScale(yAccessor(r)) + origin[1];

			// y = m * x + b
			const m /* slope */ = (y2 - y1) / (x2 - x1);
			const b /* y intercept */ = -1 * m * x1 + y1;

			const desiredY = Math.round(m * x + b);

			const hovering2 = y >= desiredY - radius && y <= desiredY + radius;

			return hovering2;
		}
	}
	drawOnCanvas(ctx, moreProps) {
		const {
			yAccessor,
			stroke,
			strokeOpacity,
			strokeWidth,
			hoverStrokeWidth,
			defined,
			strokeDasharray,
			interpolation,
			canvasClip,
		} = this.props;

		const { connectNulls } = this.props;

		const { xAccessor } = moreProps;
		const { xScale, chartConfig: { yScale }, plotData, hovering } = moreProps;

		if (canvasClip) {
			ctx.save();
			canvasClip(ctx, moreProps);
		}

		ctx.lineWidth = hovering ? hoverStrokeWidth : strokeWidth;

		ctx.strokeStyle = hexToRGBA(stroke, strokeOpacity);
		ctx.setLineDash(getStrokeDasharray(strokeDasharray).split(","));

		const dataSeries = d3Line()
			.x(d => Math.round(xScale(xAccessor(d))))
			.y(d => Math.round(yScale(yAccessor(d))));

		if (isDefined(interpolation)) {
			dataSeries.curve(interpolation);
		}
		if (!connectNulls) {
			dataSeries.defined(d => defined(yAccessor(d)));
		}

		ctx.beginPath();
		dataSeries.context(ctx)(plotData);
		ctx.stroke();

		if (canvasClip) {
			ctx.restore();
		}
	}
	renderSVG(moreProps) {
		const { yAccessor, stroke, strokeOpacity, strokeWidth, hoverStrokeWidth, defined, strokeDasharray } = this.props;
		const { connectNulls } = this.props;
		const { interpolation, style } = this.props;
		const { xAccessor, chartConfig } = moreProps;

		const { xScale, plotData, hovering } = moreProps;

		const { yScale } = chartConfig;
		const dataSeries = d3Line()
			.x(d => Math.round(xScale(xAccessor(d))))
			.y(d => Math.round(yScale(yAccessor(d))));

		if (isDefined(interpolation)) {
			dataSeries.curve(interpolation);
		}
		if (!connectNulls) {
			dataSeries.defined(d => defined(yAccessor(d)));
		}
		const d = dataSeries(plotData);

		const { fill, className } = this.props;

		return (
			<path
				style={style}
				className={`${className} ${stroke ? "" : " line-stroke"}`}
				d={d}
				stroke={stroke}
				strokeOpacity={strokeOpacity}
				strokeWidth={hovering ? hoverStrokeWidth : strokeWidth}
				strokeDasharray={getStrokeDasharray(strokeDasharray)}
				fill={fill}
			/>
		);
	}
	render() {
		const { highlightOnHover } = this.props;
		const hoverProps = highlightOnHover
			? {
				isHover: this.isHover,
				drawOn: ["mousemove", "pan"],
				canvasToDraw: getMouseCanvas
			}
			: {
				drawOn: ["pan"],
				canvasToDraw: getAxisCanvas
			};

		return <GenericChartComponent
			svgDraw={this.renderSVG}

			canvasDraw={this.drawOnCanvas}

			onClickWhenHover={this.props.onClick}
			onDoubleClickWhenHover={this.props.onDoubleClick}
			onContextMenuWhenHover={this.props.onContextMenu}
			{...hoverProps}
		/>;
	}
}

/*
function segment(points, ctx) {
	ctx.beginPath();

	const [x, y] = first(points);
	ctx.moveTo(x, y);
	for (let i = 1; i < points.length; i++) {
		const [x1, y1] = points[i];
		ctx.lineTo(x1, y1);
	}

	ctx.stroke();
}
*/

LineSeries.propTypes = {
	className: PropTypes.string,
	strokeWidth: PropTypes.number,
	strokeOpacity: PropTypes.number,
	stroke: PropTypes.string,
	hoverStrokeWidth: PropTypes.number,
	fill: PropTypes.string,
	defined: PropTypes.func,
	hoverTolerance: PropTypes.number,
	strokeDasharray: PropTypes.oneOf(strokeDashTypes),
	highlightOnHover: PropTypes.bool,
	onClick: PropTypes.func,
	onDoubleClick: PropTypes.func,
	onContextMenu: PropTypes.func,
	yAccessor: PropTypes.func,
	connectNulls: PropTypes.bool,
	interpolation: PropTypes.func,
	canvasClip: PropTypes.func,
	style: PropTypes.object,
};

LineSeries.defaultProps = {
	className: "line ",
	strokeWidth: 1,
	strokeOpacity: 1,
	hoverStrokeWidth: 4,
	fill: "none",
	stroke: "#4682B4",
	strokeDasharray: "Solid",
	defined: d => !isNaN(d),
	hoverTolerance: 6,
	highlightOnHover: false,
	connectNulls: false,
	onClick: function(e) { console.log("Click", e); },
	onDoubleClick: function(e) { console.log("Double Click", e); },
	onContextMenu: function(e) { console.log("Right Click", e); },
};

export default LineSeries;
<commit_msg>add hover/unhover props on LineSeries<commit_after>

import React, { Component } from "react";
import PropTypes from "prop-types";
import { line as d3Line } from "d3-shape";

import GenericChartComponent from "../GenericChartComponent";
import { getAxisCanvas, getMouseCanvas } from "../GenericComponent";

import {
	isDefined,
	getClosestItemIndexes,
	strokeDashTypes,
	getStrokeDasharray,
	hexToRGBA,
} from "../utils";

class LineSeries extends Component {
	constructor(props) {
		super(props);
		this.renderSVG = this.renderSVG.bind(this);
		this.drawOnCanvas = this.drawOnCanvas.bind(this);
		this.isHover = this.isHover.bind(this);
	}
	isHover(moreProps) {
		// console.log("HERE")
		const { highlightOnHover, yAccessor, hoverTolerance } = this.props;

		if (!highlightOnHover) return false;

		const { mouseXY, currentItem, xScale, plotData } = moreProps;
		const { chartConfig: { yScale, origin } } = moreProps;

		const { xAccessor } = moreProps;

		const [x, y] = mouseXY;
		const radius = hoverTolerance;

		const { left, right } = getClosestItemIndexes(plotData, xScale.invert(x), xAccessor);
		if (left === right) {
			const cy = yScale(yAccessor(currentItem)) + origin[1];
			const cx = xScale(xAccessor(currentItem)) + origin[0];

			const hovering1 = Math.pow(x - cx, 2) + Math.pow(y - cy, 2) < Math.pow(radius, 2);

			return hovering1;
		} else {
			const l = plotData[left];
			const r = plotData[right];
			const x1 = xScale(xAccessor(l)) + origin[0];
			const y1 = yScale(yAccessor(l)) + origin[1];
			const x2 = xScale(xAccessor(r)) + origin[0];
			const y2 = yScale(yAccessor(r)) + origin[1];

			// y = m * x + b
			const m /* slope */ = (y2 - y1) / (x2 - x1);
			const b /* y intercept */ = -1 * m * x1 + y1;

			const desiredY = Math.round(m * x + b);

			const hovering2 = y >= desiredY - radius && y <= desiredY + radius;

			return hovering2;
		}
	}
	drawOnCanvas(ctx, moreProps) {
		const {
			yAccessor,
			stroke,
			strokeOpacity,
			strokeWidth,
			hoverStrokeWidth,
			defined,
			strokeDasharray,
			interpolation,
			canvasClip,
		} = this.props;

		const { connectNulls } = this.props;

		const { xAccessor } = moreProps;
		const { xScale, chartConfig: { yScale }, plotData, hovering } = moreProps;

		if (canvasClip) {
			ctx.save();
			canvasClip(ctx, moreProps);
		}

		ctx.lineWidth = hovering ? hoverStrokeWidth : strokeWidth;

		ctx.strokeStyle = hexToRGBA(stroke, strokeOpacity);
		ctx.setLineDash(getStrokeDasharray(strokeDasharray).split(","));

		const dataSeries = d3Line()
			.x(d => Math.round(xScale(xAccessor(d))))
			.y(d => Math.round(yScale(yAccessor(d))));

		if (isDefined(interpolation)) {
			dataSeries.curve(interpolation);
		}
		if (!connectNulls) {
			dataSeries.defined(d => defined(yAccessor(d)));
		}

		ctx.beginPath();
		dataSeries.context(ctx)(plotData);
		ctx.stroke();

		if (canvasClip) {
			ctx.restore();
		}
	}
	renderSVG(moreProps) {
		const { yAccessor, stroke, strokeOpacity, strokeWidth, hoverStrokeWidth, defined, strokeDasharray } = this.props;
		const { connectNulls } = this.props;
		const { interpolation, style } = this.props;
		const { xAccessor, chartConfig } = moreProps;

		const { xScale, plotData, hovering } = moreProps;

		const { yScale } = chartConfig;
		const dataSeries = d3Line()
			.x(d => Math.round(xScale(xAccessor(d))))
			.y(d => Math.round(yScale(yAccessor(d))));

		if (isDefined(interpolation)) {
			dataSeries.curve(interpolation);
		}
		if (!connectNulls) {
			dataSeries.defined(d => defined(yAccessor(d)));
		}
		const d = dataSeries(plotData);

		const { fill, className } = this.props;

		return (
			<path
				style={style}
				className={`${className} ${stroke ? "" : " line-stroke"}`}
				d={d}
				stroke={stroke}
				strokeOpacity={strokeOpacity}
				strokeWidth={hovering ? hoverStrokeWidth : strokeWidth}
				strokeDasharray={getStrokeDasharray(strokeDasharray)}
				fill={fill}
			/>
		);
	}
	render() {
		const { highlightOnHover, onHover, onUnHover } = this.props;
		const hoverProps = (highlightOnHover || onHover || onUnHover)
			? {
				isHover: this.isHover,
				drawOn: ["mousemove", "pan"],
				canvasToDraw: getMouseCanvas
			}
			: {
				drawOn: ["pan"],
				canvasToDraw: getAxisCanvas
			};

		return <GenericChartComponent
			svgDraw={this.renderSVG}

			canvasDraw={this.drawOnCanvas}

			onClickWhenHover={this.props.onClick}
			onDoubleClickWhenHover={this.props.onDoubleClick}
            onContextMenuWhenHover={this.props.onContextMenu}
            onHover={this.props.onHover}
            onUnHover={this.props.onUnHover}
			{...hoverProps}
		/>;
	}
}

/*
function segment(points, ctx) {
	ctx.beginPath();

	const [x, y] = first(points);
	ctx.moveTo(x, y);
	for (let i = 1; i < points.length; i++) {
		const [x1, y1] = points[i];
		ctx.lineTo(x1, y1);
	}

	ctx.stroke();
}
*/

LineSeries.propTypes = {
	className: PropTypes.string,
	strokeWidth: PropTypes.number,
	strokeOpacity: PropTypes.number,
	stroke: PropTypes.string,
	hoverStrokeWidth: PropTypes.number,
	fill: PropTypes.string,
	defined: PropTypes.func,
	hoverTolerance: PropTypes.number,
	strokeDasharray: PropTypes.oneOf(strokeDashTypes),
	highlightOnHover: PropTypes.bool,
	onClick: PropTypes.func,
	onDoubleClick: PropTypes.func,
	onContextMenu: PropTypes.func,
	yAccessor: PropTypes.func,
	connectNulls: PropTypes.bool,
	interpolation: PropTypes.func,
	canvasClip: PropTypes.func,
	style: PropTypes.object,
};

LineSeries.defaultProps = {
	className: "line ",
	strokeWidth: 1,
	strokeOpacity: 1,
	hoverStrokeWidth: 4,
	fill: "none",
	stroke: "#4682B4",
	strokeDasharray: "Solid",
	defined: d => !isNaN(d),
	hoverTolerance: 6,
	highlightOnHover: false,
	connectNulls: false,
	onClick: function(e) { console.log("Click", e); },
	onDoubleClick: function(e) { console.log("Double Click", e); },
	onContextMenu: function(e) { console.log("Right Click", e); },
};

export default LineSeries;
<|endoftext|>


<commit_before>// Custom JS

// create referral via ajax request
function createReferral(){
    var dataString = $('form#referral_create').serialize();
    console.log(dataString);
    $.ajax({
        type: "POST",
        url: "/apiv1/referrals/",
        data: dataString,
        success: function(data) {
            console.log(data);
            var html ='
'+ data.title +'	'+ data.slug +'	'+ data.clicks +'	Edit	Delete
';
            $('.table tbody').append(html);
            var message_html = '
×' + 'New referral added.' + '
'
            $('#messages').append(message_html);
        }
     });
     return false;
}

// remove referral via ajax request
function removeReferral(referral_id){
    var el = 'form#' + referral_id
    var dataString = $(el).serializeArray();
    $.ajax({
        type: "DELETE",
        url: "/apiv1/referral/" + referral_id,
        beforeSend: function(xhr) {
            xhr.setRequestHeader("X-CSRFToken", dataString[0]['value']);
        },
        success: function(response) {
            var old_el = '#id-' + referral_id
            $(old_el).remove();
        }
     });
     return false;
}<commit_msg>fix edit link after referral creation<commit_after>// Custom JS

// create referral via ajax request
function createReferral(){
    var dataString = $('form#referral_create').serialize();
    console.log(dataString);
    $.ajax({
        type: "POST",
        url: "/apiv1/referrals/",
        data: dataString,
        success: function(data) {
            console.log(data);
            var html ='
'+ data.title +'	'+ data.slug +'	'+ data.clicks +'	Edit	<input class="btn btn-danger" type="submit" value="Delete"/>
';
            $('.table tbody').append(html);
            var message_html = '
×' + 'New referral added.' + '
'
            $('#messages').append(message_html);
        }
     });
     return false;
}

// remove referral via ajax request
function removeReferral(referral_id){
    var el = 'form#' + referral_id
    var dataString = $(el).serializeArray();
    $.ajax({
        type: "DELETE",
        url: "/apiv1/referral/" + referral_id,
        beforeSend: function(xhr) {
            xhr.setRequestHeader("X-CSRFToken", dataString[0]['value']);
        },
        success: function(response) {
            var old_el = '#id-' + referral_id
            $(old_el).remove();
        }
     });
     return false;
}<|endoftext|>


<commit_before>import { createSelector, createStructuredSelector } from 'reselect';
import difference from 'lodash/difference';
import intersection from 'lodash/intersection';
import isEmpty from 'lodash/isEmpty';
import { arrayToSentence } from 'utils';
import { getGhgEmissionDefaults, toPlural } from 'utils/ghg-emissions';
import { sortLabelByAlpha } from 'utils/graphs';
import { TOP_EMITTERS_OPTION, METRIC_OPTIONS } from 'data/constants';
import { getMeta, getRegions, getSources, getSelection } from './ghg-emissions-selectors-get';

const DEFAULTS = {
  breakBy: `regions-${METRIC_OPTIONS.ABSOLUTE_VALUE.value}`
};

const getOptionSelectedFunction = filter => (options, selected) => {
  if (!options) return null;
  if (!selected) {
    const defaultOption = options.find(b => b.value === DEFAULTS[filter]);
    return defaultOption || options[0];
  }
  return options.find(o => o.value === selected);
};

// Sources selectors
const getSourceOptions = createSelector([getSources], sources => {
  if (!sources) return null;

  return sources.map(source => ({
    name: source.name,
    label: source.label,
    value: String(source.value)
  }));
});

const getSourceSelected = createSelector(
  [getSourceOptions, getSelection('source')],
  getOptionSelectedFunction('source')
);

// BreakBy selectors
const BREAK_BY_OPTIONS = [
  {
    label: `Regions-${METRIC_OPTIONS.ABSOLUTE_VALUE.label}`,
    value: `regions-${METRIC_OPTIONS.ABSOLUTE_VALUE.value}`
  },
  {
    label: `Regions-${METRIC_OPTIONS.PER_CAPITA.label}`,
    value: `regions-${METRIC_OPTIONS.PER_CAPITA.value}`
  },
  {
    label: `Regions-${METRIC_OPTIONS.PER_GDP.label}`,
    value: `regions-${METRIC_OPTIONS.PER_GDP.value}`
  },
  {
    label: 'Sector',
    value: 'sector'
  },
  {
    label: 'Gas',
    value: 'gas'
  }
];

const getBreakByOptions = () => BREAK_BY_OPTIONS;

const getBreakByOptionSelected = createSelector(
  [getBreakByOptions, getSelection('breakBy')],
  getOptionSelectedFunction('breakBy')
);

const getBreakBySelected = createSelector(getBreakByOptionSelected, breakBySelected => {
  if (!breakBySelected) return null;
  const breakByArray = breakBySelected.value.split('-');
  return { modelSelected: breakByArray[0], metricSelected: breakByArray[1] };
});

export const getModelSelected = createSelector(
  getBreakBySelected,
  breakBySelected => (breakBySelected && breakBySelected.modelSelected) || null
);
export const getMetricSelected = createSelector(
  getBreakBySelected,
  breakBySelected => (breakBySelected && breakBySelected.metricSelected) || null
);

const getRegionsOptions = createSelector([getRegions], regions => {
  if (!regions) return null;
  const mappedRegions = [TOP_EMITTERS_OPTION];
  regions.forEach(region => {
    const regionMembers = region.members.map(m => m.iso_code3);
    mappedRegions.push({
      label: region.wri_standard_name,
      value: region.iso_code3,
      iso: region.iso_code3,
      expandsTo: regionMembers,
      groupId: 'regions'
    });
  });
  return mappedRegions;
});

const filterOptionsBySource = field =>
  createSelector([getMeta, getSourceSelected], (meta, sourceSelected) => {
    if (isEmpty(meta) || !sourceSelected) return null;
    const fieldOptions = meta[field];
    const sourceValue = sourceSelected.value;
    const sourceMeta = meta.data_source.find(s => String(s.value) === sourceValue);
    const allowedIds = sourceMeta[field];
    return fieldOptions.filter(o => allowedIds.includes(o.value));
  });

const getFieldOptions = field =>
  createSelector([getRegionsOptions, filterOptionsBySource(field)], (regions, filteredOptions) => {
    if (!filteredOptions) return [];
    const fieldOptions = filteredOptions;

    if (field === 'location') {
      if (!regions) return [];
      const countries = [];
      const regionIsos = regions.map(r => r.iso);
      fieldOptions.forEach(d => {
        if (!regionIsos.includes(d.iso)) {
          countries.push({
            ...d,
            value: d.iso,
            groupId: 'countries'
          });
        }
      });
      const sortedRegions = sortLabelByAlpha(regions).sort(x => (x.value === 'TOP' ? -1 : 0));
      return sortedRegions.concat(sortLabelByAlpha(countries));
    }

    return sortLabelByAlpha(fieldOptions);
  });

const getDefaultOptions = createSelector([getSourceSelected, getMeta], (sourceSelected, meta) => {
  if (!sourceSelected || !meta) return null;
  const defaults = getGhgEmissionDefaults(sourceSelected, meta);
  const defaultOptions = {};
  Object.keys(defaults).forEach(key => {
    const keyDefault = String(defaults[key]).split(',');
    defaultOptions[key] = meta[key].filter(
      m =>
        keyDefault.includes(m.iso) ||
        keyDefault.includes(m.label) ||
        keyDefault.includes(String(m.value))
    );
  });
  defaultOptions.location = [TOP_EMITTERS_OPTION];
  return defaultOptions;
});

const getSectorOptions = createSelector([getFieldOptions('sector')], options => {
  if (!options || isEmpty(options)) return null;
  // const hasChildren = d => options.some(o => o.parentId === d.value);
  const aggregatesComesFirst = o => (o.groupId === 'aggregations' ? -1 : 0);

  const sectors = options
    .filter(s => !s.parentId)
    .map(d => ({
      label: d.label,
      value: d.value,
      expandsTo: d.aggregatedSectorIds,
      // TODO: first add optgroup to multileveldropdown
      // groupParent: hasChildren(d) && String(d.value),
      groupId: isEmpty(d.aggregatedSectorIds) ? 'sectors' : 'aggregations'
    }))
    .sort(aggregatesComesFirst);

  const subsectors = options.filter(s => s.parentId).map(d => ({
    label: d.label,
    value: d.value,
    group: String(d.parentId),
    groupId: 'sectors'
  }));
  return [...sectors, ...subsectors];
});

const CHART_TYPE_OPTIONS = [
  { label: 'line', value: 'line' },
  { label: 'area', value: 'area' },
  { label: 'percentage', value: 'percentage' }
];

const getChartTypeOptions = () => CHART_TYPE_OPTIONS;

export const getOptions = createStructuredSelector({
  sources: getSourceOptions,
  chartType: getChartTypeOptions,
  breakBy: getBreakByOptions,
  regions: getFieldOptions('location'),
  sectors: getSectorOptions,
  gases: getFieldOptions('gas')
});

const getFiltersSelected = field =>
  createSelector(
    [getOptions, getSelection(field), getDefaultOptions],
    (options, selected, defaults) => {
      const fieldOptions =
        options && (field === 'location' ? options.regions : options[toPlural(field)]);
      if (!defaults) return null;
      if (!selected || !fieldOptions || isEmpty(fieldOptions)) {
        return defaults[field];
      }
      let selectedFilters = [];
      if (selected) {
        const selectedValues = selected.split(',');
        selectedFilters = fieldOptions.filter(
          filter =>
            selectedValues.indexOf(String(filter.value)) !== -1 ||
            selectedValues.indexOf(filter.iso_code3) !== -1
        );
      }

      return selectedFilters;
    }
  );

const getChartTypeSelected = createSelector(
  [getChartTypeOptions, getSelection('chartType')],
  getOptionSelectedFunction('chartType')
);

const getOverlappingConflicts = optionsSelected => {
  if (!optionsSelected || optionsSelected.length <= 1) return [];

  const conflicts = [];
  const overlapsCheckedIds = [];

  optionsSelected.forEach(option => {
    if (option.expandsTo) {
      const overlappingOptions = [];
      optionsSelected.forEach(option2 => {
        if (option2.value === option.value) return;
        if (overlapsCheckedIds.includes(option2.value)) return;

        const expandedOption = [option.value, ...(option.expandsTo || [])].map(String);
        const expandedOption2 = [option2.value, option2.group, ...(option2.expandsTo || [])].map(
          String
        );

        if (intersection(expandedOption, expandedOption2).length > 0) {
          overlappingOptions.push(option2.label);
        }
      });

      if (overlappingOptions.length) {
        conflicts.push(`${option.label} overlaps with ${arrayToSentence(overlappingOptions)}`);
      }

      overlapsCheckedIds.push(option.value);
    }
  });

  return conflicts;
};

const getGasConflicts = gasSelected => {
  const aggregatedGases = ['All GHG', 'Aggregate F-gases', 'Aggregate GHGs'];

  if (!gasSelected || gasSelected.length <= 1) return [];

  const conflicts = [];

  aggregatedGases.forEach(gas => {
    if (gasSelected.some(g => g.label.includes(gas))) {
      conflicts.push(`${gas} option cannot be selected with any other gas`);
    }
  });

  return conflicts;
};

export const getFiltersConflicts = createSelector(
  [
    getFiltersSelected('location'),
    getFiltersSelected('gas'),
    getFiltersSelected('sector'),
    getModelSelected,
    getChartTypeSelected
  ],
  (locationSelected, gasSelected, sectorsSelected, modelSelected, chartSelected) => {
    let conflicts = [];
    let canChangeBreakByTo = difference(['sector', 'gas', 'regions'], [modelSelected]);
    const solutions = ['Please deselect all conflicting options'];
    const isAggregatedChart = chartSelected.value !== 'line';
    const notBreakBySector = modelSelected !== 'sector';
    const notBreakByGas = modelSelected !== 'gas';
    const notBreakByRegion = modelSelected !== 'regions';

    const sectorConflicts = getOverlappingConflicts(sectorsSelected);
    const regionConflicts = getOverlappingConflicts(locationSelected);
    const gasConflicts = getGasConflicts(gasSelected);

    if (sectorConflicts.length) {
      canChangeBreakByTo = difference(canChangeBreakByTo, ['gas', 'regions']);
    }
    if (regionConflicts.length) {
      canChangeBreakByTo = difference(canChangeBreakByTo, ['sector', 'gas']);
    }
    if (gasConflicts.length) {
      canChangeBreakByTo = difference(canChangeBreakByTo, ['sector', 'regions']);
    }

    if (sectorConflicts.length && (isAggregatedChart || notBreakBySector)) {
      conflicts = conflicts.concat(sectorConflicts);
    }

    if (gasConflicts.length && (isAggregatedChart || notBreakByGas)) {
      conflicts = conflicts.concat(gasConflicts);
    }

    if (regionConflicts.length && (isAggregatedChart || notBreakByRegion)) {
      conflicts = conflicts.concat(regionConflicts);
    }

    if (conflicts.length && isAggregatedChart) {
      solutions.push('change "Chart Type" to line chart');
    }

    if (canChangeBreakByTo.length) {
      solutions.push(`change "Break by" to ${arrayToSentence(canChangeBreakByTo)}`);
    }

    return {
      conflicts,
      solutionText: solutions.join(' or ')
    };
  }
);

export const getOptionsSelected = createStructuredSelector({
  sourcesSelected: getSourceSelected,
  regionsSelected: getFiltersSelected('location'),
  sectorsSelected: getFiltersSelected('sector'),
  gasesSelected: getFiltersSelected('gas'),
  breakBySelected: getBreakByOptionSelected,
  chartTypeSelected: getChartTypeSelected
});
<commit_msg>ghg-module: silly me, bringing back multilevel on sectors<commit_after>import { createSelector, createStructuredSelector } from 'reselect';
import difference from 'lodash/difference';
import intersection from 'lodash/intersection';
import isEmpty from 'lodash/isEmpty';
import { arrayToSentence } from 'utils';
import { getGhgEmissionDefaults, toPlural } from 'utils/ghg-emissions';
import { sortLabelByAlpha } from 'utils/graphs';
import { TOP_EMITTERS_OPTION, METRIC_OPTIONS } from 'data/constants';
import { getMeta, getRegions, getSources, getSelection } from './ghg-emissions-selectors-get';

const DEFAULTS = {
  breakBy: `regions-${METRIC_OPTIONS.ABSOLUTE_VALUE.value}`
};

const getOptionSelectedFunction = filter => (options, selected) => {
  if (!options) return null;
  if (!selected) {
    const defaultOption = options.find(b => b.value === DEFAULTS[filter]);
    return defaultOption || options[0];
  }
  return options.find(o => o.value === selected);
};

// Sources selectors
const getSourceOptions = createSelector([getSources], sources => {
  if (!sources) return null;

  return sources.map(source => ({
    name: source.name,
    label: source.label,
    value: String(source.value)
  }));
});

const getSourceSelected = createSelector(
  [getSourceOptions, getSelection('source')],
  getOptionSelectedFunction('source')
);

// BreakBy selectors
const BREAK_BY_OPTIONS = [
  {
    label: `Regions-${METRIC_OPTIONS.ABSOLUTE_VALUE.label}`,
    value: `regions-${METRIC_OPTIONS.ABSOLUTE_VALUE.value}`
  },
  {
    label: `Regions-${METRIC_OPTIONS.PER_CAPITA.label}`,
    value: `regions-${METRIC_OPTIONS.PER_CAPITA.value}`
  },
  {
    label: `Regions-${METRIC_OPTIONS.PER_GDP.label}`,
    value: `regions-${METRIC_OPTIONS.PER_GDP.value}`
  },
  {
    label: 'Sector',
    value: 'sector'
  },
  {
    label: 'Gas',
    value: 'gas'
  }
];

const getBreakByOptions = () => BREAK_BY_OPTIONS;

const getBreakByOptionSelected = createSelector(
  [getBreakByOptions, getSelection('breakBy')],
  getOptionSelectedFunction('breakBy')
);

const getBreakBySelected = createSelector(getBreakByOptionSelected, breakBySelected => {
  if (!breakBySelected) return null;
  const breakByArray = breakBySelected.value.split('-');
  return { modelSelected: breakByArray[0], metricSelected: breakByArray[1] };
});

export const getModelSelected = createSelector(
  getBreakBySelected,
  breakBySelected => (breakBySelected && breakBySelected.modelSelected) || null
);
export const getMetricSelected = createSelector(
  getBreakBySelected,
  breakBySelected => (breakBySelected && breakBySelected.metricSelected) || null
);

const getRegionsOptions = createSelector([getRegions], regions => {
  if (!regions) return null;
  const mappedRegions = [TOP_EMITTERS_OPTION];
  regions.forEach(region => {
    const regionMembers = region.members.map(m => m.iso_code3);
    mappedRegions.push({
      label: region.wri_standard_name,
      value: region.iso_code3,
      iso: region.iso_code3,
      expandsTo: regionMembers,
      groupId: 'regions'
    });
  });
  return mappedRegions;
});

const filterOptionsBySource = field =>
  createSelector([getMeta, getSourceSelected], (meta, sourceSelected) => {
    if (isEmpty(meta) || !sourceSelected) return null;
    const fieldOptions = meta[field];
    const sourceValue = sourceSelected.value;
    const sourceMeta = meta.data_source.find(s => String(s.value) === sourceValue);
    const allowedIds = sourceMeta[field];
    return fieldOptions.filter(o => allowedIds.includes(o.value));
  });

const getFieldOptions = field =>
  createSelector([getRegionsOptions, filterOptionsBySource(field)], (regions, filteredOptions) => {
    if (!filteredOptions) return [];
    const fieldOptions = filteredOptions;

    if (field === 'location') {
      if (!regions) return [];
      const countries = [];
      const regionIsos = regions.map(r => r.iso);
      fieldOptions.forEach(d => {
        if (!regionIsos.includes(d.iso)) {
          countries.push({
            ...d,
            value: d.iso,
            groupId: 'countries'
          });
        }
      });
      const sortedRegions = sortLabelByAlpha(regions).sort(x => (x.value === 'TOP' ? -1 : 0));
      return sortedRegions.concat(sortLabelByAlpha(countries));
    }

    return sortLabelByAlpha(fieldOptions);
  });

const getDefaultOptions = createSelector([getSourceSelected, getMeta], (sourceSelected, meta) => {
  if (!sourceSelected || !meta) return null;
  const defaults = getGhgEmissionDefaults(sourceSelected, meta);
  const defaultOptions = {};
  Object.keys(defaults).forEach(key => {
    const keyDefault = String(defaults[key]).split(',');
    defaultOptions[key] = meta[key].filter(
      m =>
        keyDefault.includes(m.iso) ||
        keyDefault.includes(m.label) ||
        keyDefault.includes(String(m.value))
    );
  });
  defaultOptions.location = [TOP_EMITTERS_OPTION];
  return defaultOptions;
});

const getSectorOptions = createSelector([getFieldOptions('sector')], options => {
  if (!options || isEmpty(options)) return null;
  const hasChildren = d => options.some(o => o.parentId === d.value);
  const aggregatesComesFirst = o => (o.groupId === 'aggregations' ? -1 : 0);

  const sectors = options
    .filter(s => !s.parentId)
    .map(d => ({
      label: d.label,
      value: d.value,
      expandsTo: d.aggregatedSectorIds,
      groupParent: hasChildren(d) ? String(d.value) : null,
      groupId: isEmpty(d.aggregatedSectorIds) ? 'sectors' : 'aggregations'
    }))
    .sort(aggregatesComesFirst);

  const subsectors = options.filter(s => s.parentId).map(d => ({
    label: d.label,
    value: d.value,
    group: String(d.parentId),
    groupId: 'sectors'
  }));
  return [...sectors, ...subsectors];
});

const CHART_TYPE_OPTIONS = [
  { label: 'line', value: 'line' },
  { label: 'area', value: 'area' },
  { label: 'percentage', value: 'percentage' }
];

const getChartTypeOptions = () => CHART_TYPE_OPTIONS;

export const getOptions = createStructuredSelector({
  sources: getSourceOptions,
  chartType: getChartTypeOptions,
  breakBy: getBreakByOptions,
  regions: getFieldOptions('location'),
  sectors: getSectorOptions,
  gases: getFieldOptions('gas')
});

const getFiltersSelected = field =>
  createSelector(
    [getOptions, getSelection(field), getDefaultOptions],
    (options, selected, defaults) => {
      const fieldOptions =
        options && (field === 'location' ? options.regions : options[toPlural(field)]);
      if (!defaults) return null;
      if (!selected || !fieldOptions || isEmpty(fieldOptions)) {
        return defaults[field];
      }
      let selectedFilters = [];
      if (selected) {
        const selectedValues = selected.split(',');
        selectedFilters = fieldOptions.filter(
          filter =>
            selectedValues.indexOf(String(filter.value)) !== -1 ||
            selectedValues.indexOf(filter.iso_code3) !== -1
        );
      }

      return selectedFilters;
    }
  );

const getChartTypeSelected = createSelector(
  [getChartTypeOptions, getSelection('chartType')],
  getOptionSelectedFunction('chartType')
);

const getOverlappingConflicts = optionsSelected => {
  if (!optionsSelected || optionsSelected.length <= 1) return [];

  const conflicts = [];
  const overlapsCheckedIds = [];

  optionsSelected.forEach(option => {
    if (option.expandsTo) {
      const overlappingOptions = [];
      optionsSelected.forEach(option2 => {
        if (option2.value === option.value) return;
        if (overlapsCheckedIds.includes(option2.value)) return;

        const expandedOption = [option.value, ...(option.expandsTo || [])].map(String);
        const expandedOption2 = [option2.value, option2.group, ...(option2.expandsTo || [])].map(
          String
        );

        if (intersection(expandedOption, expandedOption2).length > 0) {
          overlappingOptions.push(option2.label);
        }
      });

      if (overlappingOptions.length) {
        conflicts.push(`${option.label} overlaps with ${arrayToSentence(overlappingOptions)}`);
      }

      overlapsCheckedIds.push(option.value);
    }
  });

  return conflicts;
};

const getGasConflicts = gasSelected => {
  const aggregatedGases = ['All GHG', 'Aggregate F-gases', 'Aggregate GHGs'];

  if (!gasSelected || gasSelected.length <= 1) return [];

  const conflicts = [];

  aggregatedGases.forEach(gas => {
    if (gasSelected.some(g => g.label.includes(gas))) {
      conflicts.push(`${gas} option cannot be selected with any other gas`);
    }
  });

  return conflicts;
};

export const getFiltersConflicts = createSelector(
  [
    getFiltersSelected('location'),
    getFiltersSelected('gas'),
    getFiltersSelected('sector'),
    getModelSelected,
    getChartTypeSelected
  ],
  (locationSelected, gasSelected, sectorsSelected, modelSelected, chartSelected) => {
    let conflicts = [];
    let canChangeBreakByTo = difference(['sector', 'gas', 'regions'], [modelSelected]);
    const solutions = ['Please deselect all conflicting options'];
    const isAggregatedChart = chartSelected.value !== 'line';
    const notBreakBySector = modelSelected !== 'sector';
    const notBreakByGas = modelSelected !== 'gas';
    const notBreakByRegion = modelSelected !== 'regions';

    const sectorConflicts = getOverlappingConflicts(sectorsSelected);
    const regionConflicts = getOverlappingConflicts(locationSelected);
    const gasConflicts = getGasConflicts(gasSelected);

    if (sectorConflicts.length) {
      canChangeBreakByTo = difference(canChangeBreakByTo, ['gas', 'regions']);
    }
    if (regionConflicts.length) {
      canChangeBreakByTo = difference(canChangeBreakByTo, ['sector', 'gas']);
    }
    if (gasConflicts.length) {
      canChangeBreakByTo = difference(canChangeBreakByTo, ['sector', 'regions']);
    }

    if (sectorConflicts.length && (isAggregatedChart || notBreakBySector)) {
      conflicts = conflicts.concat(sectorConflicts);
    }

    if (gasConflicts.length && (isAggregatedChart || notBreakByGas)) {
      conflicts = conflicts.concat(gasConflicts);
    }

    if (regionConflicts.length && (isAggregatedChart || notBreakByRegion)) {
      conflicts = conflicts.concat(regionConflicts);
    }

    if (conflicts.length && isAggregatedChart) {
      solutions.push('change "Chart Type" to line chart');
    }

    if (canChangeBreakByTo.length) {
      solutions.push(`change "Break by" to ${arrayToSentence(canChangeBreakByTo)}`);
    }

    return {
      conflicts,
      solutionText: solutions.join(' or ')
    };
  }
);

export const getOptionsSelected = createStructuredSelector({
  sourcesSelected: getSourceSelected,
  regionsSelected: getFiltersSelected('location'),
  sectorsSelected: getFiltersSelected('sector'),
  gasesSelected: getFiltersSelected('gas'),
  breakBySelected: getBreakByOptionSelected,
  chartTypeSelected: getChartTypeSelected
});
<|endoftext|>


<commit_before>angular.module('orka.locations')

// use proj4 either to load 25833 geojson or display transformed geojson?
// http://ahocevar.net/2014/07/10/proj4js-2-2-x-with-ol3.html
.directive('orkaLocations', ['LayersFactory', 'LayersService', 'ConfigService', 'MapService', function(LayersFactory, LayersService, ConfigService, MapService) {
    return {
        scope: {
            showLocationFeature: '=showLocationFeature'
        },
        templateUrl: 'orka/modules/locations/templates/locations.html',
        link: {
            pre: function(scope, element, attrs) {
                scope.selectedTitle = undefined;
                scope.selectedFeature = undefined;
                scope.layer = LayersFactory.newGeoJSON({
                    projection: ConfigService.config.map.projection,
                    url: ConfigService.config.locations,
                    style: function(feature, resolution) {
                        var styles = [];
                        if(scope.selectedTitle === feature.get('title')) {
                            styles.push(new ol.style.Style({
                                stroke: new ol.style.Stroke({
                                    color: 'rgba(0, 0, 0, 1)',
                                    width: 3
                                })
                            }));
                        }
                        return styles;
                    },
                    displayInLayerswitcher: false,
                    visible: true
                });
                LayersService.addLayer(scope.layer);
                scope.titles = [];
                scope.layer.getSource().on('addfeature', function(evt) {
                    scope.titles.push(evt.feature.get('title'));
                });

                scope.selectTitle = function(title) {
                    scope.selectedTitle = title;
                    var source = scope.layer.getSource();
                    if(scope.title !== undefined) {
                        source.forEachFeature(function(feature) {
                            if(feature.get('title') === scope.selectedTitle) {
                                var map = MapService.getMap();
                                map.getView().fitExtent(feature.getGeometry().getExtent(), map.getSize());
                            }
                        });
                    }
                    source.dispatchChangeEvent();
                };
            },
            post: function(scope, element, attrs) {
                scope.$watch('showLocationFeature', function(newVal, oldVal) {
                    if(newVal === false) {
                        scope.selectTitle();
                    }
                });
            }
        }
    };
}]);
<commit_msg>Fixed variable name<commit_after>angular.module('orka.locations')

// use proj4 either to load 25833 geojson or display transformed geojson?
// http://ahocevar.net/2014/07/10/proj4js-2-2-x-with-ol3.html
.directive('orkaLocations', ['LayersFactory', 'LayersService', 'ConfigService', 'MapService', function(LayersFactory, LayersService, ConfigService, MapService) {
    return {
        scope: {
            showLocationFeature: '=showLocationFeature'
        },
        templateUrl: 'orka/modules/locations/templates/locations.html',
        link: {
            pre: function(scope, element, attrs) {
                scope.selectedTitle = undefined;
                scope.selectedFeature = undefined;
                scope.layer = LayersFactory.newGeoJSON({
                    projection: ConfigService.config.map.projection,
                    url: ConfigService.config.locations,
                    style: function(feature, resolution) {
                        var styles = [];
                        if(scope.selectedTitle === feature.get('title')) {
                            styles.push(new ol.style.Style({
                                stroke: new ol.style.Stroke({
                                    color: 'rgba(0, 0, 0, 1)',
                                    width: 3
                                })
                            }));
                        }
                        return styles;
                    },
                    displayInLayerswitcher: false,
                    visible: true
                });
                LayersService.addLayer(scope.layer);
                scope.titles = [];
                scope.layer.getSource().on('addfeature', function(evt) {
                    scope.titles.push(evt.feature.get('title'));
                });

                scope.selectTitle = function(title) {
                    scope.selectedTitle = title;
                    var source = scope.layer.getSource();
                    if(scope.selectedTitle !== undefined) {
                        source.forEachFeature(function(feature) {
                            if(feature.get('title') === scope.selectedTitle) {
                                var map = MapService.getMap();
                                map.getView().fitExtent(feature.getGeometry().getExtent(), map.getSize());
                            }
                        });
                    }
                    source.dispatchChangeEvent();
                };
            },
            post: function(scope, element, attrs) {
                scope.$watch('showLocationFeature', function(newVal, oldVal) {
                    if(newVal === false) {
                        scope.selectTitle();
                    }
                });
            }
        }
    };
}]);
<|endoftext|>


<commit_before>'use strict';

var Mapusaurus = {
    //  Leaflet map
    map: null,
    //  Leaflet layers
    layers: {tract: {minority: null}},
    //  Stores geo data, along with fields for layers
    dataStore: {tract: {}},
    //  Stores stat data when the associated geos aren't loaded
    dataWithoutGeo: {tract: {minority: {}}},
    //  Keep track of which stateXcounties we've loaded
    statsLoaded: {minority: {}},
    //  Some style info
    bubbleStyle: {fillColor: '#fff', fillOpacity: 0.9, weight: 2,
                  color: '#000'},
    //  fillColor will be assigned when rendering
    tractStyle: {fillOpacity: 0.7, weight: 2, color: '#babbbd'},

    initialize: function (map) {
        var tiles = new L.TileLayer.GeoJSON('/shapes/tiles/{z}/{x}/{y}');
        map.setView([41.88, -87.63], 12);
        Mapusaurus.map = map;
        Mapusaurus.addKey(map);
        map.addLayer(tiles);
        Mapusaurus.layers.tract.minority = L.geoJson(
            {type: 'FeatureCollection', features: []},
            {onEachFeature: Mapusaurus.eachMinority,
             style: Mapusaurus.minorityContinuousStyle}
        );
        Mapusaurus.layers.tract.minority.addTo(map);
        if (Mapusaurus.urlParam('lender')) {
            Mapusaurus.layers.tract.loanVolume = L.layerGroup([]);
            Mapusaurus.layers.tract.loanVolume.addTo(map);
            Mapusaurus.dataWithoutGeo.tract.loanVolume = {};
            Mapusaurus.statsLoaded.loanVolume = {};
        }
        //  @todo: really, we only care about the part of the viewport which
        //  is new
        map.on('moveend', Mapusaurus.reloadGeo);
        //  @todo: really, we only care on zoom-out + part which is new
        map.on('zoomend', Mapusaurus.reloadGeo);
        //  Connect style selector
        $('#style-selector').on('change', function() {
            Mapusaurus.layers.tract.minority.setStyle(
                Mapusaurus[$('#style-selector').val()]);
        });
        //  Kick it off
        Mapusaurus.reloadGeo();
    },

    /* Indicates what the colors mean */
    addKey: function(map) {
        var key = L.control();
        key.onAdd = function() {
            return L.DomUtil.get('key');
        };
        key.addTo(map);
    },

    /* Naive url parameter parser */
    urlParam: function(field) {
        var url = window.location.search.replace('?', ''),
            keyValueStrs = url.split('&'),
            pairs = _.map(keyValueStrs, function(keyValueStr) {
                return keyValueStr.split('=');
            }),
            params = _.reduce(pairs, function(soFar, pair) {
                if (pair.length === 2) {
                    soFar[pair[0]] = pair[1];
                }
                return soFar;
            }, {});
        return params[field];
    },

    /* Whenever the map is shifted/zoomed, reload geo data */
    reloadGeo: function() {
        var bounds = Mapusaurus.map.getBounds(),
            xtraWidth = Math.abs(bounds.getEast() - bounds.getWest()) / 3,
            xtraHeight = Math.abs(bounds.getNorth() - bounds.getSouth()) / 3,
            //  NW-hemisphere centric
            expandedN = bounds.getNorth() + xtraHeight,
            expandedS = bounds.getSouth() - xtraHeight,
            expandedW = bounds.getWest() - xtraWidth,
            expandedE = bounds.getEast() + xtraWidth;

        /* To be responsive, only load census tract data at zoom-level 10
         * or above */
        if (Mapusaurus.map.getZoom() >  10) {
            Mapusaurus.loadTractData(
                1,
                L.latLngBounds(L.latLng(expandedS, expandedW),
                               L.latLng(expandedN, expandedE)));
        }
    },

    /* Load census tract geo data (by pages) into dataStore. Bounds should
     * remain consistent between pages of results */
    loadTractData: function(page, bounds) {
        //  This is north-western-hemisphere-centric for now
        $.getJSON('/shapes/tracts-in/?minlat=' + bounds.getSouth().toString() +
                  '&maxlat=' + bounds.getNorth().toString() +
                  '&minlon=' + bounds.getWest().toString() +
                  '&maxlon=' + bounds.getEast().toString() +
                  '&page_num=' + page.toString(),
                  function(data) {
            var newTracts = [];   //  geos we've never seen

            //  Collect new tracts, add them to dataStore
            _.each(data.features, function(feature) {
                var geoid = feature.properties.geoid;
                if (!_.has(Mapusaurus.dataStore.tract, geoid)) {
                    Mapusaurus.dataStore.tract[geoid] = feature;
                    newTracts.push(geoid);
                }
            });

            //  Check if there's anything new to draw, load stats data
            if (newTracts.length > 0) {
                Mapusaurus.updateDataWithoutGeos(newTracts);
                Mapusaurus.fetchMissingStats(newTracts);
            }

            //  Continue with next page of geo results
            if (data.features.length > 0) {
                Mapusaurus.loadTractData(page + 1, bounds);
            }
        });
    },

    /*  We may have previously loaded stats data without the geos. Run through
     *  that data and see if the new geo data matches */
    updateDataWithoutGeos: function(newTracts) {
        var toDraw = {},  // geoids by layer
            undrawnData = Mapusaurus.dataWithoutGeo.tract;
        //  For each layer
        _.each(_.keys(undrawnData), function (layerName) {
            toDraw[layerName] = [];

            //  For each shape
            _.each(newTracts, function(geoid) {
                var geoProps = Mapusaurus.dataStore.tract[geoid].properties;
                //  Check if the data can now be drawn
                if (_.has(undrawnData[layerName], geoid)) {
                    geoProps['layer_' + layerName] =
                        undrawnData[layerName][geoid];
                    toDraw[layerName].push(geoid);
                    delete undrawnData[layerName][geoid];
                }
            });
        });
        Mapusaurus.draw(toDraw);
    },

    /* We have geos without their associated stats - kick off the load */
    fetchMissingStats: function(newTracts) {
        //  This is a list of triples: [[layer name, state, county]]
        var missingStats = [];
        _.each(_.keys(Mapusaurus.layers.tract), function(layerName) {
            //  We only care about unseen stat data
            var missingData = _.filter(newTracts, function(geoid) {
                var geo = Mapusaurus.dataStore.tract[geoid],
                    stateCounty = geo.properties.statefp +
                                  geo.properties.countyfp;
                return !Mapusaurus.statsLoaded[layerName][stateCounty];
            });
            //  convert to state + county strings
            missingData = _.map(missingData, function(geoid) {
                var geo = Mapusaurus.dataStore.tract[geoid];
                return geo.properties.statefp + geo.properties.countyfp;
            });
            //  remove any duplicates; we end with what state/counties need to
            //  be retrieved
            missingData = _.uniq(missingData);

            //  Keep track of what we will be loading
            _.each(missingData, function(stateCounty) {
                //  Add to the list of data to load
                missingStats.push([layerName, stateCounty.substr(0, 2),
                                   stateCounty.substr(2)]);
                //  Signify that we are loading it...
                Mapusaurus.statsLoaded[layerName][stateCounty] = 'loading';
            });
        });
        if (missingStats.length > 0) {
            Mapusaurus.batchLoadStats(missingStats);
        }
    },

    /* We load stats data in one batch request. We need to provide the
     * endpoint/layer we care about, the data it needs (state, county, etc.),
     * and then we need to process the result. */
    batchLoadStats: function(missingStats) {
        var requests = _.map(missingStats, function(triple) {
            var params = {'state_fips': triple[1],
                          'county_fips': triple[2]};
            if (Mapusaurus.urlParam('lender')) {
                params['lender'] = Mapusaurus.urlParam('lender');
            }
            return {endpoint: triple[0], params: params};
        });

        $.ajax({
            type: 'POST',
            url: '/batch',
            contentType: 'application/json; charset=utf-8',
            data: JSON.stringify({requests: requests}),
            success: Mapusaurus.makeBatchSuccessFn(requests)
        });
    },

    /*  As the success function for making a batch request relies on the
     *  requests made, this returns a closure to handle the results of a batch
     *  load */
    makeBatchSuccessFn: function(requests) {
        return function(data) {
            var toDraw = {};
            _.each(_.keys(Mapusaurus.layers.tract), function(layerName) {
                toDraw[layerName] = [];
            });
            _.each(requests, function(request, idx) {
                var layerName = request['endpoint'],
                    llName = 'layer_' + layerName,
                    response = data['responses'][idx],
                    stateCounty = request.params['state_fips'] +
                                  request.params['county_fips'];
                _.each(_.keys(response), function(geoid) {
                    var geo = Mapusaurus.dataStore.tract[geoid];
                    //  Have not loaded the geo data yet
                    if (!geo) {
                        Mapusaurus.dataWithoutGeo.tract[layerName][geoid] =
                            response[geoid];
                    //  Have the geo data, but haven't drawn the stats yet
                    } else if (!geo.properties[llName]) {
                        geo.properties[llName] = response[geoid];
                        toDraw[layerName].push(geoid);
                    }
                });
                Mapusaurus.statsLoaded[layerName][stateCounty] = true;
            });
            Mapusaurus.draw(toDraw);
        };
    },

    /* Given a list of geo ids, segmented by layer name, add them to the
     * leaflet layer. */
    draw: function(layerToGeoids) {
        // For each layer
        _.each(_.keys(layerToGeoids), function(layerName) {
            var geoData = _.map(layerToGeoids[layerName], function(geoid) {
              return Mapusaurus.dataStore.tract[geoid];
            });
            switch(layerName) {
                case 'minority':
                    // Filter out geos with no people; don't want to draw them
                    geoData = _.filter(geoData, function(geo) {
                        return geo.properties['layer_minority'][
                            'total_pop'] > 0;
                    });
                    Mapusaurus.layers.tract.minority.addData(geoData);
                    Mapusaurus.layers.tract.minority.bringToBack();
                    break;
                case 'loanVolume':
                    _.each(geoData, function(geo) {
                        Mapusaurus.layers.tract.loanVolume.addLayer(
                            Mapusaurus.makeBubble(geo.properties)
                        );
                    });
                    break;
            }
        });
    },

    /* Styles/extras for originations layer */
    makeBubble: function(geoProps) {
        var data = geoProps['layer_loanVolume'],
            circle = L.circle([geoProps.intptlat, geoProps.intptlon],
                              100 * data['volume_per_100_households'],
                              Mapusaurus.bubbleStyle);
        //  keep expected functionality with double clicking
        circle.on('dblclick', function(ev) {
            Mapusaurus.map.setZoomAround(
                ev.latlng, Mapusaurus.map.getZoom() + 1);
        });
        circle.on('mouseover mousemove', function(e){
            new L.Rrose({ offset: new L.Point(0,-10), closeButton: false, autoPan: false })
              .setContent(data['volume'] + ' loans<br />' + data['num_households'] + ' households')
              .setLatLng(e.latlng)
              .openOn(Mapusaurus.map);
        });
        circle.on('mouseout', function(){
            Mapusaurus.map.closePopup();
        });
        return circle;
    },

    //  Used to determine color within a gradient
    minorityContinuousStyle: function(feature) {
        return Mapusaurus.minorityStyle(
            feature,
            function(minorityPercent, bucket) {
                return (minorityPercent - bucket.lowerBound) / bucket.span;
            }
        );
    },
    //  Determines colors via distinct buckets
    minorityBucketedStyle: function(feature) {
        return Mapusaurus.minorityStyle(feature, function() { return 0.5; });
    },
    //  Shared function for minority styling; called by the two previous fns
    minorityStyle: function(feature, percentFn) {
        var minorityPercent = 1 - feature.properties['layer_minority'][
                                'non_hisp_white_only_perc'],
            bucket = Mapusaurus.toBucket(minorityPercent),
            // convert given percentage to percents within bucket's bounds
            bucketPercent = percentFn(minorityPercent, bucket);
        return $.extend({}, Mapusaurus.tractStyle, {
            fillColor: Mapusaurus.colorFromPercent(bucketPercent,
                                                   bucket.colors)
        });
    },

    /* Style/extras for each census tract in the minorities layer */
    eachMinority: function(feature, layer) {
      var nonMinorityPercent = feature.properties['layer_minority'][
          'non_hisp_white_only_perc'];
      //  keep expected functionality with double clicking
      layer.on('dblclick', function(ev) {
          Mapusaurus.map.setZoomAround(ev.latlng, Mapusaurus.map.getZoom() + 1);
      });
      layer.on('mouseover mousemove', function(e){
          new L.Rrose({ offset: new L.Point(0,-10), closeButton: false, autoPan: false })
              .setContent(((1 - nonMinorityPercent) * 100).toFixed() + '% "Minority"')
              .setLatLng(e.latlng)
              .openOn(Mapusaurus.map);
      });
      layer.on('mouseout', function(){
          Mapusaurus.map.closePopup();
      });
    },

    colorRanges: [
        {
            span: 0.5,
            lowerBound: 0,
            colors: {
                lowR: 255,
                lowG: 255,
                lowB: 217,
                highR: 198,
                highG: 234,
                highB: 178
            }
        },
        {
            span: 0.3,
            lowerBound: 0.5,
            colors: {
                lowR: 198,
                lowG: 234,
                lowB: 178,
                highR: 14,
                highG: 144,
                highB: 194
            }
        },
        {
            span: 0.2,
            lowerBound: 0.8,
            colors: {
                lowR: 14,
                lowG: 144,
                lowB: 194,
                highR: 29,
                highG: 92,
                highB: 170
            }
        }
    ],

    toBucket: function(percent) {
        var i,
            len = Mapusaurus.colorRanges.length;
        for (i = 0; i < len - 1; i++) {
            //  Next bucket is too far
            if (Mapusaurus.colorRanges[i + 1].lowerBound > percent) {
                return Mapusaurus.colorRanges[i];
            }
        }
        return Mapusaurus.colorRanges[len - 1];  //  last color
    },

    /* Given low and high colors and a percent, figure out the RGB of said
     * percent in that scale */
    colorFromPercent: function(percent, c) {
        var diffR = (c.highR - c.lowR) * percent,
            diffG = (c.highG - c.lowG) * percent,
            diffB = (c.highB - c.lowB) * percent;
        return 'rgb(' + (c.lowR + diffR).toFixed() + ', ' +
               (c.lowG + diffG).toFixed() + ', ' +
               (c.lowB + diffB).toFixed() + ')';
    }
};
<commit_msg>Use geojson tiles for minority data<commit_after>'use strict';

L.TileLayer.HookableGeoJSON = L.TileLayer.GeoJSON.extend({
    _tileLoaded: function(tile) {
        var geoids = _.map(tile.datum.features, function(feature) {
                return feature.properties.geoid;
            }),
            newTracts = _.filter(geoids, function(geoid) {
                return !_.has(Mapusaurus.dataStore.tract, geoid);
            });

        //  "super"
        L.TileLayer.GeoJSON.prototype._tileLoaded.apply(this, arguments);

        if (newTracts.length > 0) {
            Mapusaurus.updateDataWithoutGeos(newTracts);
            Mapusaurus.fetchMissingStats(newTracts);
        }
    }
});

var Mapusaurus = {
    //  Leaflet map
    map: null,
    //  Leaflet layers
    layers: {tract: {minority: null}},
    //  Stores geo data, along with fields for layers
    dataStore: {tract: {}},
    drawn: {},
    //  Stores stat data when the associated geos aren't loaded
    dataWithoutGeo: {tract: {minority: {}}},
    //  Keep track of which stateXcounties we've loaded
    statsLoaded: {minority: {}},
    //  Some style info
    bubbleStyle: {fillColor: '#fff', fillOpacity: 0.9, weight: 2,
                  color: '#000'},
    //  fillColor will be assigned when rendering
    tractStyle: {stroke: true, fillOpacity: 0.7, weight: 2, color: '#babbbd',
                 fill: true},
    //  used when loading census tracts
    noStyle: {stroke: false, fill: false},

    initialize: function (map) {
        map.setView([41.88, -87.63], 12);
        Mapusaurus.map = map;
        Mapusaurus.addKey(map);
        Mapusaurus.layers.tract.minority = new L.TileLayer.HookableGeoJSON(
            '/shapes/tiles/{z}/{x}/{y}', {}, {
                onEachFeature: Mapusaurus.eachTract,
                style: Mapusaurus.minorityContinousStyle,
                // Don't redraw any tracts
                filter: function(feature) {
                    return !_.has(Mapusaurus.drawn, feature.properties.geoid);
                }
        });
        Mapusaurus.layers.tract.minority.addTo(map);
        if (Mapusaurus.urlParam('lender')) {
            Mapusaurus.layers.tract.loanVolume = L.layerGroup([]);
            Mapusaurus.layers.tract.loanVolume.addTo(map);
            Mapusaurus.dataWithoutGeo.tract.loanVolume = {};
            Mapusaurus.statsLoaded.loanVolume = {};
        }
        //  Census tracts get cleared whenever zooming in/out (analogous to
        //  other tile layers)
        map.on('zoomstart', function() { Mapusaurus.drawn = {}; });

        //  Selector to change bucket/continuous shading
        $('#style-selector').on('change', function() {
            Mapusaurus.layers.tract.minority.setStyle(
                Mapusaurus[$('#style-selector').val()]);
        });
    },

    /* Indicates what the colors mean */
    addKey: function(map) {
        var key = L.control();
        key.onAdd = function() {
            return L.DomUtil.get('key');
        };
        key.addTo(map);
    },

    /* Naive url parameter parser */
    urlParam: function(field) {
        var url = window.location.search.replace('?', ''),
            keyValueStrs = url.split('&'),
            pairs = _.map(keyValueStrs, function(keyValueStr) {
                return keyValueStr.split('=');
            }),
            params = _.reduce(pairs, function(soFar, pair) {
                if (pair.length === 2) {
                    soFar[pair[0]] = pair[1];
                }
                return soFar;
            }, {});
        return params[field];
    },

    /* Style/handling function for each census tract loaded from the geoJson
     * tile layer. Adds interactions */
    eachTract: function(feature, layer) {
        var geoid = feature.properties.geoid;
        Mapusaurus.drawn[geoid] = true;
        if (!_.has(Mapusaurus.dataStore.tract, geoid)) {
            Mapusaurus.dataStore.tract[geoid] = feature.properties;
        }
        //  keep expected functionality with double clicking
        layer.on('dblclick', function(ev) {
            Mapusaurus.map.setZoomAround(ev.latlng,
                                         Mapusaurus.map.getZoom() + 1);
        });
        layer.on('mouseover mousemove', function(e){
            var marker = new L.Rrose({
                offset: new L.Point(0, -10),
                closeButton: false,
                autoPan: false
            });
            Mapusaurus.dataStore.tract[geoid].marker = marker;
            marker.setContent(Mapusaurus.tractHoverText(geoid))
                  .setLatLng(e.latlng)
                  .openOn(Mapusaurus.map);
        });
        layer.on('mouseout', function(){
            Mapusaurus.map.closePopup();
        });
    },

    /* Depending on whether or not stats have been loaded, the hover text may
     * be different */
    tractHoverText: function(geoid) {
        var tract = Mapusaurus.dataStore.tract[geoid];
        if (_.has(tract, 'layer_minority')) {
            return (
                (1 - tract['layer_minority']['non_hisp_white_only_perc']) *
                100).toFixed() + '% "Minority"';
        } else {
            return 'Loading...';
        }
    },

    /*  We may have previously loaded stats data without the geos. Run through
     *  that data and see if the new geo data matches */
    updateDataWithoutGeos: function(newTracts) {
        var toDraw = {},  // geoids by layer
            undrawnData = Mapusaurus.dataWithoutGeo.tract;
        //  For each layer
        _.each(_.keys(undrawnData), function (layerName) {
            toDraw[layerName] = [];

            //  For each shape
            _.each(newTracts, function(geoid) {
                var geoProps = Mapusaurus.dataStore.tract[geoid];
                //  Check if the data can now be drawn
                if (_.has(undrawnData[layerName], geoid)) {
                    geoProps['layer_' + layerName] =
                        undrawnData[layerName][geoid];
                    toDraw[layerName].push(geoid);
                    delete undrawnData[layerName][geoid];
                }
            });
        });
        Mapusaurus.draw(toDraw);
    },

    /* We have geos without their associated stats - kick off the load */
    fetchMissingStats: function(newTracts) {
        //  This is a list of triples: [[layer name, state, county]]
        var missingStats = [];
        _.each(_.keys(Mapusaurus.layers.tract), function(layerName) {
            //  We only care about unseen stat data
            var missingData = _.filter(newTracts, function(geoid) {
                var geo = Mapusaurus.dataStore.tract[geoid],
                    stateCounty = geo.statefp +
                                  geo.countyfp;
                return !Mapusaurus.statsLoaded[layerName][stateCounty];
            });
            //  convert to state + county strings
            missingData = _.map(missingData, function(geoid) {
                var geo = Mapusaurus.dataStore.tract[geoid];
                return geo.statefp + geo.countyfp;
            });
            //  remove any duplicates; we end with what state/counties need to
            //  be retrieved
            missingData = _.uniq(missingData);

            //  Keep track of what we will be loading
            _.each(missingData, function(stateCounty) {
                //  Add to the list of data to load
                missingStats.push([layerName, stateCounty.substr(0, 2),
                                   stateCounty.substr(2)]);
                //  Signify that we are loading it...
                Mapusaurus.statsLoaded[layerName][stateCounty] = 'loading';
            });
        });
        if (missingStats.length > 0) {
            Mapusaurus.batchLoadStats(missingStats);
        }
    },

    /* We load stats data in one batch request. We need to provide the
     * endpoint/layer we care about, the data it needs (state, county, etc.),
     * and then we need to process the result. */
    batchLoadStats: function(missingStats) {
        var requests = _.map(missingStats, function(triple) {
            var params = {'state_fips': triple[1],
                          'county_fips': triple[2]};
            if (Mapusaurus.urlParam('lender')) {
                params['lender'] = Mapusaurus.urlParam('lender');
            }
            return {endpoint: triple[0], params: params};
        });

        $.ajax({
            type: 'POST',
            url: '/batch',
            contentType: 'application/json; charset=utf-8',
            data: JSON.stringify({requests: requests}),
            success: Mapusaurus.makeBatchSuccessFn(requests)
        });
    },

    /*  As the success function for making a batch request relies on the
     *  requests made, this returns a closure to handle the results of a batch
     *  load */
    makeBatchSuccessFn: function(requests) {
        return function(data) {
            var toDraw = {};
            _.each(_.keys(Mapusaurus.layers.tract), function(layerName) {
                toDraw[layerName] = [];
            });
            _.each(requests, function(request, idx) {
                var layerName = request['endpoint'],
                    llName = 'layer_' + layerName,
                    response = data['responses'][idx],
                    stateCounty = request.params['state_fips'] +
                                  request.params['county_fips'];
                _.each(_.keys(response), function(geoid) {
                    var geo = Mapusaurus.dataStore.tract[geoid];
                    //  Have not loaded the geo data yet
                    if (!geo) {
                        Mapusaurus.dataWithoutGeo.tract[layerName][geoid] =
                            response[geoid];
                    //  Have the geo data, but haven't drawn the stats yet
                    } else if (!geo[llName]) {
                        geo[llName] = response[geoid];
                        toDraw[layerName].push(geoid);
                    }
                });
                Mapusaurus.statsLoaded[layerName][stateCounty] = true;
            });
            Mapusaurus.draw(toDraw);
        };
    },

    /* Given a list of geo ids, segmented by layer name, add them to the
     * leaflet layer. */
    draw: function(layerToGeoids) {
        // For each layer
        _.each(_.keys(layerToGeoids), function(layerName) {
            var geoData = _.map(layerToGeoids[layerName], function(geoid) {
              return Mapusaurus.dataStore.tract[geoid];
            });
            switch(layerName) {
                case 'minority':
                    Mapusaurus.layers.tract.minority.geojsonLayer.setStyle(
                        Mapusaurus.minorityContinuousStyle);
                    break;
                case 'loanVolume':
                    _.each(geoData, function(geo) {
                        Mapusaurus.layers.tract.loanVolume.addLayer(
                            Mapusaurus.makeBubble(geo)
                        );
                    });
                    break;
            }
        });
    },

    /* Styles/extras for originations layer */
    makeBubble: function(geoProps) {
        var data = geoProps['layer_loanVolume'],
            circle = L.circle([geoProps.intptlat, geoProps.intptlon],
                              100 * data['volume_per_100_households'],
                              Mapusaurus.bubbleStyle);
        //  keep expected functionality with double clicking
        circle.on('dblclick', function(ev) {
            Mapusaurus.map.setZoomAround(
                ev.latlng, Mapusaurus.map.getZoom() + 1);
        });
        circle.on('mouseover mousemove', function(e){
            new L.Rrose({ offset: new L.Point(0,-10), closeButton: false, autoPan: false })
              .setContent(data['volume'] + ' loans<br />' + data['num_households'] + ' households')
              .setLatLng(e.latlng)
              .openOn(Mapusaurus.map);
        });
        circle.on('mouseout', function(){
            Mapusaurus.map.closePopup();
        });
        return circle;
    },

    //  Used to determine color within a gradient
    minorityContinuousStyle: function(feature) {
        return Mapusaurus.minorityStyle(
            feature,
            function(minorityPercent, bucket) {
                return (minorityPercent - bucket.lowerBound) / bucket.span;
            }
        );
    },
    //  Determines colors via distinct buckets
    minorityBucketedStyle: function(feature) {
        return Mapusaurus.minorityStyle(feature, function() { return 0.5; });
    },
    //  Shared function for minority styling; called by the two previous fns
    minorityStyle: function(feature, percentFn) {
        var geoid = feature.properties.geoid,
            tract = Mapusaurus.dataStore.tract[geoid];
        //  If we haven't loaded the data yet or the tract has zero people,
        //  don't draw it
        if (!_.has(tract, 'layer_minority') ||
            tract['layer_minority']['total_pop'] === 0) {
            return Mapusaurus.noStyle;
        } else {
            var minorityPercent = 1 - tract['layer_minority'][
                                'non_hisp_white_only_perc'],
                bucket = Mapusaurus.toBucket(minorityPercent),
                // convert given percentage to percents within bucket's bounds
                bucketPercent = percentFn(minorityPercent, bucket);
            return $.extend({}, Mapusaurus.tractStyle, {
                fillColor: Mapusaurus.colorFromPercent(bucketPercent,
                                                       bucket.colors)
            });
        }
    },

    colorRanges: [
        {
            span: 0.5,
            lowerBound: 0,
            colors: {
                lowR: 255,
                lowG: 255,
                lowB: 217,
                highR: 198,
                highG: 234,
                highB: 178
            }
        },
        {
            span: 0.3,
            lowerBound: 0.5,
            colors: {
                lowR: 198,
                lowG: 234,
                lowB: 178,
                highR: 14,
                highG: 144,
                highB: 194
            }
        },
        {
            span: 0.2,
            lowerBound: 0.8,
            colors: {
                lowR: 14,
                lowG: 144,
                lowB: 194,
                highR: 29,
                highG: 92,
                highB: 170
            }
        }
    ],

    toBucket: function(percent) {
        var i,
            len = Mapusaurus.colorRanges.length;
        for (i = 0; i < len - 1; i++) {
            //  Next bucket is too far
            if (Mapusaurus.colorRanges[i + 1].lowerBound > percent) {
                return Mapusaurus.colorRanges[i];
            }
        }
        return Mapusaurus.colorRanges[len - 1];  //  last color
    },

    /* Given low and high colors and a percent, figure out the RGB of said
     * percent in that scale */
    colorFromPercent: function(percent, c) {
        var diffR = (c.highR - c.lowR) * percent,
            diffG = (c.highG - c.lowG) * percent,
            diffB = (c.highB - c.lowB) * percent;
        return 'rgb(' + (c.lowR + diffR).toFixed() + ', ' +
               (c.lowG + diffG).toFixed() + ', ' +
               (c.lowB + diffB).toFixed() + ')';
    }
};
<|endoftext|>