block by zanarmstrong 15558afddc79a52847bc

Weather Line Chart w/ alternate to tooltip

Full Screen

404: Not Found

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="weatherHourly.css">
</head>
<body>
  	<select id="metric" name="Select Metric:">
		<option value="cloudCover" selected>Percent Cloudy</option>
    <option value="normalTemperature">Temperature</option>
    <option value="heatIndex">Temperature with Heat Index</option>
    <option value="windChill">Temp with Wind Chill</option>
    <option value="aveWindSpeed">Ave Wind Speed</option>
	</select>
  <p id="citySelection"></p>
	<div id="instructions">
		<p><span id="info">The graph shows the normal temperature, percent cloudy skies, or other metric in a particular city by day of the year and hour of the day, based on the last 30 years of data. Each line represents a different hour of the day. Click on a line or on the legend to highlight a particular hour.</span></p>
		<p><span id="info">Data from <a href="https://gis.ncdc.noaa.gov/geoportal/catalog/search/resource/details.page?id=gov.noaa.ncdc:C00824">NOAA</a></span></p>
	</div>
	<section id="weatherLines">
		<p></p>
	</section>
  <script src="//d3js.org/d3.v3.min.js"></script>
  <script src="//d3js.org/topojson.v1.min.js"></script>
  <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.8.4/moment.min.js"></script>
  <script src="viz.js"></script>
  <script src="state.js"></script>
  <script src="data.js"></script>
  <script src="selectionViz.js"></script>
  <script src="weatherHourly.js"></script>
</body>

data.js

/////////////////////////////
//  Manage Data
/////////////////////////////
function dataObj() {
  this.inputData = {};
  this.filteredData = {};
  this.pathData = [];
};

dataObj.prototype.updateData = function(data, state) {
  this.inputData = data;
  self = this;

  for (var i = 0; i < 24; i++) {
    this.pathData[i] = [];
  };
  this.filteredData = this.inputData
                          .filter(function(d,i) {
                              if (d.city == state.getCity()) {
                                self.pathData[+d.hour][moment(d.day).dayOfYear() - 1] = d[state.getMetric()];
                                return d;
                          }});
}

dataObj.prototype.updateState = function(state) {
  self = this;

  for (var i = 0; i < 24; i++) {
    this.pathData[i] = [];
  };

  this.filteredData = this.inputData
                          .filter(function(d,i) {
                              if (d.city == state.getCity()) {
                                self.pathData[+d.hour][moment(d.day).dayOfYear() - 1] = d[state.getMetric()];
                                return d;
                          }});
}

dataObj.prototype.getInputData = function(){
  return this.inputData;
}

dataObj.prototype.getPathData = function(){
  return this.pathData;
}

dataObj.prototype.getFilteredData = function(){
  return this.filteredData;
}

formatting.R

df <- read.csv('Downloads/454789.csv', header = TRUE, as.is = TRUE)
require(lubridate)
weather <- df
weather$hour <- hour(weather$Date)
weather$Date <- strptime(weather$DATE, "%Y%m%d %H:%M")
weather$hour <- hour(weather$Date)
weather$day <- as.Date(weather$Date)
weather <- subset(weather, select = -STATION)
weather <- subset(weather, select = -DATE)
weather <- subset(weather, select = -Date)
weather$HLY.TEMP.NORMAL <- weather$HLY.TEMP.NORMAL / 10
weather$HLY.CLOD.PCTOVC <- weather$HLY.CLOD.PCTOVC / 10
stations <- read.csv('station_name.csv', header = TRUE, as.is = TRUE)
names(stations) = c("STATION_NAME", "city")
k <- merge(weather, stations, all.y = TRUE)
k <- subset(k, select = -STATION_NAME)
names(k) <- c("HLYCLODPCTOVC", "HLYTEMPNORMAL", "hour", "day", "city")
write.csv(k, 'learninghtml/webProjects/weatherHourlyAltTooltip/twentyCities.csv', row.names = FALSE, quote = FALSE)

selectionViz.js


// -------------------------------
// map to select cities
// -------------------------------

setUpMap = function() {
  // place city selection text
  d3.select('#citySelection')
    .style("left", (width + margin.left + 45) + "px")
    .style("top", (margin.top - 10) + "px");

  // label with current city
  document.getElementById('citySelection').innerHTML = "City: " + cState.getCity();

  // defines size of map, and location on the screen
  var projection = d3.geo.albersUsa()
    .translate([width + margin.left + 20, 55])
    .scale([200]);

  var path = d3.geo.path().projection(projection);

  // read in US geometry
  d3.json("us.json", function(error, topology) {

    // limit to continental US
    topology.objects.cb_2013_us_state_20m.geometries =
      topology.objects.cb_2013_us_state_20m.geometries.filter(
        function(d) {
          if (["Alaska", "Hawaii", "Puerto Rico"].indexOf(d.id) == -1) {
            return d
          }
        }
      )

    // attach path for US boundary
    svg.append("path")
      .datum(topojson.feature(topology, topology.objects.cb_2013_us_state_20m))
      .attr("d", path)
      .attr("class", "map");

    function mapSelections(k) {
      k.on("mouseover", function(d) {
        document.getElementById('citySelection').innerHTML = "Click to choose " + d.city;
      })
      .on("mouseout", function(d) {
        document.getElementById('citySelection').innerHTML = "City: " + cState.getCity();
      })
    }


    cities = svg.append("g")
      .attr("class", "cities");

    cities.selectAll(".citiesBackground")
      .data(citiesData)
      .enter().append("circle")
      .attr("transform", function(d) {
        return "translate(" + projection([
          d.location.longitude,
          d.location.latitude
        ]) + ")"
      })
      .attr("r", 7)
      .attr("class", "citiesBackground")
      .attr("opacity", function(d) {
        if (d.city.toLowerCase() == cState.getCity().toLowerCase()) {
          return 1
        } else {
          return 0
        }
      })
      .call(mapSelections)
      .on("click", function(d) {
        updateCity(d.city);
      });

    cities.selectAll(".cityForeground")
      .data(citiesData)
      .enter().append("circle")
      .attr("transform", function(d) {
        return "translate(" + projection([
          d.location.longitude,
          d.location.latitude
        ]) + ")"
      })
      .attr("r", 3)
      .attr("class", "citiesForeground")
      .call(mapSelections)
      .on("click", function(d) {
        updateCity(d.city);
      });
  });

}

updateCities = function(city) {
  // update text above US map
  document.getElementById('citySelection').innerHTML = "City: " + city;

  // update opacity of background circle for cities on map
  d3.selectAll(".citiesBackground")
    .data(citiesData)
    .attr("opacity", function(d) {
      if (d.city.toLowerCase() == city.toLowerCase()) {
        return 1
      } else {
        return 0
      }
    })
}

// CITIES TO MAP, AND LOCATIONS
// use city lookup tool to get other cities http://bl.ocks.org/zanarmstrong/raw/b7381e04dcded29b2b6f/

// city list
var citiesData = [
  {
    "city": "DENVER",
    "country": "USA",
    "location": {
      "latitude": 39.858333333333334,
      "longitude": -104.66694444444445
    }
  },
  {
    "city": "AUSTIN",
    "country": "USA",
    "location": {
      "latitude": 30.194444444444446,
      "longitude": -97.66972222222223
    }
  },
  {
    "city": "CHARLOTTE",
    "country": "USA",
    "location": {
      "latitude": 35.21388888888889,
      "longitude": -80.94305555555556
    }
  },
  {
    "city": "CHICAGO",
    "country": "USA",
    "location": {
      "latitude": 41.78583333333333,
      "longitude": -87.75222222222222
    }
  },
  {
    "city": "COLUMBUS",
    "country": "USA",
    "location": {
      "latitude": 39.99777777777778,
      "longitude": -82.89166666666668
    }
  },
  {
    "city": "DALLAS",
    "country": "USA",
    "location": {
      "latitude": 32.846944444444446,
      "longitude": -96.85166666666666
    }
  },
  {
    "city": "DETROIT",
    "country": "USA",
    "location": {
      "latitude": 42.409166666666664,
      "longitude": -83.00972222222222
    }
  },
  {
    "city": "EL PASO",
    "country": "USA",
    "location": {
      "latitude": 31.849444444444444,
      "longitude": -106.38
    }
  },
  {
    "city": "HOUSTON",
    "country": "USA",
    "location": {
      "latitude": 29.607222222222223,
      "longitude": -95.15861111111111
    }
  },
  {
    "city": "INDIANAPOLIS",
    "country": "USA",
    "location": {
      "latitude": 39.717222222222226,
      "longitude": -86.29416666666667
    }
  },
  {
    "city": "JACKSONVILLE",
    "country": "USA",
    "location": {
      "latitude": 30.49388888888889,
      "longitude": -81.68777777777778
    }
  },
  {
    "city": "LOS ANGELES",
    "country": "USA",
    "location": {
      "latitude": 33.942499999999995,
      "longitude": -118.40805555555556
    }
  },
  {
    "city": "MEMPHIS",
    "country": "USA",
    "location": {
      "latitude": 35.04222222222222,
      "longitude": -89.97666666666667
    }
  },
  {
    "city": "NEW YORK",
    "country": "USA",
    "location": {
      "latitude": 40.63972222222222,
      "longitude": -73.77888888888889
    }
  },
  {
    "city": "PHILADELPHIA",
    "country": "USA",
    "location": {
      "latitude": 39.871944444444445,
      "longitude": -75.24111111111111
    }
  },
  {
    "city": "PHOENIX",
    "country": "USA",
    "location": {
      "latitude": 33.535,
      "longitude": -112.38305555555554
    }
  },
  {
    "city": "SAN ANTONIO",
    "country": "USA",
    "location": {
      "latitude": 29.529444444444444,
      "longitude": -98.27888888888889
    }
  },
  {
    "city": "SAN DIEGO",
    "country": "USA",
    "location": {
      "latitude": 32.69916666666666,
      "longitude": -117.21527777777779
    }
  },
  {
    "city": "SAN FRANCISCO",
    "country": "USA",
    "location": {
      "latitude": 37.61888888888889,
      "longitude": -122.37472222222222
    }
  },
  {
    "city": "SEATTLE",
    "country": "USA",
    "location": {
      "latitude": 47.52972222222222,
      "longitude": -122.30194444444444
    }
  },
  {
    "city": "MINNEAPOLIS",
    "country": "USA",
    "location": {
      "latitude": 44.88027777777778,
      "longitude": -93.21666666666667
    }
  },
  {
    "city": "MIAMI",
    "country": "USA",
    "location": {
      "latitude": 25.793055555555558,
      "longitude": -80.29055555555556
    }
  },
  {
    "city": "GREAT FALLS",
    "country": "USA",
    "location": {
      "latitude": 47.481944444444444,
      "longitude": -111.37055555555555
    }
  },
  {
    "city": "PORTLAND",
    "country": "USA",
    "location": {
      "latitude": 45.58861111111111,
      "longitude": -122.5975
    }
  },
  {
    "city": "ATLANTA",
    "country": "USA",
    "location": {
      "latitude": 33.640277777777776,
      "longitude": -84.42694444444444
    }
  },
  {
    "city": "LAS VEGAS",
    "country": "USA",
    "location": {
      "latitude": 36.08027777777778,
      "longitude": -115.15222222222222
    }
  },
  {
    "city": "FARGO",
    "country": "USA",
    "location": {
      "latitude": 46.87719,
      "longitude": -96.78980
    }
  }
]

state.js

"use strict";

function state(city, metric, yDomain, dimensions, legendRectHeight) {
	this.city = city;
	this.metric = metric;
	this.yDomain = yDomain;
	this.selectedHours= [];
	this.dimensions = dimensions;
	this.scales = {
		// x axis
		x: d3.scale.linear().domain([0, 365]).range([0, dimensions.width]),
		// y axis
		y: d3.scale.linear().domain(this.yDomain[this.metric]).range([dimensions.height, 0]),
		// color for hours 0-23
		color: d3.scale.linear().domain([0, 6, 12, 18, 23]).range(["#0A4D94", "#87B5E6", "#FFC639", "#9F8DE9", "#2C109D"]),
		// x axis formatted for tick marks
		xTime: d3.time.scale().domain([moment("2010-01-01"), moment("2010-12-31")]).range([0, dimensions.width]),
		// for hour legend on right
		legendY: d3.scale.linear().domain([0, 23]).range([dimensions.height / 2 + legendRectHeight * 12 + 30, dimensions.height / 2 - legendRectHeight * 12 + 30])
	};
};

state.prototype.setCity = function(city) {
	this.city = city;
};

state.prototype.getCity = function() {
	return this.city;
};

state.prototype.setMetric = function(metric) {
	this.metric = metric;
	this.scales.y.domain(this.yDomain[this.metric]);
};

state.prototype.getMetric = function() {
	return this.metric;
};

state.prototype.getScales = function(){
	return this.scales;
}

state.prototype.updateSelectedHourList = function(hour) {
  if (this.selectedHours.indexOf(hour) == -1) {
    this.selectedHours.push(hour)
  } else {
    this.selectedHours.splice(this.selectedHours.indexOf(hour), 1);
  }
}

state.prototype.getSelectedHours = function() {
	return this.selectedHours;
}

state.prototype.getTitle = function(){
	var metric = "";
	if(this.metric == "normalTemperature"){
		metric = "Normal Temperature";
	} else if (this.metric == "cloudCover"){
		metric = "Percent of Cloud Cover ";
	} else if (this.metric == "heatIndex"){
		metric = "Heat Index (what temperature it feels like due to humidity) ";
	} else if (this.metric == "windChill"){
		metric = "Wind Chill (what temperature it feels like due to wind) ";
	} else if (this.metric == "aveWindSpeed"){
		metric = "Average Wind Speed ";
	} 
	return metric + " in " + this.city + " by hour of day, based on last 30 years";;
}

state.prototype.getYText = function(){
	if(["normalTemperature", "heatIndex", "windChill"].indexOf(this.metric) != -1){
		return "°F";
	} else if (this.metric == "cloudCover"){
		return "%";
	} else if (this.metric == "aveWindSpeed"){
		return "mph";
	} else {
		return "";
	}
}

state.prototype.getYTextAxis = function(){
	if(this.metric == "normalTemperature"){
		return "Typical Temperature (°F)";
	} else if (this.metric == "cloudCover"){
		return "Typical Cloud Cover, as % of sky";
	} else if (this.metric == "headIndex"){
		return "Apparent Temperature, taking into account humidity (°F)";
	} else if (this.metric == "windChill"){
		return "Apparent Temperature, taking into account wind (°F)";
	} else if (this.metric == "aveWindSpeed"){
		return "Average Wind Speed, in mph";
	} else {
		return "";
	}
}

state.prototype.showCrosshairs = function(bool){
	d3.select(".crosshairs").classed("hidden", !bool);
}

viz.js

"use strict";

///////////////////////////
// view objects
///////////////////////////
function view() {
	this.setUpDot();
	this.setUpCrosshairs();
	this.voronoi = {};
};

view.prototype.setView = function(state, data, filteredData) {
  this.drawLines(state, data);
  this.drawAxis(state);
  this.drawTitle(state);
  this.drawLegend(cState);
  this.drawClock();

  this.voronoi = d3.geom.voronoi()
             		.x(function(d) {
              			return state.getScales().xTime(moment(d.day));
            		})
            		.y(function(d) {
              			return state.getScales().y(d[state.getMetric()]);
            		})
            		.clipExtent([[0, 0],[state.dimensions.width, state.dimensions.height]]);

  this.drawVoronoi(this.voronoi(filteredData), state);
}

view.prototype.updateView = function(state, data, filteredData) {
  this.updateLines(state, data);
  this.updateTitle(state);
  this.updateAxis(state);
  this.updateVoronoi(this.voronoi(filteredData));
};

// manage hour selections // 
view.prototype.updateSelectedHoursView = function(state) {
  var selectedHours = state.getSelectedHours();
  // check if selected line is already selected or not
  if (selectedHours.length == 0) {
    d3.selectAll(".hourlyLines").attr("opacity", 1).classed("selected", false);
    d3.selectAll(".legend rect").attr("opacity", .7).attr("width", 50);
    d3.selectAll(".legend text")
      .text(function(d, i) {if ([0, 6, 12, 18].indexOf(i) != -1) {return formatHours(i);}});
  } else {
    d3.selectAll(".hourlyLines")
      .attr("opacity", function(d, i) {
        if (selectedHours.indexOf(i) != -1) {
          return 1
        } else {
          return .4
        }
      })
      .classed('selected', function(d, i) {
        if (selectedHours.indexOf(i) != -1) {
          return true
        } else {
          return false
        }
      });

    d3.selectAll(".legend rect")
      .attr("opacity", function(d, i) {
        if (selectedHours.indexOf(i) != -1) {
          return 1
        } else {
          return .7
        }
      })
      .attr("width", function(d, i) {
        if (selectedHours.indexOf(i) != -1) {
          return 60
        } else {
          return 50
        }
      });

    d3.selectAll(".legend text")
      .text(function(d, i) {
        if (selectedHours.indexOf(i) != -1) {
          return formatHours(i);
        } else {
          return ""
        }
      })
  }
}

// voronoi for mouseovers // 
view.prototype.drawVoronoi = function(data, state) {
	var self = this;
	// define mouseover and mouseout functions
	function vMouseover(d) {
  		var xpos = state.getScales().xTime(moment(d.day));
  		var ypos = state.getScales().y(d[state.getMetric()])
  		var dot = svg.select(".dot")
    		.classed('hidden', false);

  		dot.attr("transform", "translate(" + (xpos - 2) + "," + (ypos) + ")");

		viz.setCrosshairs(xpos, ypos, d);
  		cState.showCrosshairs(true);
  		self.activateClock(d.hour);
	}

	function vMouseout(d) {
  		svg.select('.dot').classed('hidden', true);
  		state.showCrosshairs(false);
  		self.resetClock();
	}

  var self = this;

  svg.append("g")
    .attr("class", "voronoi")
    .selectAll("path")
    .data(data)
    .enter()
    .append("path")
    .call(definePathAndDatum)
    .on("mouseover", vMouseover)
    .on("mouseout", vMouseout)
    .on('click', function(d, i) {
      cState.updateSelectedHourList(+d.hour);
      self.updateSelectedHoursView(state);
    });
}

view.prototype.updateVoronoi = function(data) {
  svg.selectAll('.voronoi')
    .selectAll("path")
    .data(data).call(definePathAndDatum);
}

// dot to show selected point
view.prototype.setUpDot = function() {
	svg.append("g")
  		.attr("transform", "translate(-100,-100)")
  		.attr("class", "dot hidden")
  		.append("circle")
  		.attr("r", 2);
}

// title
view.prototype.drawTitle = function(state) {
  svg.append("text")
    .text(state.getTitle())
    .attr("x", width / 2)
    .attr("y", -20)
    .style('text-anchor', 'middle')
    .attr("class", "title");
}

view.prototype.updateTitle = function(state) {
  svg.select('.title').text(state.getTitle());
}
// ----- AXIS --------
view.prototype.drawAxis = function(state) {
  var xAxis = d3.svg.axis()
    .tickFormat(d3.time.format("%b"))
    .scale(state.getScales().xTime)
    .orient('bottom');

  var yAxis = d3.svg.axis()
    .scale(state.getScales().y)
    .orient('left');

  svg.append('g')
    .attr('class', 'x axis')
    .attr('transform', 'translate(0,' + height + ')')
    .call(xAxis);

  svg.append('g')
    .attr('class', 'y axis')
    .call(yAxis)
    .append('text')
    .attr('class', 'label')
    .attr('x', 10)
    .attr('y', -40)
    .attr("transform", "rotate(-90)")
    .text(state.getYTextAxis());

  // adjust text labels
  svg.selectAll('.x')
    .selectAll('text')
    .attr('transform', 'translate(' + width / 24 + ',0)')
}

view.prototype.updateAxis = function(state) {
  svg.select(".y")
     .call(d3.svg.axis().scale(state.getScales().y).orient('left'))

  svg.select(".label").text(state.getYTextAxis());
}


// ----- LINES -------
view.prototype.drawLines = function(state, data) {
  var lineFunction = d3.svg.line()
  		.x(function(d, i) {
    		return state.getScales().x(i);
  		})
  		.y(function(d) {
    		return state.getScales().y(d);
  		})
  		.interpolate('basis');

  // line graph
  svg.selectAll('path')
  	// need to improve this
    .data(data)
    .enter()
    .append('path')
    .attr("d", function(d) {
      return lineFunction(d)
    })
    .attr("stroke", function(d, i) {
      return state.getScales().color(i);
    })
    .attr("class", "hourlyLines");
}

view.prototype.updateLines = function(state, data) {
	var lineFunction = d3.svg.line()
  		.x(function(d, i) {
    		return state.getScales().x(i);
  		})
  		.y(function(d) {
    		return state.getScales().y(d);
  		})
  		.interpolate('basis');

	svg.selectAll('.hourlyLines').data(data)
    .attr("d", function(d) {
      return lineFunction(d)
    });
}


// -----------------------------
// set up crosshairs
// -----------------------------
view.prototype.setUpCrosshairs = function() {

  // set up hover cross-hairs
  var crosshairs = svg.append("g").attr("class", "crosshairs hidden");

  crosshairs.append("line")
    .attr({
      x1: 0,
      x2: width,
      y1: 0,
      y2: 0
    })
    .classed("xLine", true);

  crosshairs.append("line")
    .attr({
      x1: 0,
      x2: 0,
      y1: 0,
      y2: height
    })
    .classed("yLine", true);

  crosshairs.append("text")
    .attr("x", 10)
    .attr("y", 0)
    .classed("xText", true);

  crosshairs.append("text")
    .attr("x", 0)
    .attr("y", height - 10)
    .classed("yText", true);

  crosshairs.append("text")
    .attr("x", 0)
    .attr("y", 0)
    .classed("zText", true);

}

view.prototype.setCrosshairs = function(xpos, ypos, d) {
  // move crosshairs
  d3.select(".xLine")
    .attr("transform", "translate(0," + (ypos) + ")")
    .attr("x2", xpos)
  d3.select(".yLine").attr("transform", "translate(" + (xpos) + ",0)").attr("y1", ypos);

  // show text for crosshairs
  d3.select(".crosshairs").select('.xText').text(d[cState.getMetric()] + cState.getYText()).attr("y", ypos - 10);
  d3.select(".crosshairs").select('.zText').text(formatHours(d.hour)).attr("y", ypos - 10).attr("x", xpos + 8);
  d3.select(".crosshairs").select('.yText').text(moment(d.day).format("MMM DD")).attr("x", xpos + 8);
}

// ----- LEGEND -----
view.prototype.drawLegend = function(state) {
  var twentyFourHours = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23];
  var self = this;
  
  // colored rectangles for legend
  var legend = svg.append("g").attr("class", "legend");
  legend.selectAll('rect')
    .data(twentyFourHours)
    .enter()
    .append('rect')
    .attr('class', 'legend')
    .attr("x", width + margin.right / 2 - 25)
    .attr("y", function(d, i) {
      return state.getScales().legendY(i)
    })
    .attr("height", legendRectHeight + 1)
    .attr("width", 50)
    .attr("fill", function(d, i) {
      return state.getScales().color(i)
    })
    .attr("opacity", .7)
    .on('click', function(d, i) {
      state.updateSelectedHourList(i);
      self.updateSelectedHoursView(state);
    });

  // text labels for hours in legend, show only midnight, 6am, noon, and 6pm
  svg.select(".legend").selectAll('text')
    .data(twentyFourHours)
    .enter()
    .append('text')
    .attr('x', width + margin.right / 2 + 40)
    .attr('y', function(d, i) {
      return state.getScales().legendY(i) + legendRectHeight
    })
    .on('click', function(d, i) {
      state.updateSelectedHourList(i);
      sView.updateSelectedHoursView(state);
    })
    .text(function(d, i) {
      if ([0, 6, 12, 18].indexOf(i) != -1) {
        return formatHours(i);
      }
    });
}


// ----- CLOCK ------ // 

view.prototype.drawClock = function() {

  var hour = 0;
  var rotate = 360 / 12 * hour;
  var clock = svg.append("g").attr("class", "clock");

  clock.append("circle")
    .attr("stroke", "grey")
    .attr("stroke-width", 2)
    .attr("fill", "none")
    .attr("r", 50)
    .attr("cx", clockPosition.x)
    .attr("cy", clockPosition.y)

  clock.append("circle")
    .attr("stroke", "none")
    .attr("fill", "grey")
    .attr("r", 2)
    .attr("cx", clockPosition.x)
    .attr("cy", clockPosition.y)

  clock.append("line")
    .attr("stroke", "grey")
    .attr("stroke-width", 2)
    .attr("x1", clockPosition.x)
    .attr("x2", clockPosition.x)
    .attr("y1", clockPosition.y)
    .attr("y2", clockPosition.y - 30)
    .attr("transform", "rotate(" + rotate + " ," + clockPosition.x + "," + clockPosition.y + ")");

  clock.append('text').attr("class", "am partOfDay")
    .attr("fill", "grey")
    .attr("x", clockPosition.x - 50 - 13.5)
    .attr("y", clockPosition.y - 45)
    .text("AM")

  clock.append('text').attr("class", "pm partOfDay")
    .attr("fill", "grey")
    .attr("x", clockPosition.x + 50 - 13.5)
    .attr("y", clockPosition.y - 45)
    .text("PM")

  d3.select(".clock").selectAll(".clockHour").data(clockHours).enter().append('text')
    .attr("class", "clockHour")
    //   .attr("fill", function(d){if(hour == d){return "white"} else {return "grey"}})
    .attr("fill", "grey")
    .attr("font-size", 14)
    .attr("x", function(d) {
      if ([10, 11, 12].indexOf(d) == -1) {
        return clockPosition.x - 3.5
      } else {
        return clockPosition.x - 3.5 - 4
      }
    })
    .attr("y", clockPosition.y + 5)
    .attr("transform", function(d) {
      var rotateN = 360 / 12 * d;
      return "translate(" + (39 * Math.cos((rotateN - 90) * Math.PI / 180)) + "," + (39 * Math.sin((rotateN - 90) * Math.PI / 180)) + ")"
    })
    .text(function(d) {
      return d
    });

}

view.prototype.activateClock = function(hour) {
  var partOfDay = 'am'
  if (hour > 11) {
    hour = hour - 12;
    partOfDay = 'pm';
  }
  // AM vs PM
  d3.select("." + partOfDay).attr("fill", "white");

  // line
  var rotate = 360 / 12 * hour;
  d3.select(".clock")
    .select("line")
    .attr("transform", "rotate(" + rotate + " ," + clockPosition.x + "," + clockPosition.y + ")").attr("stroke", "white");

  // time in text
  d3.selectAll(".clockHour").data(clockHours)
    .attr("fill", function(d) {
      if (hour == d % 12) {
        return "white"
      } else {
        return "grey"
      }
    });
}

view.prototype.resetClock = function() {
  d3.select(".clock").selectAll("text").attr("fill", "grey");
  d3.select(".clock").selectAll("line").attr("stroke", "grey")
}

// helper function for voronoi mouseovers
function definePathAndDatum(selection){
    selection.attr("d", function(d) {
      if(typeof(d) != 'undefined'){
		return "M" + d.join("L") + "Z"};
    })
    .datum(function(d) {
    	if(typeof(d) != 'undefined'){
			return d.point;
    }})
}

// helper function for formatting hours into normal readable
function formatHours(num) {
  if (num == 0) {
    return "midnight";
  } else if (num < 12) {
    return num + "am";
  } else if (num == 12) {
    return "noon";
  } else {
    return (num - 12) + "pm";
  }
}

weatherHourly.css

body {
	background-color: #3F3F3F;
}

p {
	color: white;
	font-size: 14px;
}

.title {
  font: 20px;
  fill: white;
}

.hourlyLines {
	stroke-width: 1;
	fill: none;
}

.selected {
	stroke-width: 3;
	fill: none;
}

.hidden {
		  display: none;
}

.axis path,
.axis line {
	fill: none;
	stroke: white;
	shape-rendering: crispEdges;
}

.legend text {
	fill: white;
	text-anchor: left;
	font-size: 14px;
}

.tick {
	fill: white;
}

.y .label {
	fill: white;
	font-size: 14px;
	text-anchor: end;
}


.voronoi path {
  stroke: none;
  fill: none;
  pointer-events: all;
}

.crosshairs {
	stroke: white;
	stroke-width: 0.2;
}

.crosshairs text {
	fill: white;
}

#metric {
		  position: absolute;
		  top: 10px;
		  left: 1000px;
		  width: auto;
		  height: auto;
}

#instructions {
		  position: absolute;
		  top: 10px;
		  left: 105px;
		  width: 850px;
		  height: auto;
		  padding: 5px 10px;
		  background-color: rgba(255, 255, 255, 0.7);
		  -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, 0.7);
		  -moz-box-shadow: 0 0 5px rgba(0, 0, 0, 0.7);
		  box-shadow: 0 0 5px rgba(0, 0, 0, 0.7);
		  pointer-events: none;
}

#instructions p {
		  margin: 0;
		  line-height: 20px;
		  color: black;
		  font-size: 14px;
}

.dot circle {
	fill: rgba(255, 255, 255, 1);
}


.crosshairs {
	stroke: white;
	stroke-width: 0.2;
}

.crosshairs text {
	fill: white;
}

#citySelection {
  position: absolute;
}


.map {
  fill: white;
  stroke: none;
}

.cities {
	fill: #FF7F15;
}

.citiesBackground:hover {
  opacity: 1;
}

.citiesForeground:hover {
	stroke: #FF7F15;
  stroke-width: 8;
}

weatherHourly.js

"use strict";

// Data location
var dataFile = "moreCities.csv";

// STANDARD VARIABLES
var margin = {
    top: 120,
    right: 250,
    bottom: 60,
    left: 100
  },
  width = 1200 - margin.left - margin.right,
  height = 700 - margin.top - margin.bottom;

// other variables
var legendRectHeight = 13,
    clockPosition = {x: width - 60, y: 60},
    clockHours = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];

// initialize state
var cState = new state('SAN FRANCISCO', 
                       "cloudCover", 
                       {normalTemperature: [-5,105], heatIndex: [-5,105], windChill: [-5,105], cloudCover: [0,100], aveWindSpeed: [0,25]},
                       {width: width, height: height},
                       legendRectHeight);

// initialize data
var data = new dataObj();

// STANDARD SVG SETUP
var svg = d3.select('#weatherLines')
  .append('svg')
  .attr('width', width + margin.left + margin.right)
  .attr('height', height + margin.top + margin.bottom)
  .append('g')
  .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

// initialize view
var viz = new view();

// when metric changes, update data and view
d3.select('#metric')
  .on("change", function() {
    console.log(this.value)
    cState.setMetric(this.value);
    updateDataAndView();
  })

// -----------------------------
// READ IN DATA AND DRAW GRAPH
// -----------------------------
d3.csv(dataFile, function(error, inputData) {
  if (error) return console.error(error);

  // transform data to useable format (better way to do this?)
  data.updateData(inputData, cState);

  // draw lines 
  viz.setView(cState, data.getPathData(), data.getFilteredData());
  setUpMap();

});

// ----- helper functions ----- // 
// update selected city
function updateCity(city) {
    cState.setCity(city);
    updateCities(city);
    updateDataAndView();
}

// update data and view
function updateDataAndView() {
  data.updateState(cState);
  viz.updateView(cState, data.getPathData(), data.getFilteredData());
}