<commit_before>

function Tree( name, id, root, parent ) { // root parameter only used internally

	if ( root === undefined ) {

		this.id = 0;
		this.maxId = 0;
		this.root = this;
		this.parent = null;

	} else {

		this.root = root;
		this.parent = parent;
		this.id =  ( id === null ) ? ++root.maxId : id;

	}

	this.name = name || '';
	this.children = [];

}

Tree.prototype.constructor = Tree;

Tree.prototype.traverse = function ( func ) {

	var children = this.children;

	func ( this );

	for ( var i = 0; i < children.length; i++ ) {

		children[ i ].traverse( func );

	}

};

Tree.prototype.traverseDepthFirst = function ( func ) {

	var children = this.children;

	for ( var i = 0; i < children.length; i++ ) {

		children[ i ].traverseDepthFirst( func );

	}

	func ( this );

};

Tree.prototype.forEachChild = function ( func, recurse ) {

	var children = this.children;
	var child;

	for ( var i = 0; i < children.length; i++ ) {

		child = children[ i ];

		func( child );

		if ( recurse === true ) child.forEachChild( func, true );

	}

};

Tree.prototype.addById = function ( name, id, parentId, properties ) {

	var parentNode = this.findById( parentId );

	if ( parentNode ) {

		var node = new Tree( name, id, this.root, parentNode );

		if ( properties !== undefined ) Object.assign( node, properties );

		parentNode.children.push ( node );

		var root = this.root;
		root.maxId = Math.max( root.maxId, id );

		return id;

	}

	return null;

};

Tree.prototype.findById = function ( id ) {

	if ( this.id == id ) return this;

	for ( var i = 0, l = this.children.length; i < l; i++ ) {

		var child = this.children[ i ];

		var found = child.findById( id );

		if ( found ) return found;

	}

	return undefined;

};

Tree.prototype.getByPath = function ( path ) {

	var pathArray = path.split( '.' );
	var node = this.getByPathArray( pathArray );

	return pathArray.length === 0 ? node : undefined;

};

Tree.prototype.getByPathArray = function ( path ) {

	var node  = this;
	var search = true;

	while ( search && path.length > 0 ) {

		search = false;

		for ( var i = 0, l = node.children.length; i < l; i++ ) {

			var child = node.children[ i ];

			if ( child.name === path[ 0 ] ) {

				node = child;
				path.shift();
				search = true;

				break;

			}

		}

	}

	return node;

};

Tree.prototype.addPath = function ( path, properties ) {

	var node;
	var newNode;

	// find part of path that exists already

	node = this.getByPathArray( path );

	if ( path.length === 0 ) return node.id;

	// add remainder of path to node

	while ( path.length > 0 ) {

		newNode = new Tree( path.shift(), null, this.root, node );

		node.children.push( newNode );
		node = newNode;

	}

	if ( properties !== undefined ) Object.assign( node, properties );

	return node.id;

};

Tree.prototype.getPath = function () {

	var node = this;
	var path = [];

	do {

		path.push( node.name );
		node = node.parent;

	} while ( node !== null );

	return path.reverse().join( '.' );

};

Tree.prototype.getSubtreeIds = function ( id, idSet ) {

	var node = this.findById( id );

	node.traverse( _getId );

	function _getId( node ) {

		idSet.add( node.id );

	}

};

Tree.prototype.getIdByPath = function ( path ) {

	var node = this.getByPathArray( path );

	if ( path.length === 0 ) {

		return node.id;

	} else {

		return undefined;

	}

};

export { Tree };

// EOF<commit_msg>whitespace fix<commit_after>

function Tree( name, id, root, parent ) { // root parameter only used internally

	if ( root === undefined ) {

		this.id = 0;
		this.maxId = 0;
		this.root = this;
		this.parent = null;

	} else {

		this.root = root;
		this.parent = parent;
		this.id = ( id === null ) ? ++root.maxId : id;

	}

	this.name = name || '';
	this.children = [];

}

Tree.prototype.constructor = Tree;

Tree.prototype.traverse = function ( func ) {

	var children = this.children;

	func ( this );

	for ( var i = 0; i < children.length; i++ ) {

		children[ i ].traverse( func );

	}

};

Tree.prototype.traverseDepthFirst = function ( func ) {

	var children = this.children;

	for ( var i = 0; i < children.length; i++ ) {

		children[ i ].traverseDepthFirst( func );

	}

	func ( this );

};

Tree.prototype.forEachChild = function ( func, recurse ) {

	var children = this.children;
	var child;

	for ( var i = 0; i < children.length; i++ ) {

		child = children[ i ];

		func( child );

		if ( recurse === true ) child.forEachChild( func, true );

	}

};

Tree.prototype.addById = function ( name, id, parentId, properties ) {

	var parentNode = this.findById( parentId );

	if ( parentNode ) {

		var node = new Tree( name, id, this.root, parentNode );

		if ( properties !== undefined ) Object.assign( node, properties );

		parentNode.children.push( node );

		var root = this.root;
		root.maxId = Math.max( root.maxId, id );

		return id;

	}

	return null;

};

Tree.prototype.findById = function ( id ) {

	if ( this.id == id ) return this;

	for ( var i = 0, l = this.children.length; i < l; i++ ) {

		var child = this.children[ i ];

		var found = child.findById( id );

		if ( found ) return found;

	}

	return undefined;

};

Tree.prototype.getByPath = function ( path ) {

	var pathArray = path.split( '.' );
	var node = this.getByPathArray( pathArray );

	return pathArray.length === 0 ? node : undefined;

};

Tree.prototype.getByPathArray = function ( path ) {

	var node  = this;
	var search = true;

	while ( search && path.length > 0 ) {

		search = false;

		for ( var i = 0, l = node.children.length; i < l; i++ ) {

			var child = node.children[ i ];

			if ( child.name === path[ 0 ] ) {

				node = child;
				path.shift();
				search = true;

				break;

			}

		}

	}

	return node;

};

Tree.prototype.addPath = function ( path, properties ) {

	var node;
	var newNode;

	// find part of path that exists already

	node = this.getByPathArray( path );

	if ( path.length === 0 ) return node.id;

	// add remainder of path to node

	while ( path.length > 0 ) {

		newNode = new Tree( path.shift(), null, this.root, node );

		node.children.push( newNode );
		node = newNode;

	}

	if ( properties !== undefined ) Object.assign( node, properties );

	return node.id;

};

Tree.prototype.getPath = function () {

	var node = this;
	var path = [];

	do {

		path.push( node.name );
		node = node.parent;

	} while ( node !== null );

	return path.reverse().join( '.' );

};

Tree.prototype.getSubtreeIds = function ( id, idSet ) {

	var node = this.findById( id );

	node.traverse( _getId );

	function _getId( node ) {

		idSet.add( node.id );

	}

};

Tree.prototype.getIdByPath = function ( path ) {

	var node = this.getByPathArray( path );

	if ( path.length === 0 ) {

		return node.id;

	} else {

		return undefined;

	}

};

export { Tree };

// EOF<|endoftext|>


<commit_before>/** Component for showing the queried index data as a set of graphs */

modulejs.define( "graphs-view", [
  "lib/lodash",
  "lib/jquery",
  "lib/d3",
  "lib/util",
  "preferences"
], function(
  _,
  $,
  D3,
  Util,
  Preferences
) {
  "use strict";

  var GraphView = function() {
    this.graphConf = {};
  };

  var SERIES_MARKER = {
    "":               "circle",
    "Detached":       "diamond",
    "SemiDetached":   "square",
    "Terraced":       "triangle-down",
    "FlatMaisonette": "triangle-up"
  };

  var oneDecimalPlace = D3.format( ".2g" );

  var GRAPHS_OPTIONS = {
    averagePrice: {
      cssClass: "average-price",
      ticksCount: 5,
      yDomain: "",
      graphType: "lineAndPoints"
    },
    housePriceIndex: {
      cssClass: "house-price-index",
      ticksCount: 5,
      yDomain: "",
      graphType: "lineAndPoints"
    },
    percentageChange: {
      cssClass: "percentage-monthly-change",
      ticksCount: 5,
      yDomain: "",
      graphType: "lineAndPoints",
      symmetricalYAxis: true,
      tickFormat: function( d) {return oneDecimalPlace( d ) + "%";}
    },
    percentageAnnualChange: {
      cssClass: "percentage-annual-change",
      ticksCount: 5,
      yDomain: "",
      graphType: "lineAndPoints",
      symmetricalYAxis: true,
      tickFormat: function( d) {return oneDecimalPlace( d ) + "%";}
    }
  };

  var GRAPH_PADDING = {
    top: 30,
    right: 20,
    bottom: 20,
    left: 80
  };
  GRAPH_PADDING.horizontal = GRAPH_PADDING.left + GRAPH_PADDING.right;
  GRAPH_PADDING.vertical = GRAPH_PADDING.top + GRAPH_PADDING.bottom;

  /** Width of graph bars, in pixels */
  var BAR_MARGIN = 1;
  var VISIBLE_BAR_WIDTH = 3;
  var BAR_WIDTH = VISIBLE_BAR_WIDTH + BAR_MARGIN;
  var HALF_BAR_WIDTH = BAR_WIDTH / 2;

  /** Minimum height of graph bars (so that zero values are still visible) */
  var MIN_BAR_HEIGHT = 2;

  /* Methods */

  _.extend( GraphView.prototype, {
    prefs: function() {
      return this.preferences;
    },

    resetGraphs: function( prefs ) {
      $(".js-graph").addClass( "hidden" );

      $(".c-graph-key li").addClass( "hidden" );
      _.each( prefs.categories(), function( category ) {
        $(".c-graph-key li.key-" + categoryCssClass( category )).removeClass( "hidden" );
      } );
    },

    showQueryResults: function( qr ) {
      var gv = this;
      var dateRange = qr.dateRange();
      var prefs = new Preferences();

      this.resetGraphs( prefs );

      _.each( prefs.indicators(), function( indicator ) {
        var options = GRAPHS_OPTIONS[indicator];
        if (options) {
          gv.graphConf[indicator] = {};
          var graphConf = gv.graphConf[indicator];

          graphConf.elem = revealGraphElem( options, qr.location() );
          graphConf.root = drawGraphRoot( graphConf );

          var valueRange = calculateValueRange( indicator, prefs, qr, options );
          _.merge( graphConf, configureAxes( graphConf, dateRange, valueRange, options ) );
          drawAxes( graphConf );
          drawGraph( indicator, prefs, qr, graphConf, options );
        }
      } );
    }
  } );

  /* Helper functions */

  var revealGraphElem = function( options, location ) {
    var selector = ".js-graph." + options.cssClass;
    $( selector )
      .removeClass( "hidden" )
      .find( "svg" )
      .empty();
    $( selector )
      .find( ".c-graph-heading span" )
      .empty()
      .text( ": " + location );
    return D3.select( selector + " svg" );
  };

  var configureAxes = function( graphConf, dateRange, valueRange, options ) {
    var scales = createScales( graphConf.elem );
    var axes = createAxes( scales, options, graphConf );

    setScaleDomain( scales, dateRange, valueRange );

    return {
      scales: scales,
      axes: axes
    };
  };

  var createScales = function( graphElem ) {
    var xScale = D3.time.scale().nice(D3.time.month);
    var yScale = D3.scale.linear();
    return setScaleViewDimensions( xScale, yScale, graphElem );
  };

  var setScaleViewDimensions = function( xScale, yScale, elem ) {
    var width = parseInt( elem.style("width") ) - GRAPH_PADDING.horizontal;
    var height = parseInt( elem.style("height") ) - GRAPH_PADDING.vertical;

    xScale.range( [0, width] );
    yScale.range( [height, 0] );

    return {x: xScale, y: yScale, width: width, height: height};
  };

  var createAxes = function( scales, options, graphConf ) {
    var xAxis = D3.svg.axis()
      .scale( scales.x )
      .orient("bottom")
      .tickFormat(D3.time.format("%b %Y"))
      .ticks( 8 );

    var yAxis = D3.svg.axis()
      .scale( scales.y )
      .orient("left")
      .ticks( options.ticksCount )
      .innerTickSize( -1 * scales.width );

    if (options.tickFormat) {
      yAxis = yAxis.tickFormat( options.tickFormat );
    }

    return {x: xAxis, y: yAxis};
  };

  var setScaleDomain = function( scales, dateRange, valueRange ) {
    scales.x.domain( dateRange ).nice();
    scales.y.domain( valueRange ).nice();
  };

  var calculateValueRange = function( indicator, prefs, qr, options ) {
    var aspects = aspectNames( indicator, prefs );
    var extents = _.map( aspects, function( aspect ) {
      return D3.extent( qr.results(), function( result ) {
        return result.value( aspect );
      } );
    } );

    var overallRange = D3.extent( _.flatten( extents ) );

    if (options.symmetricalYAxis) {
      var largest = _.max( _.map( overallRange, Math.abs ) );
      return [largest * -1.0, largest];
    }
    else {
      return [0, overallRange[1]];
    }
  };

  var aspectNames = function( indicator, prefs ) {
    var aspects = prefs.aspects( {indicators: [indicator]} );
    return _.map( aspects, function( a ) {return "ukhpi:" + a;} );
  };

  var drawGraphRoot = function( graphConf ) {
    return graphConf
      .elem
      .append("g")
      .attr("transform", "translate(" + GRAPH_PADDING.left + "," + GRAPH_PADDING.top + ")");
  };

  var drawAxes = function( graphConf ) {
    graphConf.root
      .append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + graphConf.scales.height + ")")
      .call( graphConf.axes.x );
    graphConf.root
      .append("g")
      .attr("class", "y axis")
      .call( graphConf.axes.y );
  };

  var categoryCssClass = function( categoryName ) {
    return Util.Text.unCamelCase( categoryName, "-" ).toLocaleLowerCase();
  };

  var drawGraph = function( indicator, prefs, qr, graphConf, options ) {
    switch (options.graphType) {
    case "lineAndPoints":
      drawPoints( indicator, prefs, qr, graphConf );
      drawLine( indicator, prefs, qr, graphConf );
      break;
    case "bars":
      drawBars( indicator, prefs, qr, graphConf );
      break;
    default:
      console.log( "Unknown graph type" );
    }
  };

  var collectedSeries = function( indicator, prefs, qr ) {
    var s = _.map( prefs.categories(), function( c, i ) {
      var series = qr.series( indicator, c );
      _.each( series, function( d ) {d.categoryIndex = i;} );

      return series;
    } );

    return _.flatten( s );
  };

  var drawPoints = function( indicator, prefs, qr, graphConf ) {
    var x = graphConf.scales.x;
    var y = graphConf.scales.y;
    var data = collectedSeries( indicator, prefs, qr );

    graphConf.root
      .selectAll(".point")
      .data( data )
      .enter()
      .append("path")
      .attr("class", function( d ) {return "point " + categoryCssClass( d.cat );} )
      .attr("d", function( d, i ) {return D3.svg.symbol().type( SERIES_MARKER[d.cat] )( d, i );} )
      .attr("transform", function(d) { return "translate(" + x(d.x) + "," + y(d.y) + ")"; });

  };

  var drawLine = function( indicator, prefs, qr, graphConf ) {
    var x = graphConf.scales.x;
    var y = graphConf.scales.y;

    var line = D3.svg
      .line()
      .x( function(d) { return x( d.x ); } )
      .y( function(d) { return y( d.y ); } );

    _.each( prefs.categories(), function( c ) {
      var s = qr.series( indicator, c );
      graphConf.root
        .append( "path" )
        .datum( s )
        .attr( "class", "line " + categoryCssClass( c ) )
        .attr( "d", line );
    } );
  };

  var drawBars = function( indicator, prefs, qr, graphConf ) {
    var x = graphConf.scales.x;
    var y = graphConf.scales.y;
    var y0 = y( 0 );

    var offsets = barOffsets( prefs.categories().length );
    var data = collectedSeries( indicator, prefs, qr );

    graphConf.root
      .selectAll( ".bar" )
      .data( data )
      .enter()
      .append( "rect" )
      .attr( "class", function( d ) {return "bar " + categoryCssClass( d.cat );} )
      .attr( "x", function( d ) { return x( d.x ) + offsets[d.categoryIndex]; })
      .attr( "y", function( d ) { return y( Math.max( 0, d.y ) ); })
      .attr( "width", VISIBLE_BAR_WIDTH )
      .attr( "height", function( d ) {return Math.max( MIN_BAR_HEIGHT, Math.abs( y( d.y ) - y0 ) );} );
  };

  /** @return An array of the offsets around 0 for n category bars */
  var barOffsets = function( nPrefs ) {
    var lower = (nPrefs - 1) * HALF_BAR_WIDTH * -1.0;
    var upper = nPrefs * HALF_BAR_WIDTH;
    return _.range( lower, upper, BAR_WIDTH );
  };

  return GraphView;
} );
<commit_msg>Only show one sample point per line to add visual distinction<commit_after>/** Component for showing the queried index data as a set of graphs */

modulejs.define( "graphs-view", [
  "lib/lodash",
  "lib/jquery",
  "lib/d3",
  "lib/util",
  "constants",
  "preferences"
], function(
  _,
  $,
  D3,
  Util,
  Constants,
  Preferences
) {
  "use strict";

  var GraphView = function() {
    this.graphConf = {};
  };

  var SERIES_MARKER = {
    "":               "circle",
    "Detached":       "diamond",
    "SemiDetached":   "square",
    "Terraced":       "triangle-down",
    "FlatMaisonette": "triangle-up"
  };

  var oneDecimalPlace = D3.format( ".2g" );

  var GRAPHS_OPTIONS = {
    averagePrice: {
      cssClass: "average-price",
      ticksCount: 5,
      yDomain: "",
      graphType: "lineAndPoints"
    },
    housePriceIndex: {
      cssClass: "house-price-index",
      ticksCount: 5,
      yDomain: "",
      graphType: "lineAndPoints"
    },
    percentageChange: {
      cssClass: "percentage-monthly-change",
      ticksCount: 5,
      yDomain: "",
      graphType: "lineAndPoints",
      symmetricalYAxis: true,
      tickFormat: function( d) {return oneDecimalPlace( d ) + "%";}
    },
    percentageAnnualChange: {
      cssClass: "percentage-annual-change",
      ticksCount: 5,
      yDomain: "",
      graphType: "lineAndPoints",
      symmetricalYAxis: true,
      tickFormat: function( d) {return oneDecimalPlace( d ) + "%";}
    }
  };

  var GRAPH_PADDING = {
    top: 30,
    right: 20,
    bottom: 20,
    left: 80
  };
  GRAPH_PADDING.horizontal = GRAPH_PADDING.left + GRAPH_PADDING.right;
  GRAPH_PADDING.vertical = GRAPH_PADDING.top + GRAPH_PADDING.bottom;

  /** Width of graph bars, in pixels */
  var BAR_MARGIN = 1;
  var VISIBLE_BAR_WIDTH = 3;
  var BAR_WIDTH = VISIBLE_BAR_WIDTH + BAR_MARGIN;
  var HALF_BAR_WIDTH = BAR_WIDTH / 2;

  /** Minimum height of graph bars (so that zero values are still visible) */
  var MIN_BAR_HEIGHT = 2;

  /* Methods */

  _.extend( GraphView.prototype, {
    prefs: function() {
      return this.preferences;
    },

    resetGraphs: function( prefs ) {
      $(".js-graph").addClass( "hidden" );

      $(".c-graph-key li").addClass( "hidden" );
      _.each( prefs.categories(), function( category ) {
        $(".c-graph-key li.key-" + categoryCssClass( category )).removeClass( "hidden" );
      } );
    },

    showQueryResults: function( qr ) {
      var gv = this;
      var dateRange = qr.dateRange();
      var prefs = new Preferences();

      this.resetGraphs( prefs );

      _.each( prefs.indicators(), function( indicator ) {
        var options = GRAPHS_OPTIONS[indicator];
        if (options) {
          gv.graphConf[indicator] = {};
          var graphConf = gv.graphConf[indicator];

          graphConf.elem = revealGraphElem( options, qr.location() );
          graphConf.root = drawGraphRoot( graphConf );

          var valueRange = calculateValueRange( indicator, prefs, qr, options );
          _.merge( graphConf, configureAxes( graphConf, dateRange, valueRange, options ) );
          drawAxes( graphConf );
          drawGraph( indicator, prefs, qr, graphConf, options );
        }
      } );
    }
  } );

  /* Helper functions */

  var revealGraphElem = function( options, location ) {
    var selector = ".js-graph." + options.cssClass;
    $( selector )
      .removeClass( "hidden" )
      .find( "svg" )
      .empty();
    $( selector )
      .find( ".c-graph-heading span" )
      .empty()
      .text( ": " + location );
    return D3.select( selector + " svg" );
  };

  var configureAxes = function( graphConf, dateRange, valueRange, options ) {
    var scales = createScales( graphConf.elem );
    var axes = createAxes( scales, options, graphConf );

    setScaleDomain( scales, dateRange, valueRange );

    return {
      scales: scales,
      axes: axes
    };
  };

  var createScales = function( graphElem ) {
    var xScale = D3.time.scale().nice(D3.time.month);
    var yScale = D3.scale.linear();
    return setScaleViewDimensions( xScale, yScale, graphElem );
  };

  var setScaleViewDimensions = function( xScale, yScale, elem ) {
    var width = parseInt( elem.style("width") ) - GRAPH_PADDING.horizontal;
    var height = parseInt( elem.style("height") ) - GRAPH_PADDING.vertical;

    xScale.range( [0, width] );
    yScale.range( [height, 0] );

    return {x: xScale, y: yScale, width: width, height: height};
  };

  var createAxes = function( scales, options ) {
    var xAxis = D3.svg.axis()
      .scale( scales.x )
      .orient("bottom")
      .tickFormat(D3.time.format("%b %Y"))
      .ticks( 8 );

    var yAxis = D3.svg.axis()
      .scale( scales.y )
      .orient("left")
      .ticks( options.ticksCount )
      .innerTickSize( -1 * scales.width );

    if (options.tickFormat) {
      yAxis = yAxis.tickFormat( options.tickFormat );
    }

    return {x: xAxis, y: yAxis};
  };

  var setScaleDomain = function( scales, dateRange, valueRange ) {
    scales.x.domain( dateRange ).nice();
    scales.y.domain( valueRange ).nice();
  };

  var calculateValueRange = function( indicator, prefs, qr, options ) {
    var aspects = aspectNames( indicator, prefs );
    var extents = _.map( aspects, function( aspect ) {
      return D3.extent( qr.results(), function( result ) {
        return result.value( aspect );
      } );
    } );

    var overallRange = D3.extent( _.flatten( extents ) );

    if (options.symmetricalYAxis) {
      var largest = _.max( _.map( overallRange, Math.abs ) );
      return [largest * -1.0, largest];
    }
    else {
      return [0, overallRange[1]];
    }
  };

  var aspectNames = function( indicator, prefs ) {
    var aspects = prefs.aspects( {indicators: [indicator]} );
    return _.map( aspects, function( a ) {return "ukhpi:" + a;} );
  };

  var drawGraphRoot = function( graphConf ) {
    return graphConf
      .elem
      .append("g")
      .attr("transform", "translate(" + GRAPH_PADDING.left + "," + GRAPH_PADDING.top + ")");
  };

  var drawAxes = function( graphConf ) {
    graphConf.root
      .append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + graphConf.scales.height + ")")
      .call( graphConf.axes.x );
    graphConf.root
      .append("g")
      .attr("class", "y axis")
      .call( graphConf.axes.y );
  };

  var categoryCssClass = function( categoryName ) {
    return Util.Text.unCamelCase( categoryName, "-" ).toLocaleLowerCase();
  };

  var drawGraph = function( indicator, prefs, qr, graphConf, options ) {
    switch (options.graphType) {
    case "lineAndPoints":
      drawPoints( indicator, prefs, qr, graphConf );
      drawLine( indicator, prefs, qr, graphConf );
      break;
    case "bars":
      drawBars( indicator, prefs, qr, graphConf );
      break;
    default:
      console.log( "Unknown graph type" );
    }
  };

  var collectedSeries = function( indicator, prefs, qr ) {
    var s = _.map( prefs.categories(), function( c, i ) {
      var series = qr.series( indicator, c );
      _.each( series, function( d ) {d.categoryIndex = i;} );

      return series;
    } );

    return _.flatten( s );
  };

  var sampledSeries = function( indicator, prefs, qr ) {
    var s = _.map( prefs.categories(), function( c, i ) {
      var series = qr.series( indicator, c );
      var datum = _.sample( series );
      datum.categoryIndex = i;

      return datum;
    } );

    return s;
  };

  var drawPoints = function( indicator, prefs, qr, graphConf ) {
    var x = graphConf.scales.x;
    var y = graphConf.scales.y;
    var data = sampledSeries( indicator, prefs, qr );

    graphConf.root
      .selectAll(".point")
      .data( data )
      .enter()
      .append("path")
      .attr("class", function( d ) {return "point " + categoryCssClass( d.cat );} )
      .attr("d", function( d, i ) {return D3.svg.symbol().type( SERIES_MARKER[d.cat] )( d, i );} )
      .attr("transform", function(d) { return "translate(" + x(d.x) + "," + y(d.y) + ")"; });

  };

  var drawLine = function( indicator, prefs, qr, graphConf ) {
    var x = graphConf.scales.x;
    var y = graphConf.scales.y;

    var line = D3.svg
      .line()
      .x( function(d) { return x( d.x ); } )
      .y( function(d) { return y( d.y ); } );

    _.each( prefs.categories(), function( c ) {
      var s = qr.series( indicator, c );
      graphConf.root
        .append( "path" )
        .datum( s )
        .attr( "class", "line " + categoryCssClass( c ) )
        .attr( "d", line );
    } );
  };

  var drawBars = function( indicator, prefs, qr, graphConf ) {
    var x = graphConf.scales.x;
    var y = graphConf.scales.y;
    var y0 = y( 0 );

    var offsets = barOffsets( prefs.categories().length );
    var data = collectedSeries( indicator, prefs, qr );

    graphConf.root
      .selectAll( ".bar" )
      .data( data )
      .enter()
      .append( "rect" )
      .attr( "class", function( d ) {return "bar " + categoryCssClass( d.cat );} )
      .attr( "x", function( d ) { return x( d.x ) + offsets[d.categoryIndex]; })
      .attr( "y", function( d ) { return y( Math.max( 0, d.y ) ); })
      .attr( "width", VISIBLE_BAR_WIDTH )
      .attr( "height", function( d ) {return Math.max( MIN_BAR_HEIGHT, Math.abs( y( d.y ) - y0 ) );} );
  };

  /** @return An array of the offsets around 0 for n category bars */
  var barOffsets = function( nPrefs ) {
    var lower = (nPrefs - 1) * HALF_BAR_WIDTH * -1.0;
    var upper = nPrefs * HALF_BAR_WIDTH;
    return _.range( lower, upper, BAR_WIDTH );
  };

  return GraphView;
} );
<|endoftext|>


<commit_before>import Container from '../mixin/container';
import Lazyload from '../mixin/lazyload';
import Position from '../mixin/position';
import Togglable from '../mixin/togglable';
import {
    addClass,
    append,
    apply,
    attr,
    css,
    hasClass,
    includes,
    isTouch,
    matches,
    MouseTracker,
    offset,
    on,
    once,
    parent,
    pointerCancel,
    pointerDown,
    pointerEnter,
    pointerLeave,
    pointerUp,
    query,
    removeClass,
    toggleClass,
    within,
} from 'uikit-util';

export let active;

export default {
    mixins: [Container, Lazyload, Position, Togglable],

    args: 'pos',

    props: {
        mode: 'list',
        toggle: Boolean,
        boundary: Boolean,
        boundaryAlign: Boolean,
        delayShow: Number,
        delayHide: Number,
        clsDrop: String,
    },

    data: {
        mode: ['click', 'hover'],
        toggle: '- *',
        boundary: true,
        boundaryAlign: false,
        delayShow: 0,
        delayHide: 800,
        clsDrop: false,
        animation: ['uk-animation-fade'],
        cls: 'uk-open',
        container: false,
    },

    created() {
        this.tracker = new MouseTracker();
    },

    connected() {
        this.clsPos = this.clsDrop = this.$props.clsDrop || `uk-${this.$options.name}`;
        addClass(this.$el, this.clsDrop);

        if (this.toggle && !this.target) {
            this.target = this.$create('toggle', query(this.toggle, this.$el), {
                target: this.$el,
                mode: this.mode,
            }).$el;
            attr(this.target, 'aria-haspopup', true);
            this.lazyload(this.target);
        }
    },

    disconnected() {
        if (this.isActive()) {
            active = null;
        }
    },

    events: [
        {
            name: 'click',

            delegate() {
                return `.${this.clsDrop}-close`;
            },

            handler(e) {
                e.preventDefault();
                this.hide(false);
            },
        },

        {
            name: 'click',

            delegate() {
                return 'a[href^="#"]';
            },

            handler({ defaultPrevented, current: { hash } }) {
                if (!defaultPrevented && hash && !within(hash, this.$el)) {
                    this.hide(false);
                }
            },
        },

        {
            name: 'beforescroll',

            handler() {
                this.hide(false);
            },
        },

        {
            name: 'toggle',

            self: true,

            handler(e, toggle) {
                e.preventDefault();

                if (this.isToggled()) {
                    this.hide(false);
                } else {
                    this.show(toggle?.$el, false);
                }
            },
        },

        {
            name: 'toggleshow',

            self: true,

            handler(e, toggle) {
                e.preventDefault();
                this.show(toggle?.$el);
            },
        },

        {
            name: 'togglehide',

            self: true,

            handler(e) {
                e.preventDefault();
                if (!matches(this.$el, ':focus,:hover')) {
                    this.hide();
                }
            },
        },

        {
            name: `${pointerEnter} focusin`,

            filter() {
                return includes(this.mode, 'hover');
            },

            handler(e) {
                if (!isTouch(e)) {
                    this.clearTimers();
                }
            },
        },

        {
            name: `${pointerLeave} focusout`,

            filter() {
                return includes(this.mode, 'hover');
            },

            handler(e) {
                if (!isTouch(e) && e.relatedTarget) {
                    this.hide();
                }
            },
        },

        {
            name: 'toggled',

            self: true,

            handler(e, toggled) {
                if (!toggled) {
                    return;
                }

                this.clearTimers();
                this.position();
            },
        },

        {
            name: 'show',

            self: true,

            handler() {
                active = this;

                this.tracker.init();

                for (const handler of [
                    on(
                        document,
                        pointerDown,
                        ({ target }) =>
                            !within(target, this.$el) &&
                            once(
                                document,
                                `${pointerUp} ${pointerCancel} scroll`,
                                ({ defaultPrevented, type, target: newTarget }) => {
                                    if (
                                        !defaultPrevented &&
                                        type === pointerUp &&
                                        target === newTarget &&
                                        !(this.target && within(target, this.target))
                                    ) {
                                        this.hide(false);
                                    }
                                },
                                true
                            )
                    ),

                    on(document, 'keydown', (e) => {
                        if (e.keyCode === 27) {
                            this.hide(false);
                        }
                    }),
                    on(window, 'resize', () => this.$emit('resize')),
                ]) {
                    once(this.$el, 'hide', handler, { self: true });
                }
            },
        },

        {
            name: 'beforehide',

            self: true,

            handler() {
                this.clearTimers();
            },
        },

        {
            name: 'hide',

            handler({ target }) {
                if (this.$el !== target) {
                    active =
                        active === null && within(target, this.$el) && this.isToggled()
                            ? this
                            : active;
                    return;
                }

                active = this.isActive() ? null : active;
                this.tracker.cancel();
            },
        },
    ],

    update: {
        write() {
            if (this.isToggled() && !hasClass(this.$el, this.clsEnter)) {
                this.position();
            }
        },

        events: ['resize'],
    },

    methods: {
        show(target = this.target, delay = true) {
            if (this.isToggled() && target && this.target && target !== this.target) {
                this.hide(false);
            }

            this.target = target;

            this.clearTimers();

            if (this.isActive()) {
                return;
            }

            if (active) {
                if (delay && active.isDelaying) {
                    this.showTimer = setTimeout(() => matches(target, ':hover') && this.show(), 10);
                    return;
                }

                let prev;
                while (active && prev !== active && !within(this.$el, active.$el)) {
                    prev = active;
                    active.hide(false);
                }
            }

            if (this.container && parent(this.$el) !== this.container) {
                append(this.container, this.$el);
            }

            this.showTimer = setTimeout(
                () => this.toggleElement(this.$el, true),
                (delay && this.delayShow) || 0
            );
        },

        hide(delay = true) {
            const hide = () => this.toggleElement(this.$el, false, false);

            this.clearTimers();

            this.isDelaying = getPositionedElements(this.$el).some((el) =>
                this.tracker.movesTo(el)
            );

            if (delay && this.isDelaying) {
                this.hideTimer = setTimeout(this.hide, 50);
            } else if (delay && this.delayHide) {
                this.hideTimer = setTimeout(hide, this.delayHide);
            } else {
                hide();
            }
        },

        clearTimers() {
            clearTimeout(this.showTimer);
            clearTimeout(this.hideTimer);
            this.showTimer = null;
            this.hideTimer = null;
            this.isDelaying = false;
        },

        isActive() {
            return active === this;
        },

        position() {
            const boundary = query(this.boundary, this.$el) || window;
            removeClass(this.$el, `${this.clsDrop}-stack`);
            toggleClass(this.$el, `${this.clsDrop}-boundary`, this.boundaryAlign);

            const boundaryOffset = offset(boundary);
            const targetOffset = offset(this.target);
            const alignTo = this.boundaryAlign ? boundaryOffset : targetOffset;

            if (this.align === 'justify') {
                const prop = this.getAxis() === 'y' ? 'width' : 'height';
                css(
                    this.$el,
                    prop,
                    (this.boundaryAlign || boundary !== window ? boundaryOffset : targetOffset)[
                        prop
                    ]
                );
            } else if (
                this.$el.offsetWidth >
                Math.max(boundaryOffset.right - alignTo.left, alignTo.right - boundaryOffset.left)
            ) {
                addClass(this.$el, `${this.clsDrop}-stack`);
            }

            this.positionAt(this.$el, this.boundaryAlign ? boundary : this.target, boundary);
        },
    },
};

function getPositionedElements(el) {
    const result = [];
    apply(el, (el) => css(el, 'position') !== 'static' && result.push(el));
    return result;
}
<commit_msg>revert: drops justify to boundary, except when boundary is the window<commit_after>import Container from '../mixin/container';
import Lazyload from '../mixin/lazyload';
import Position from '../mixin/position';
import Togglable from '../mixin/togglable';
import {
    addClass,
    append,
    apply,
    attr,
    css,
    hasClass,
    includes,
    isTouch,
    matches,
    MouseTracker,
    offset,
    on,
    once,
    parent,
    pointerCancel,
    pointerDown,
    pointerEnter,
    pointerLeave,
    pointerUp,
    query,
    removeClass,
    toggleClass,
    within,
} from 'uikit-util';

export let active;

export default {
    mixins: [Container, Lazyload, Position, Togglable],

    args: 'pos',

    props: {
        mode: 'list',
        toggle: Boolean,
        boundary: Boolean,
        boundaryAlign: Boolean,
        delayShow: Number,
        delayHide: Number,
        clsDrop: String,
    },

    data: {
        mode: ['click', 'hover'],
        toggle: '- *',
        boundary: true,
        boundaryAlign: false,
        delayShow: 0,
        delayHide: 800,
        clsDrop: false,
        animation: ['uk-animation-fade'],
        cls: 'uk-open',
        container: false,
    },

    created() {
        this.tracker = new MouseTracker();
    },

    connected() {
        this.clsPos = this.clsDrop = this.$props.clsDrop || `uk-${this.$options.name}`;
        addClass(this.$el, this.clsDrop);

        if (this.toggle && !this.target) {
            this.target = this.$create('toggle', query(this.toggle, this.$el), {
                target: this.$el,
                mode: this.mode,
            }).$el;
            attr(this.target, 'aria-haspopup', true);
            this.lazyload(this.target);
        }
    },

    disconnected() {
        if (this.isActive()) {
            active = null;
        }
    },

    events: [
        {
            name: 'click',

            delegate() {
                return `.${this.clsDrop}-close`;
            },

            handler(e) {
                e.preventDefault();
                this.hide(false);
            },
        },

        {
            name: 'click',

            delegate() {
                return 'a[href^="#"]';
            },

            handler({ defaultPrevented, current: { hash } }) {
                if (!defaultPrevented && hash && !within(hash, this.$el)) {
                    this.hide(false);
                }
            },
        },

        {
            name: 'beforescroll',

            handler() {
                this.hide(false);
            },
        },

        {
            name: 'toggle',

            self: true,

            handler(e, toggle) {
                e.preventDefault();

                if (this.isToggled()) {
                    this.hide(false);
                } else {
                    this.show(toggle?.$el, false);
                }
            },
        },

        {
            name: 'toggleshow',

            self: true,

            handler(e, toggle) {
                e.preventDefault();
                this.show(toggle?.$el);
            },
        },

        {
            name: 'togglehide',

            self: true,

            handler(e) {
                e.preventDefault();
                if (!matches(this.$el, ':focus,:hover')) {
                    this.hide();
                }
            },
        },

        {
            name: `${pointerEnter} focusin`,

            filter() {
                return includes(this.mode, 'hover');
            },

            handler(e) {
                if (!isTouch(e)) {
                    this.clearTimers();
                }
            },
        },

        {
            name: `${pointerLeave} focusout`,

            filter() {
                return includes(this.mode, 'hover');
            },

            handler(e) {
                if (!isTouch(e) && e.relatedTarget) {
                    this.hide();
                }
            },
        },

        {
            name: 'toggled',

            self: true,

            handler(e, toggled) {
                if (!toggled) {
                    return;
                }

                this.clearTimers();
                this.position();
            },
        },

        {
            name: 'show',

            self: true,

            handler() {
                active = this;

                this.tracker.init();

                for (const handler of [
                    on(
                        document,
                        pointerDown,
                        ({ target }) =>
                            !within(target, this.$el) &&
                            once(
                                document,
                                `${pointerUp} ${pointerCancel} scroll`,
                                ({ defaultPrevented, type, target: newTarget }) => {
                                    if (
                                        !defaultPrevented &&
                                        type === pointerUp &&
                                        target === newTarget &&
                                        !(this.target && within(target, this.target))
                                    ) {
                                        this.hide(false);
                                    }
                                },
                                true
                            )
                    ),

                    on(document, 'keydown', (e) => {
                        if (e.keyCode === 27) {
                            this.hide(false);
                        }
                    }),
                    on(window, 'resize', () => this.$emit('resize')),
                ]) {
                    once(this.$el, 'hide', handler, { self: true });
                }
            },
        },

        {
            name: 'beforehide',

            self: true,

            handler() {
                this.clearTimers();
            },
        },

        {
            name: 'hide',

            handler({ target }) {
                if (this.$el !== target) {
                    active =
                        active === null && within(target, this.$el) && this.isToggled()
                            ? this
                            : active;
                    return;
                }

                active = this.isActive() ? null : active;
                this.tracker.cancel();
            },
        },
    ],

    update: {
        write() {
            if (this.isToggled() && !hasClass(this.$el, this.clsEnter)) {
                this.position();
            }
        },

        events: ['resize'],
    },

    methods: {
        show(target = this.target, delay = true) {
            if (this.isToggled() && target && this.target && target !== this.target) {
                this.hide(false);
            }

            this.target = target;

            this.clearTimers();

            if (this.isActive()) {
                return;
            }

            if (active) {
                if (delay && active.isDelaying) {
                    this.showTimer = setTimeout(() => matches(target, ':hover') && this.show(), 10);
                    return;
                }

                let prev;
                while (active && prev !== active && !within(this.$el, active.$el)) {
                    prev = active;
                    active.hide(false);
                }
            }

            if (this.container && parent(this.$el) !== this.container) {
                append(this.container, this.$el);
            }

            this.showTimer = setTimeout(
                () => this.toggleElement(this.$el, true),
                (delay && this.delayShow) || 0
            );
        },

        hide(delay = true) {
            const hide = () => this.toggleElement(this.$el, false, false);

            this.clearTimers();

            this.isDelaying = getPositionedElements(this.$el).some((el) =>
                this.tracker.movesTo(el)
            );

            if (delay && this.isDelaying) {
                this.hideTimer = setTimeout(this.hide, 50);
            } else if (delay && this.delayHide) {
                this.hideTimer = setTimeout(hide, this.delayHide);
            } else {
                hide();
            }
        },

        clearTimers() {
            clearTimeout(this.showTimer);
            clearTimeout(this.hideTimer);
            this.showTimer = null;
            this.hideTimer = null;
            this.isDelaying = false;
        },

        isActive() {
            return active === this;
        },

        position() {
            const boundary = query(this.boundary, this.$el) || window;
            removeClass(this.$el, `${this.clsDrop}-stack`);
            toggleClass(this.$el, `${this.clsDrop}-boundary`, this.boundaryAlign);

            const boundaryOffset = offset(boundary);
            const targetOffset = offset(this.target);
            const alignTo = this.boundaryAlign ? boundaryOffset : targetOffset;

            if (this.align === 'justify') {
                const prop = this.getAxis() === 'y' ? 'width' : 'height';
                css(this.$el, prop, alignTo[prop]);
            } else if (
                this.$el.offsetWidth >
                Math.max(boundaryOffset.right - alignTo.left, alignTo.right - boundaryOffset.left)
            ) {
                addClass(this.$el, `${this.clsDrop}-stack`);
            }

            this.positionAt(this.$el, this.boundaryAlign ? boundary : this.target, boundary);
        },
    },
};

function getPositionedElements(el) {
    const result = [];
    apply(el, (el) => css(el, 'position') !== 'static' && result.push(el));
    return result;
}
<|endoftext|>


<commit_before>/** Component for showing the queried index data as a set of graphs */

modulejs.define( "graphs-view", [
  "lib/lodash",
  "lib/jquery",
  "lib/d3",
  "lib/util",
  "preferences",
  "aspects"
], function(
  _,
  $,
  D3,
  Util,
  Preferences,
  Aspects
) {
  "use strict";

  var GraphView = function() {
    this.preferences = new Preferences();
    this.graphConf = {};
  };

  var SERIES_MARKER = {
    "":               "circle",
    "Detached":       "diamond",
    "SemiDetached":   "square",
    "Terraced":       "triangle-down",
    "FlatMaisonette": "triangle-up"
  };

  var GRAPHS_OPTIONS = {
    averagePrice: {
      cssClass: "average-price",
      ticksCount: 5,
      yDomain: "",
      graphType: "lineAndPoints"
    },
    housePriceIndex: {
      cssClass: "house-price-index",
      ticksCount: 5,
      yDomain: "",
      graphType: "lineAndPoints"
    },
    percentageMonthlyChange: {
      cssClass: "percentage-monthly-change",
      ticksCount: 5,
      yDomain: "",
      graphType: "bars",
      symmetricalYAxis: true
    },
    percentageYearlyChange: {
      cssClass: "percentage-yearly-change",
      ticksCount: 5,
      yDomain: "",
      graphType: "bars",
      symmetricalYAxis: true
    }
  };

  var GRAPH_PADDING = {
    top: 30,
    right: 20,
    bottom: 20,
    left: 80
  };
  GRAPH_PADDING.horizontal = GRAPH_PADDING.left + GRAPH_PADDING.right;
  GRAPH_PADDING.vertical = GRAPH_PADDING.top + GRAPH_PADDING.bottom;

  /** Width of graph bars, in pixels */
  var BAR_MARGIN = 1;
  var VISIBLE_BAR_WIDTH = 3;;
  var BAR_WIDTH = VISIBLE_BAR_WIDTH + BAR_MARGIN;
  var HALF_BAR_WIDTH = BAR_WIDTH / 2;

  /** Minimum height of graph bars (so that zero values are still visible) */
  var MIN_BAR_HEIGHT = 2;

  /* Methods */

  _.extend( GraphView.prototype, {
    prefs: function() {
      return this.preferences;
    },

    showQueryResults: function( qr ) {
      var gv = this;
      var dateRange = qr.dateRange();

      _.each( this.prefs().indicators(), function( indicator ) {
        var options = GRAPHS_OPTIONS[indicator];
        if (options) {
          gv.graphConf[indicator] = {};
          var graphConf = gv.graphConf[indicator];

          graphConf.elem = revealGraphElem( options );
          graphConf.root = drawGraphRoot( graphConf );

          var valueRange = calculateValueRange( indicator, gv.prefs(), qr, options );
          _.merge( graphConf, configureAxes( graphConf, dateRange, valueRange, options ) );
          drawAxes( graphConf );
          drawGraph( indicator, gv.prefs(), qr, graphConf, options );
        }
      } );
    }
  } );

  /* Helper functions */

  var revealGraphElem = function( options ) {
    var selector = ".js-graph." + options.cssClass;
    $( selector )
      .removeClass( "hidden" )
      .find( "svg" )
      .empty();
    return D3.select( selector + " svg" );
  };

  var configureAxes = function( graphConf, dateRange, valueRange, options ) {
    var scales = createScales( graphConf.elem );
    var axes = createAxes( scales, options );

    setScaleDomain( scales, dateRange, valueRange );

    return {
      scales: scales,
      axes: axes
    };
  };

  var createScales = function( graphElem ) {
    var xScale = D3.time.scale().nice(D3.time.month);
    var yScale = D3.scale.linear();
    return setScaleViewDimensions( xScale, yScale, graphElem );
  };

  var setScaleViewDimensions = function( xScale, yScale, elem ) {
    var width = parseInt( elem.style("width") ) - GRAPH_PADDING.horizontal;
    var height = parseInt( elem.style("height") ) - GRAPH_PADDING.vertical;

    xScale.range( [0, width] );
    yScale.range( [height, 0] );

    return {x: xScale, y: yScale, width: width, height: height};
  };

  var createAxes = function( scales, options ) {
    var xAxis = D3.svg.axis()
      .scale( scales.x )
      .orient("bottom")
      .tickFormat(D3.time.format("%b %Y"))
      .ticks( 8 );

    var yAxis = D3.svg.axis()
      .scale( scales.y )
      .orient("left")
      .ticks( options.ticksCount );

    return {x: xAxis, y: yAxis};
  };

  var setScaleDomain = function( scales, dateRange, valueRange ) {
    scales.x.domain( dateRange ).nice();
    scales.y.domain( valueRange ).nice();
  };

  var calculateValueRange = function( indicator, prefs, qr, options ) {
    var aspects = aspectNames( indicator, prefs );
    var extents = _.map( aspects, function( aspect ) {
      return D3.extent( qr.results(), function( result ) {
        return result.value( aspect );
      } );
    } );

    var overallRange = D3.extent( _.flatten( extents ) );

    if (options.symmetricalYAxis) {
      var largest = _.max( _.map( overallRange, Math.abs ) );
      return [largest * -1.0, largest];
    }
    else {
      return [0, overallRange[1]];
    }
  };

  var aspectNames = function( indicator, prefs ) {
    var aspects = prefs.aspects( {indicators: [indicator]} );
    return _.map( aspects, function( a ) {return "ukhpi:" + a;} );
  };

  var drawGraphRoot = function( graphConf ) {
    return graphConf
      .elem
      .append("g")
      .attr("transform", "translate(" + GRAPH_PADDING.left + "," + GRAPH_PADDING.top + ")");
  };

  var drawAxes = function( graphConf ) {
    graphConf.root
      .append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + graphConf.scales.height + ")")
      .call( graphConf.axes.x );
    graphConf.root
      .append("g")
      .attr("class", "y axis")
      .call( graphConf.axes.y );
  };

  var categoryCssClass = function( categoryName ) {
    return Util.Text.unCamelCase( categoryName, "-" ).toLocaleLowerCase();
  };

  var drawGraph = function( indicator, prefs, qr, graphConf, options ) {
    switch (options.graphType) {
    case "lineAndPoints":
      drawPoints( indicator, prefs, qr, graphConf, options );
      drawLine( indicator, prefs, qr, graphConf );
      break;
    case "bars":
      drawBars( indicator, prefs, qr, graphConf );
      break;
    default:
      console.log( "Unknown graph type" );
    }
  };

  var collectedSeries = function( indicator, prefs, qr ) {
    var s = _.map( prefs.categories(), function( c, i ) {
      var series = qr.series( indicator, c );
      _.each( series, function( d ) {d.categoryIndex = i;} );

      return series;
    } );

    return _.flatten( s );
  };

  var drawPoints = function( indicator, prefs, qr, graphConf, options ) {
    var x = graphConf.scales.x;
    var y = graphConf.scales.y;
    var data = collectedSeries( indicator, prefs, qr );

    graphConf.root
      .selectAll(".point")
      .data( data )
      .enter()
      .append("path")
      .attr("class", function( d ) {return "point " + categoryCssClass( d.cat );} )
      .attr("d", function( d, i ) {return D3.svg.symbol().type( SERIES_MARKER[d.cat] )( d, i );} )
      .attr("transform", function(d) { return "translate(" + x(d.x) + "," + y(d.y) + ")"; });

  };

  var drawLine = function( indicator, prefs, qr, graphConf ) {
    var x = graphConf.scales.x;
    var y = graphConf.scales.y;

    var line = D3.svg
      .line()
      .x( function(d) { return x( d.x ); } )
      .y( function(d) { return y( d.y ); } );

    _.each( prefs.categories(), function( c ) {
      var s = qr.series( indicator, c );
      graphConf.root
        .append( "path" )
        .datum( s )
        .attr( "class", "line " + categoryCssClass( c ) )
        .attr( "d", line );
    } );
  };

  var drawBars = function( indicator, prefs, qr, graphConf ) {
    var x = graphConf.scales.x;
    var y = graphConf.scales.y;
    var y0 = y( 0 );

    var offsets = barOffsets( prefs.categories().length );
    var data = collectedSeries( indicator, prefs, qr );

    graphConf.root
      .selectAll( ".bar" )
      .data( data )
      .enter()
      .append( "rect" )
      .attr( "class", function( d ) {return "bar " + categoryCssClass( d.cat );} )
      .attr( "x", function( d ) { return x( d.x ) + offsets[d.categoryIndex]; })
      .attr( "y", function( d ) { return y( Math.max( 0, d.y ) ); })
      .attr( "width", VISIBLE_BAR_WIDTH )
      .attr( "height", function( d ) {return Math.max( MIN_BAR_HEIGHT, Math.abs( y( d.y ) - y0 ) );} );
  };

  /** @return An array of the offsets around 0 for n category bars */
  var barOffsets = function( nPrefs ) {
    var lower = (nPrefs - 1) * HALF_BAR_WIDTH * -1.0;
    var upper = nPrefs * HALF_BAR_WIDTH;
    return _.range( lower, upper, BAR_WIDTH );
  };

  return GraphView;
} );
<commit_msg>Hide de-selected graphs<commit_after>/** Component for showing the queried index data as a set of graphs */

modulejs.define( "graphs-view", [
  "lib/lodash",
  "lib/jquery",
  "lib/d3",
  "lib/util",
  "preferences",
  "aspects"
], function(
  _,
  $,
  D3,
  Util,
  Preferences,
  Aspects
) {
  "use strict";

  var GraphView = function() {
    this.graphConf = {};
  };

  var SERIES_MARKER = {
    "":               "circle",
    "Detached":       "diamond",
    "SemiDetached":   "square",
    "Terraced":       "triangle-down",
    "FlatMaisonette": "triangle-up"
  };

  var GRAPHS_OPTIONS = {
    averagePrice: {
      cssClass: "average-price",
      ticksCount: 5,
      yDomain: "",
      graphType: "lineAndPoints"
    },
    housePriceIndex: {
      cssClass: "house-price-index",
      ticksCount: 5,
      yDomain: "",
      graphType: "lineAndPoints"
    },
    percentageMonthlyChange: {
      cssClass: "percentage-monthly-change",
      ticksCount: 5,
      yDomain: "",
      graphType: "bars",
      symmetricalYAxis: true
    },
    percentageYearlyChange: {
      cssClass: "percentage-yearly-change",
      ticksCount: 5,
      yDomain: "",
      graphType: "bars",
      symmetricalYAxis: true
    }
  };

  var GRAPH_PADDING = {
    top: 30,
    right: 20,
    bottom: 20,
    left: 80
  };
  GRAPH_PADDING.horizontal = GRAPH_PADDING.left + GRAPH_PADDING.right;
  GRAPH_PADDING.vertical = GRAPH_PADDING.top + GRAPH_PADDING.bottom;

  /** Width of graph bars, in pixels */
  var BAR_MARGIN = 1;
  var VISIBLE_BAR_WIDTH = 3;;
  var BAR_WIDTH = VISIBLE_BAR_WIDTH + BAR_MARGIN;
  var HALF_BAR_WIDTH = BAR_WIDTH / 2;

  /** Minimum height of graph bars (so that zero values are still visible) */
  var MIN_BAR_HEIGHT = 2;

  /* Methods */

  _.extend( GraphView.prototype, {
    prefs: function() {
      return this.preferences;
    },

    resetGraphs: function() {
      $(".js-graph").addClass( "hidden" );
    },

    showQueryResults: function( qr ) {
      var gv = this;
      var dateRange = qr.dateRange();
      var prefs = new Preferences();
      this.resetGraphs();

      _.each( prefs.indicators(), function( indicator ) {
        var options = GRAPHS_OPTIONS[indicator];
        if (options) {
          gv.graphConf[indicator] = {};
          var graphConf = gv.graphConf[indicator];

          graphConf.elem = revealGraphElem( options );
          graphConf.root = drawGraphRoot( graphConf );

          var valueRange = calculateValueRange( indicator, prefs, qr, options );
          _.merge( graphConf, configureAxes( graphConf, dateRange, valueRange, options ) );
          drawAxes( graphConf );
          drawGraph( indicator, prefs, qr, graphConf, options );
        }
      } );
    }
  } );

  /* Helper functions */

  var revealGraphElem = function( options ) {
    var selector = ".js-graph." + options.cssClass;
    $( selector )
      .removeClass( "hidden" )
      .find( "svg" )
      .empty();
    return D3.select( selector + " svg" );
  };

  var configureAxes = function( graphConf, dateRange, valueRange, options ) {
    var scales = createScales( graphConf.elem );
    var axes = createAxes( scales, options );

    setScaleDomain( scales, dateRange, valueRange );

    return {
      scales: scales,
      axes: axes
    };
  };

  var createScales = function( graphElem ) {
    var xScale = D3.time.scale().nice(D3.time.month);
    var yScale = D3.scale.linear();
    return setScaleViewDimensions( xScale, yScale, graphElem );
  };

  var setScaleViewDimensions = function( xScale, yScale, elem ) {
    var width = parseInt( elem.style("width") ) - GRAPH_PADDING.horizontal;
    var height = parseInt( elem.style("height") ) - GRAPH_PADDING.vertical;

    xScale.range( [0, width] );
    yScale.range( [height, 0] );

    return {x: xScale, y: yScale, width: width, height: height};
  };

  var createAxes = function( scales, options ) {
    var xAxis = D3.svg.axis()
      .scale( scales.x )
      .orient("bottom")
      .tickFormat(D3.time.format("%b %Y"))
      .ticks( 8 );

    var yAxis = D3.svg.axis()
      .scale( scales.y )
      .orient("left")
      .ticks( options.ticksCount );

    return {x: xAxis, y: yAxis};
  };

  var setScaleDomain = function( scales, dateRange, valueRange ) {
    scales.x.domain( dateRange ).nice();
    scales.y.domain( valueRange ).nice();
  };

  var calculateValueRange = function( indicator, prefs, qr, options ) {
    var aspects = aspectNames( indicator, prefs );
    var extents = _.map( aspects, function( aspect ) {
      return D3.extent( qr.results(), function( result ) {
        return result.value( aspect );
      } );
    } );

    var overallRange = D3.extent( _.flatten( extents ) );

    if (options.symmetricalYAxis) {
      var largest = _.max( _.map( overallRange, Math.abs ) );
      return [largest * -1.0, largest];
    }
    else {
      return [0, overallRange[1]];
    }
  };

  var aspectNames = function( indicator, prefs ) {
    var aspects = prefs.aspects( {indicators: [indicator]} );
    return _.map( aspects, function( a ) {return "ukhpi:" + a;} );
  };

  var drawGraphRoot = function( graphConf ) {
    return graphConf
      .elem
      .append("g")
      .attr("transform", "translate(" + GRAPH_PADDING.left + "," + GRAPH_PADDING.top + ")");
  };

  var drawAxes = function( graphConf ) {
    graphConf.root
      .append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + graphConf.scales.height + ")")
      .call( graphConf.axes.x );
    graphConf.root
      .append("g")
      .attr("class", "y axis")
      .call( graphConf.axes.y );
  };

  var categoryCssClass = function( categoryName ) {
    return Util.Text.unCamelCase( categoryName, "-" ).toLocaleLowerCase();
  };

  var drawGraph = function( indicator, prefs, qr, graphConf, options ) {
    switch (options.graphType) {
    case "lineAndPoints":
      drawPoints( indicator, prefs, qr, graphConf, options );
      drawLine( indicator, prefs, qr, graphConf );
      break;
    case "bars":
      drawBars( indicator, prefs, qr, graphConf );
      break;
    default:
      console.log( "Unknown graph type" );
    }
  };

  var collectedSeries = function( indicator, prefs, qr ) {
    var s = _.map( prefs.categories(), function( c, i ) {
      var series = qr.series( indicator, c );
      _.each( series, function( d ) {d.categoryIndex = i;} );

      return series;
    } );

    return _.flatten( s );
  };

  var drawPoints = function( indicator, prefs, qr, graphConf, options ) {
    var x = graphConf.scales.x;
    var y = graphConf.scales.y;
    var data = collectedSeries( indicator, prefs, qr );

    graphConf.root
      .selectAll(".point")
      .data( data )
      .enter()
      .append("path")
      .attr("class", function( d ) {return "point " + categoryCssClass( d.cat );} )
      .attr("d", function( d, i ) {return D3.svg.symbol().type( SERIES_MARKER[d.cat] )( d, i );} )
      .attr("transform", function(d) { return "translate(" + x(d.x) + "," + y(d.y) + ")"; });

  };

  var drawLine = function( indicator, prefs, qr, graphConf ) {
    var x = graphConf.scales.x;
    var y = graphConf.scales.y;

    var line = D3.svg
      .line()
      .x( function(d) { return x( d.x ); } )
      .y( function(d) { return y( d.y ); } );

    _.each( prefs.categories(), function( c ) {
      var s = qr.series( indicator, c );
      graphConf.root
        .append( "path" )
        .datum( s )
        .attr( "class", "line " + categoryCssClass( c ) )
        .attr( "d", line );
    } );
  };

  var drawBars = function( indicator, prefs, qr, graphConf ) {
    var x = graphConf.scales.x;
    var y = graphConf.scales.y;
    var y0 = y( 0 );

    var offsets = barOffsets( prefs.categories().length );
    var data = collectedSeries( indicator, prefs, qr );

    graphConf.root
      .selectAll( ".bar" )
      .data( data )
      .enter()
      .append( "rect" )
      .attr( "class", function( d ) {return "bar " + categoryCssClass( d.cat );} )
      .attr( "x", function( d ) { return x( d.x ) + offsets[d.categoryIndex]; })
      .attr( "y", function( d ) { return y( Math.max( 0, d.y ) ); })
      .attr( "width", VISIBLE_BAR_WIDTH )
      .attr( "height", function( d ) {return Math.max( MIN_BAR_HEIGHT, Math.abs( y( d.y ) - y0 ) );} );
  };

  /** @return An array of the offsets around 0 for n category bars */
  var barOffsets = function( nPrefs ) {
    var lower = (nPrefs - 1) * HALF_BAR_WIDTH * -1.0;
    var upper = nPrefs * HALF_BAR_WIDTH;
    return _.range( lower, upper, BAR_WIDTH );
  };

  return GraphView;
} );
<|endoftext|>


<commit_before>import Logger from '../utils/logger.js'

export default (state) => {
  const expRequired = (level) => Math.pow(level + 1, 2.6) * 100

  const gainExp = (state, xp) => {
    state.totalXp += xp
    if (state.totalXpToLevelUp <= state.totalXp) {
      state.currentXp = Math.abs(state.totalXpToLevelUp - state.totalXp)
      levelUp(state)
      state.xpToLevelUp = expRequired(state.level) - state.totalXp
    } else {
      state.currentXp += xp
    }

    return state
  }

  const levelUp = (state) => {
    state.level++
    state.damage++
    state.totalXpToLevelUp = expRequired(state.level)
    Logger.log(`gz you are level ${state.level}`)

    return state
  }

  const hit = (state) => {
    Logger.log(`hit monster for ${state.damage} damage.`)
    state.monster.life = state.monster.life - state.damage
    Logger.log(`monster life's : ${state.monster.life} hp.`)

    return state
  }

  const gainRewards = (state, rewards) => {
    state = gainExp(state, rewards.xp)
    state.chest.gold += rewards.chest.gold
    state.chest.items = state.chest.items.concat(rewards.chest.items)
    Logger.log(rewards.chest.items)
    Logger.log(`New gold value ${state.chest.gold}`)

    return state
  }

  const initialState = (state) => {
    return Object.assign({
      pseudo: '',
      ias: 10,
      damage: 1,
      chest: {
        gold: 0,
        items: []
      },
      level: 1,
      totalXp: 0,
      currentXp: 0,
      xpToLevelUp: 100,
      totalXpToLevelUp: 100,
      monster: {
        life: 5
      }
    }, state)
  }

  return { gainRewards, hit, gainExp, expRequired, initialState }
}
<commit_msg>Round xp needed to level up<commit_after>import Logger from '../utils/logger.js'

export default (state) => {
  const expRequired = (level) => Math.round(Math.pow(level + 1, 2.6) * 100)

  const gainExp = (state, xp) => {
    state.totalXp += xp
    if (state.totalXpToLevelUp <= state.totalXp) {
      state.currentXp = Math.abs(state.totalXpToLevelUp - state.totalXp)
      levelUp(state)
      state.xpToLevelUp = expRequired(state.level) - state.totalXp
    } else {
      state.currentXp += xp
    }

    return state
  }

  const levelUp = (state) => {
    state.level++
    state.damage++
    state.totalXpToLevelUp = expRequired(state.level)
    Logger.log(`gz you are level ${state.level}`)

    return state
  }

  const hit = (state) => {
    Logger.log(`hit monster for ${state.damage} damage.`)
    state.monster.life = state.monster.life - state.damage
    Logger.log(`monster life's : ${state.monster.life} hp.`)

    return state
  }

  const gainRewards = (state, rewards) => {
    state = gainExp(state, rewards.xp)
    state.chest.gold += rewards.chest.gold
    state.chest.items = state.chest.items.concat(rewards.chest.items)
    Logger.log(rewards.chest.items)
    Logger.log(`New gold value ${state.chest.gold}`)

    return state
  }

  const initialState = (state) => {
    return Object.assign({
      pseudo: '',
      ias: 10,
      damage: 1,
      chest: {
        gold: 0,
        items: []
      },
      level: 1,
      totalXp: 0,
      currentXp: 0,
      xpToLevelUp: 100,
      totalXpToLevelUp: 100,
      monster: {
        life: 5
      }
    }, state)
  }

  return { gainRewards, hit, gainExp, expRequired, initialState }
}
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import assign from 'object-assign';

import { showMenu, hideMenu } from './actions';
import { callIfExists, cssClasses } from './helpers';

export default class ContextMenuTrigger extends Component {
    static propTypes = {
        id: PropTypes.string.isRequired,
        children: PropTypes.node.isRequired,
        attributes: PropTypes.object,
        collect: PropTypes.func,
        disable: PropTypes.bool,
        holdToDisplay: PropTypes.number,
        posX: PropTypes.number,
        posY: PropTypes.number,
        renderTag: PropTypes.oneOfType([
            PropTypes.node,
            PropTypes.func
        ]),
        mouseButton: PropTypes.number,
        disableIfShiftIsPressed: PropTypes.bool
    };

    static defaultProps = {
        attributes: {},
        collect() { return null; },
        disable: false,
        holdToDisplay: 1000,
        renderTag: 'div',
        posX: 0,
        posY: 0,
        mouseButton: 2, // 0 is left click, 2 is right click
        disableIfShiftIsPressed: false
    };

    touchHandled = false;

    handleMouseDown = (event) => {
        if (this.props.holdToDisplay >= 0 && event.button === 0) {
            event.persist();
            event.stopPropagation();

            this.mouseDownTimeoutId = setTimeout(
                () => this.handleContextClick(event),
                this.props.holdToDisplay
            );
        }
        callIfExists(this.props.attributes.onMouseDown, event);
    }

    handleMouseUp = (event) => {
        if (event.button === 0) {
            clearTimeout(this.mouseDownTimeoutId);
        }
        callIfExists(this.props.attributes.onMouseUp, event);
    }

    handleMouseOut = (event) => {
        if (event.button === 0) {
            clearTimeout(this.mouseDownTimeoutId);
        }
        callIfExists(this.props.attributes.onMouseOut, event);
    }

    handleTouchstart = (event) => {
        this.touchHandled = false;

        if (this.props.holdToDisplay >= 0) {
            event.persist();
            event.stopPropagation();

            this.touchstartTimeoutId = setTimeout(
                () => {
                    this.handleContextClick(event);
                    this.touchHandled = true;
                },
                this.props.holdToDisplay
            );
        }
        callIfExists(this.props.attributes.onTouchStart, event);
    }

    handleTouchEnd = (event) => {
        if (this.touchHandled) {
            event.preventDefault();
        }
        clearTimeout(this.touchstartTimeoutId);
        callIfExists(this.props.attributes.onTouchEnd, event);
    }

    handleContextMenu = (event) => {
        if (event.button === this.props.mouseButton) {
            this.handleContextClick(event);
        }
        callIfExists(this.props.attributes.onContextMenu, event);
    }

    handleMouseClick = (event) => {
        if (event.button === this.props.mouseButton) {
            this.handleContextClick(event);
        }
        callIfExists(this.props.attributes.onClick, event);
    }

    handleContextClick = (event) => {
        if (this.props.disable) return;
        if (this.props.disableIfShiftIsPressed && event.shiftKey) return;

        event.preventDefault();
        event.stopPropagation();

        let x = event.clientX || (event.touches && event.touches[0].pageX);
        let y = event.clientY || (event.touches && event.touches[0].pageY);

        if (this.props.posX) {
            x -= this.props.posX;
        }
        if (this.props.posY) {
            y -= this.props.posY;
        }

        hideMenu();

        let data = callIfExists(this.props.collect, this.props);
        let showMenuConfig = {
            position: { x, y },
            target: this.elem,
            id: this.props.id
        };
        if (data && (typeof data.then === 'function')) {
            // it's promise
            data.then((resp) => {
                showMenuConfig.data = assign({}, resp, {
                    target: event.target
                });
                showMenu(showMenuConfig);
            });
        } else {
            showMenuConfig.data = assign({}, data, {
                target: event.target
            });
            showMenu(showMenuConfig);
        }
    }

    elemRef = (c) => {
        this.elem = c;
    }

    render() {
        const { renderTag, attributes, children } = this.props;
        const newAttrs = assign({}, attributes, {
            className: cx(cssClasses.menuWrapper, attributes.className),
            onContextMenu: this.handleContextMenu,
            onClick: this.handleMouseClick,
            onMouseDown: this.handleMouseDown,
            onMouseUp: this.handleMouseUp,
            onTouchStart: this.handleTouchstart,
            onTouchEnd: this.handleTouchEnd,
            onMouseOut: this.handleMouseOut,
            ref: this.elemRef
        });

        return React.createElement(renderTag, newAttrs, children);
    }
}
<commit_msg>Prop type of renderTag changed to elementType. Fixes #300 (#301)<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import assign from 'object-assign';

import { showMenu, hideMenu } from './actions';
import { callIfExists, cssClasses } from './helpers';

export default class ContextMenuTrigger extends Component {
    static propTypes = {
        id: PropTypes.string.isRequired,
        children: PropTypes.node.isRequired,
        attributes: PropTypes.object,
        collect: PropTypes.func,
        disable: PropTypes.bool,
        holdToDisplay: PropTypes.number,
        posX: PropTypes.number,
        posY: PropTypes.number,
        renderTag: PropTypes.elementType,
        mouseButton: PropTypes.number,
        disableIfShiftIsPressed: PropTypes.bool
    };

    static defaultProps = {
        attributes: {},
        collect() { return null; },
        disable: false,
        holdToDisplay: 1000,
        renderTag: 'div',
        posX: 0,
        posY: 0,
        mouseButton: 2, // 0 is left click, 2 is right click
        disableIfShiftIsPressed: false
    };

    touchHandled = false;

    handleMouseDown = (event) => {
        if (this.props.holdToDisplay >= 0 && event.button === 0) {
            event.persist();
            event.stopPropagation();

            this.mouseDownTimeoutId = setTimeout(
                () => this.handleContextClick(event),
                this.props.holdToDisplay
            );
        }
        callIfExists(this.props.attributes.onMouseDown, event);
    }

    handleMouseUp = (event) => {
        if (event.button === 0) {
            clearTimeout(this.mouseDownTimeoutId);
        }
        callIfExists(this.props.attributes.onMouseUp, event);
    }

    handleMouseOut = (event) => {
        if (event.button === 0) {
            clearTimeout(this.mouseDownTimeoutId);
        }
        callIfExists(this.props.attributes.onMouseOut, event);
    }

    handleTouchstart = (event) => {
        this.touchHandled = false;

        if (this.props.holdToDisplay >= 0) {
            event.persist();
            event.stopPropagation();

            this.touchstartTimeoutId = setTimeout(
                () => {
                    this.handleContextClick(event);
                    this.touchHandled = true;
                },
                this.props.holdToDisplay
            );
        }
        callIfExists(this.props.attributes.onTouchStart, event);
    }

    handleTouchEnd = (event) => {
        if (this.touchHandled) {
            event.preventDefault();
        }
        clearTimeout(this.touchstartTimeoutId);
        callIfExists(this.props.attributes.onTouchEnd, event);
    }

    handleContextMenu = (event) => {
        if (event.button === this.props.mouseButton) {
            this.handleContextClick(event);
        }
        callIfExists(this.props.attributes.onContextMenu, event);
    }

    handleMouseClick = (event) => {
        if (event.button === this.props.mouseButton) {
            this.handleContextClick(event);
        }
        callIfExists(this.props.attributes.onClick, event);
    }

    handleContextClick = (event) => {
        if (this.props.disable) return;
        if (this.props.disableIfShiftIsPressed && event.shiftKey) return;

        event.preventDefault();
        event.stopPropagation();

        let x = event.clientX || (event.touches && event.touches[0].pageX);
        let y = event.clientY || (event.touches && event.touches[0].pageY);

        if (this.props.posX) {
            x -= this.props.posX;
        }
        if (this.props.posY) {
            y -= this.props.posY;
        }

        hideMenu();

        let data = callIfExists(this.props.collect, this.props);
        let showMenuConfig = {
            position: { x, y },
            target: this.elem,
            id: this.props.id
        };
        if (data && (typeof data.then === 'function')) {
            // it's promise
            data.then((resp) => {
                showMenuConfig.data = assign({}, resp, {
                    target: event.target
                });
                showMenu(showMenuConfig);
            });
        } else {
            showMenuConfig.data = assign({}, data, {
                target: event.target
            });
            showMenu(showMenuConfig);
        }
    }

    elemRef = (c) => {
        this.elem = c;
    }

    render() {
        const { renderTag, attributes, children } = this.props;
        const newAttrs = assign({}, attributes, {
            className: cx(cssClasses.menuWrapper, attributes.className),
            onContextMenu: this.handleContextMenu,
            onClick: this.handleMouseClick,
            onMouseDown: this.handleMouseDown,
            onMouseUp: this.handleMouseUp,
            onTouchStart: this.handleTouchstart,
            onTouchEnd: this.handleTouchEnd,
            onMouseOut: this.handleMouseOut,
            ref: this.elemRef
        });

        return React.createElement(renderTag, newAttrs, children);
    }
}
<|endoftext|>