"use strict"; var StockVis = function() { /** Define global constants. */ const SVG_WIDTH = 960; const SVG_HEIGHT = 850; const MARGIN = { top: 20, right: 20, bottom: 30, left: 50 }; const ENTIRE_CHART_WIDTH = SVG_WIDTH - MARGIN.left - MARGIN.right; const ENTIRE_CHART_HEIGHT = SVG_HEIGHT - MARGIN.top - MARGIN.bottom; const STOCK_CHART_WIDTH = ENTIRE_CHART_WIDTH; const STOCK_CHART_HEIGHT = 450; const BALANCE_CHART_WIDTH = ENTIRE_CHART_WIDTH; const BALANCE_CHART_HEIGHT = 200; const BALANCE_SHEET_COLOR = "#aecc00"; const BORDER_STROKE_COLOR = "black"; const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; /** Define global variables. */ var self = this; var parseDate = d3.timeParse("%m/%d/%y"); /** Define class member fields. */ self.stockData; self.dataLength; self.balanceSheetData; self.initialStockVolume; self.sectors; self.baselineSector; self.stockLayers; self.balanceLayer; self.stockStreamArea; self.balanceStreamArea; self.balanceBars; self.stockColorScale; self.activeSectors = new Set(); self.isMouseActive = true; self.chart = d3.select("body") .append("div") .attr("class", "chart") .append("svg") .attr("width", SVG_WIDTH) .attr("height", SVG_HEIGHT) .append("g") .attr("transform", "translate(" + MARGIN.left + "," + MARGIN.top + ")"); self.tooltip = d3.select(".chart") .append("div") .attr("class", "tooltip hidden") .style("top", MARGIN.top + "px") .style("left", ENTIRE_CHART_WIDTH * 0.7 + "px"); self.tooltip.append("p") .attr("class", "first-row"); self.tooltip.append("p") .attr("class", "second-row"); self.tooltip.append("p") .attr("class", "third-row"); self.tooltip.append("p") .attr("class", "fourth-row"); self.tooltip.append("p") .attr("class", "fifth-row"); /** The public function to be called in the main js. */ self.run = function(stockDataPath, monthlyDataPath) { buildOnStockData(stockDataPath); buildOnBalanceData(monthlyDataPath); buildOnInterestDate(monthlyDataPath); }; function buildOnStockData(stockDataPath) { d3.csv(stockDataPath, function(error, rawData) { if (error) throw error; self.sectors = rawData.columns.slice(1); for (var i in self.sectors) { self.activeSectors.add(self.sectors[i]); } self.stockColorScale = getStockColorScale(getActiveSectors()); self.baselineSector = self.sectors[0]; self.stockData = parseStockData(rawData, self.sectors); self.dataLength = self.stockData.length; self.initialStockVolume = getValueSum(self.stockData[0]); var stack = d3.stack() .keys(self.sectors); var stackStockData = stack(self.stockData); var stockXScale = getStockXScale(self.stockData, STOCK_CHART_WIDTH); var stockYScale = getStockYScale(stackStockData, STOCK_CHART_HEIGHT); var areaMaker = getAreaMaker(stockXScale, stockYScale); generateStockStreamChart(stackStockData, areaMaker); generateTimeAxis(stockXScale); var legendObj = { title: "Stock Sectors", data: self.sectors.slice().reverse(), colors: self.stockColorScale, titleX: 10, titleY: 10, rectX: 10, rectY: 20, textX: 40, textY: 30 }; var legend = generateLegends(legendObj); attachFilters(legend); }); }; function buildOnBalanceData(balanceDataPath) { d3.csv(balanceDataPath, function(error, rawData) { if (error) throw error; self.balanceSheetData = parseBalanceSheetData(rawData); var padding = 50; var balanceYScale = d3.scaleLinear() .rangeRound([STOCK_CHART_HEIGHT + BALANCE_CHART_HEIGHT, STOCK_CHART_HEIGHT + padding]) .domain(d3.extent(self.balanceSheetData, function(d) { return d.value; })); generateBalanceStreamChart(balanceYScale); generateBalanceBarChart(balanceYScale); var legendObj = { title: "Federal Reserve", data: ["Balance Sheet Volume"], colors: BALANCE_SHEET_COLOR, titleX: 10, titleY: STOCK_CHART_HEIGHT + 45, rectX: 10, rectY: STOCK_CHART_HEIGHT + 55, textX: 40, textY: STOCK_CHART_HEIGHT + 65 }; generateLegends(legendObj); }); }; function buildOnInterestDate(interestDataPath) { d3.csv(interestDataPath, function(error, rawData) { if (error) throw error; var output = parseInterestData(rawData); }); }; // This function parse the raw data. function parseStockData(input, keys) { var output = []; var start = 0; for (var i = 1; i < input.length; i++) { var currMonth = parseDate(input[i].Date).getMonth(), priorMonth = parseDate(input[i - 1].Date).getMonth(); if (currMonth != priorMonth) { var num = i - start; var obj = {} keys.forEach(function(key) { obj[key] = input.slice(start, i).reduce(function(sum, item) { return sum + item[key] / num; }, 0); }); obj.date = parseDate(input[i - 1].Date); obj.date = new Date(obj.date.getFullYear(), obj.date.getMonth() + 1, 0); output.push(obj); start = i; } } var num = input.length - start; var obj = {} keys.forEach(function(key) { obj[key] = input.slice(start, input.length).reduce(function(sum, item) { return sum + item[key] / num; }, 0); }); obj.date = parseDate(input[input.length - 1].Date); obj.date = new Date(obj.date.getFullYear(), obj.date.getMonth() + 1, 0); output.push(obj); return output; }; function parseBalanceSheetData(input) { var output = []; for (var i = 0; i < input.length; i++) { //if var obj = {}; obj.date = parseDate(input[i].date); obj.value = +input[i].balance_sheet; output.push(obj); } return output; }; function parseInterestData(input) { var output = []; for (var i = 0; i < input.length; i++) { if (input[i].category == "'rate'") { var obj = {}; obj.date = parseDate(input[i].date); obj.amount = +input[i].amount; output.push(obj); } } return output; } // This function generate the stream chart of the stock sectors. function generateStockStreamChart(stackStockData, areaMaker) { self.stockLayers = self.chart.append("g") .attr("class", "stock-layer") .selectAll("path") .data(stackStockData) .enter().append("path") .attr("class", "stock-area") .style("fill", function(d) { return self.stockColorScale(d.key); }) .attr("d", areaMaker) .attr("opacity", 1) .on("mouseover", function(selected, i) { if (!self.isMouseActive) { return; } obscureALlExcept(self.stockLayers, i, 250); }) .on("mousemove", function(selected, i) { if (!self.isMouseActive) { return; } var mouseX = d3.mouse(this)[0]; var invertedX = getInvertedX(mouseX, STOCK_CHART_WIDTH, self.dataLength); var title = selected[invertedX].data.date.getFullYear() + " " + MONTHS[selected[invertedX].data.date.getMonth()] + " " + selected.key + " Sector", stockValue = selected[invertedX].data[selected.key], stockValueChange = (stockValue - selected[0].data[selected.key]) / selected[0].data[selected.key], balance = self.balanceSheetData[invertedX].value, balanceChange = (balance - self.balanceSheetData[0].value) / self.balanceSheetData[0].value; showAreaBorder(this); showTooltip(title, stockValue, stockValueChange, balance, balanceChange); obscureALlExcept(self.balanceBars, invertedX, 100, 0); obscureAll(self.balanceLayer, 100, 0.4); }) .on("mouseout", function(selected, i) { if (!self.isMouseActive) { return; } obscureAll(self.balanceBars); unobscureAll(self.stockLayers, 250); unobscureAll(self.balanceLayer); hideAreaBorder(this); hideTooltip(); }) .on("click", function(selected, i) { if (!self.isMouseActive) { return; } if (selected.key != self.baselineSector) { self.baselineSector = selected.key; var stack = getOrderedStack(self.baselineSector, getActiveSectors(), self.sectors.length); var stackStockData = stack(self.stockData); var stockXScale = getStockXScale(self.stockData, STOCK_CHART_WIDTH); var stockYScale = getStockYScale(stackStockData, STOCK_CHART_HEIGHT); var areaMaker = getAreaMaker(stockXScale, stockYScale); updateStockStreamChart(stackStockData, areaMaker); } }); }; function updateStockStreamChart(stackStockData, areaMaker) { unobscureAll(self.stockLayers); // Disable mouse detection functions until transition is complete. self.isMouseActive = false; self.stockStreamArea = self.chart.selectAll(".stock-area") .data(stackStockData, function (d) { return d.key; }); self.stockStreamArea.exit().remove(); self.stockStreamArea .enter().append("path") .attr("class", "stock-area") .classed("hover", false) .attr("stroke-width", "0px") .merge(self.stockStreamArea) .style("fill", function(d) { return self.stockColorScale(d.key); }) .transition() .on("end", function() { self.isMouseActive = true; }) .duration(500) .ease(d3.easeLinear) .attr("d", areaMaker) .attr("opacity", 1); }; function generateBalanceStreamChart(balanceYScale) { var balanceXScale = d3.scaleTime() .rangeRound([0, BALANCE_CHART_WIDTH]) .domain(d3.extent(self.balanceSheetData, function(d) { return d.date; })); var areaMaker = d3.area() .curve(d3.curveCatmullRom) .x(function(d, i) { return balanceXScale(d.date); }) .y0(function(d) { return balanceYScale(0); }) .y1(function(d) { return balanceYScale(d.value); }); self.balanceLayer = self.chart.append("g") .attr("class", "balance-layer") .append("path") .datum(self.balanceSheetData) .style("fill", function(d) { return BALANCE_SHEET_COLOR; }) .attr("d", areaMaker) .attr("opacity", 1) } function generateTimeAxis(stockXScale) { self.chart.append("g") .attr("class", "axis") .attr("transform", "translate(0," + ENTIRE_CHART_HEIGHT + ")") .call(d3.axisBottom(stockXScale) .ticks(10) .tickFormat(d3.timeFormat("%Y"))); }; function generateLegends(legendObj) { self.chart.append("g") .attr("class", "legend") .attr("text-anchor", "start") .append("g") .append("text") .attr("x", legendObj.titleX) .attr("y", legendObj.titleY) .attr("font-family", "Arial Black") .attr("font-size", 13) .attr("font-weight", "bold") .text(legendObj.title); var legend = self.chart .select(".legend") .append("g") .attr("font-family", "sans-serif") .attr("font-size", 12) .selectAll("g") .data(legendObj.data) .enter().append("g") .attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; }); legend.append("rect") .attr("x", legendObj.rectX) .attr("y", legendObj.rectY) .attr("width", 19) .attr("height", 19) .attr("fill", legendObj.colors); legend.append("text") .attr("x", legendObj.textX) .attr("y", legendObj.textY) .attr("dy", "0.32em") .text(function(d) { return d; }); return legend; }; function attachFilters(clickables) { clickables.on("click", function(selected, i) { if (!self.isMouseActive) { return; } if (self.activeSectors.has(selected) && self.activeSectors.size == 1) { return } else if (self.activeSectors.has(selected)) { self.activeSectors.delete(selected); if (selected == self.baselineSector) { self.baselineSector = getActiveSectors()[0]; //console.log(selected, getActiveSectors); } } else { self.activeSectors.add(selected); } var stack = getOrderedStack(self.baselineSector, getActiveSectors(), self.sectors.length); var stackStockData = stack(self.stockData); var stockXScale = getStockXScale(self.stockData, STOCK_CHART_WIDTH); var stockYScale = getStockYScale(stackStockData, STOCK_CHART_HEIGHT); var areaMaker = getAreaMaker(stockXScale, stockYScale); updateStockStreamChart(stackStockData, areaMaker); }); }; function generateBalanceBarChart(balanceYScale) { var balanceXScale = d3.scaleBand() .rangeRound([0, BALANCE_CHART_WIDTH]) .domain(self.balanceSheetData.map(function(d) { return d.date; })); self.chart.append("g") .attr("class", "bar-area") .selectAll(".bar") .data(self.balanceSheetData) .enter().append("rect") .attr("class", "bar") .attr("x", function(d) { return balanceXScale(d.date); }) .attr("y", function(d) { return balanceYScale(d.value); }) .attr("width", balanceXScale.bandwidth()) .attr("height", function(d) { return BALANCE_CHART_HEIGHT + STOCK_CHART_HEIGHT + 35 - balanceYScale(d.value); }) .attr("opacity", 0) .style("fill", BALANCE_SHEET_COLOR); self.balanceBars = self.chart.selectAll(".bar"); self.balanceBars.on("mouseover", function(selected, i) { if (!self.isMouseActive) { return; } obscureALlExcept(self.balanceBars, i, 250, 0); obscureAll(self.balanceLayer, 100, 0.4); }) .on("mousemove", function(selected, i) { if (!self.isMouseActive) { return; } var mouseX = d3.mouse(this)[0]; var invertedX = getInvertedX(mouseX, STOCK_CHART_WIDTH, self.dataLength); var title = self.balanceSheetData[invertedX].date.getFullYear() + " " + MONTHS[self.balanceSheetData[invertedX].date.getMonth()] + " Entire Market", stockValue = getValueSum(self.stockData[invertedX]), stockValueChange = (stockValue - self.initialStockVolume) / self.initialStockVolume, balance = self.balanceSheetData[invertedX].value, balanceChange = (balance - self.balanceSheetData[0].value) / self.balanceSheetData[0].value; showTooltip(title, stockValue, stockValueChange, balance, balanceChange); }) .on("mouseout", function(selected, i) { if (!self.isMouseActive) { return; } unobscureAll(self.balanceBars); unobscureAll(self.balanceLayer); hideTooltip(); }) }; function getActiveSectors() { return Array.from(self.activeSectors); } function showTooltip(title, stockValue, stockValueChange, balance, balanceChange) { self.tooltip.classed("hidden", false); var stockValueChangePercentage = Math.round(stockValueChange * 1000) / 10; var balanceChangePercentage = Math.round(balanceChange * 1000) / 10; self.tooltip.select(".first-row") .text(title); self.tooltip.select(".second-row") .text("Stock Volume: " + parseInt(stockValue)); self.tooltip.select(".third-row") .text("Stock Volume Change Since Start: " + stockValueChangePercentage + "%"); self.tooltip.select(".fourth-row") .text("Balance Sheet Volume: " + balance); self.tooltip.select(".fifth-row") .text("Balance Sheet Volume Change Since Start: " + balanceChangePercentage + "%"); } function hideTooltip() { self.tooltip.classed("hidden", true); }; function showAreaBorder(selectedArea) { d3.select(selectedArea) .classed("hover", true) .attr("stroke", BORDER_STROKE_COLOR) .attr("stroke-width", "2px"); }; function hideAreaBorder(selectedArea) { d3.select(selectedArea) .classed("hover", false) .attr("stroke-width", "0px") }; };