diff --git a/client/package-lock.json b/client/package-lock.json index 27e8eb2d20..8da82e5cf1 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -3981,6 +3981,19 @@ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" }, + "d3": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", + "integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=" + }, + "d3-3": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/d3-3/-/d3-3-0.0.0.tgz", + "integrity": "sha1-xrj+3d590sqJMiKxJ7n9qY41Aq0=", + "requires": { + "d3": "^3" + } + }, "cheerio": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", @@ -4030,8 +4043,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -4049,13 +4061,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4068,18 +4078,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -4182,8 +4189,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -4193,7 +4199,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4206,20 +4211,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.3.5", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4236,7 +4238,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4309,8 +4310,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -4320,7 +4320,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4396,8 +4395,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -4427,7 +4425,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4445,7 +4442,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4484,13 +4480,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -14664,7 +14658,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" } } diff --git a/client/package.json b/client/package.json index 2d07dd5e0b..18dada10cc 100644 --- a/client/package.json +++ b/client/package.json @@ -20,6 +20,7 @@ "axios": "^0.19.0", "browser-cookies": "^1.2.0", "chai": "^4.2.0", + "d3-3": "0.0.0", "date-fns": "^1.30.1", "entities": "^1.1.2", "enzyme": "^3.10.0", diff --git a/client/src/templates/Guide/components/calculators/CodingBootcampCostCalculator/CodingBootcampCostCalculator.css b/client/src/templates/Guide/components/calculators/CodingBootcampCostCalculator/CodingBootcampCostCalculator.css new file mode 100644 index 0000000000..5291016152 --- /dev/null +++ b/client/src/templates/Guide/components/calculators/CodingBootcampCostCalculator/CodingBootcampCostCalculator.css @@ -0,0 +1,39 @@ +.article .d3-chart { + position: relative; +} + +.article div.tooltip { + position: absolute; + text-align: center; + width: 80px; + padding: 5px; + font: 12px sans-serif; + background: rgba(0, 0, 0, 0.8); + border: 0px; + border-radius: 8px; + pointer-events: none; + color: #fff; + display: table; +} + +.article div.tooltip > div { + display: table-cell; + vertical-align: middle; +} + +.article div.tooltip:after { + box-sizing: border-box; + font-size: 10px; + width: 100%; + line-height: 1; + color: rgba(0, 0, 0, 0.8); + content: "\25BC"; + position: absolute; + text-align: center; + bottom: -9px; + left: 0; +} + +.select-city { + display: block; +} \ No newline at end of file diff --git a/client/src/templates/Guide/components/calculators/CodingBootcampCostCalculator/CodingBootcampCostCalculator.js b/client/src/templates/Guide/components/calculators/CodingBootcampCostCalculator/CodingBootcampCostCalculator.js new file mode 100644 index 0000000000..5dd44a58c5 --- /dev/null +++ b/client/src/templates/Guide/components/calculators/CodingBootcampCostCalculator/CodingBootcampCostCalculator.js @@ -0,0 +1,228 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import { graphql } from 'gatsby'; + +import ArticleLayout from '../../ArticleLayout'; + +import CostCalculatorChart from './CostCalculatorChart'; +import './CodingBootcampCostCalculator.css'; + +const propTypes = { + data: PropTypes.object +}; + +class CostCalculator extends React.Component { + state = { + cities: [], + incomes: [], + city: null, + cityLabel: '_______', + lastYearsIncome: null, + lastYearsIncomeLabel: '_______', + bootcamps: null + }; + + initComponent() { + const bootcamps = JSON.parse( + document.getElementById('bootcamps').innerHTML + ); + + document + .getElementById('bootcamps-data-link') + .setAttribute( + 'href', + `data:text/plain;charset=utf-8,${encodeURIComponent( + JSON.stringify(bootcamps, null, 2) + )}` + ); + + this.setState({ + bootcamps: bootcamps, + cities: bootcamps + .reduce((previous, current) => { + return previous.concat(current.cities); + }, []) + .filter((city, idx, me) => { + return me.indexOf(city) === idx; + }) + .sort(), + incomes: [ + '0', + '10000', + '20000', + '30000', + '40000', + '50000', + '60000', + '70000', + '80000', + '90000', + '100000', + '120000', + '140000', + '160000', + '180000', + '200000' + ] + }); + + const selectCityDiv = document.getElementById('select-city'); + const selectIncomeDiv = document.getElementById('select-income'); + const cityLabelSpan = document.getElementById('city-label'); + const lastYearsIncomeLabelSpan = document.getElementById( + 'last-years-income-label' + ); + const chartComponentDiv = document.getElementById('chart-component'); + this.setState({ + init: true, + selectCityDiv, + selectIncomeDiv, + cityLabelSpan, + lastYearsIncomeLabelSpan, + chartComponentDiv + }); + + this.handleCitySelector = this.handleCitySelector.bind(this); + this.handleIncomeSelector = this.handleIncomeSelector.bind(this); + } + + componentDidMount() { + this.initComponent(); + } + + handleCitySelector(event) { + let el = event.target; + this.setState({ + city: event.target.value, + cityLabel: el.options[el.selectedIndex].text + }); + } + + handleIncomeSelector(event) { + let el = event.target; + this.setState({ + lastYearsIncome: parseInt(event.target.value, 10), + lastYearsIncomeLabel: el.options[el.selectedIndex].text + }); + } + + renderSelectCity() { + return ( + + ); + } + + renderSelectIncome() { + return ( + + ); + } + + renderChartComponent() { + return ( + + ); + } + + render() { + if (!this.state.init) { + return null; + } + return ( +
+ {ReactDOM.createPortal( + this.renderSelectCity(), + this.state.selectCityDiv + )} + {ReactDOM.createPortal( + this.renderSelectIncome(), + this.state.selectIncomeDiv + )} + {ReactDOM.createPortal(this.state.cityLabel, this.state.cityLabelSpan)} + {ReactDOM.createPortal( + this.state.lastYearsIncomeLabel, + this.state.lastYearsIncomeLabelSpan + )} + {ReactDOM.createPortal( + this.renderChartComponent(), + this.state.chartComponentDiv + )} +
+ ); + } +} + +const CodingBootcampCostCalculator = props => { + const { + data: { + markdownRemark: { html } + } + } = props; + return ( + +
+ + + ); +}; + +CodingBootcampCostCalculator.displayName = 'CodingBootcampCostCalculator'; +CodingBootcampCostCalculator.propTypes = propTypes; +export default CodingBootcampCostCalculator; + +export const pageQuery = graphql` + query CodingBootcampCostCalculator($id: String!) { + markdownRemark(id: { eq: $id }) { + html + ...ArticleLayout + } + } +`; diff --git a/client/src/templates/Guide/components/calculators/CodingBootcampCostCalculator/CostCalculatorChart.js b/client/src/templates/Guide/components/calculators/CodingBootcampCostCalculator/CostCalculatorChart.js new file mode 100644 index 0000000000..e5118c020a --- /dev/null +++ b/client/src/templates/Guide/components/calculators/CodingBootcampCostCalculator/CostCalculatorChart.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { updateCalculator } from './CostCalculatorChartD3.js'; + +const propTypes = { + bootcamps: PropTypes.array, + city: PropTypes.string, + lastYearsIncome: PropTypes.number +}; + +class CostCalculatorChart extends React.Component { + constructor(props) { + super(props); + this.rootRef = React.createRef(); + } + + componentDidMount() { + this.updateChart(); + } + + componentDidUpdate() { + this.updateChart(); + } + + updateChart() { + const { bootcamps, city, lastYearsIncome } = this.props; + if (city !== null && lastYearsIncome !== null) { + const node = this.rootRef.current; + updateCalculator(node, bootcamps, city, lastYearsIncome); + } + } + + render() { + return
; + } +} + +CostCalculatorChart.propTypes = propTypes; + +export default CostCalculatorChart; diff --git a/client/src/templates/Guide/components/calculators/CodingBootcampCostCalculator/CostCalculatorChartD3.js b/client/src/templates/Guide/components/calculators/CodingBootcampCostCalculator/CostCalculatorChartD3.js new file mode 100644 index 0000000000..bc6b953d29 --- /dev/null +++ b/client/src/templates/Guide/components/calculators/CodingBootcampCostCalculator/CostCalculatorChartD3.js @@ -0,0 +1,240 @@ +import d3 from 'd3-3'; + +export function updateCalculator(d3Node, bootcamps, city, lastYearsIncome) { + d3Node.className = 'd3-chart'; + var categoryNames = [ + 'Lost Wages', + 'Financing Cost', + 'Housing Cost', + 'Tuition / Est. Wage Garnishing' + ]; + // Tooltips + var tip = d3.select(d3Node).select('div.tooltip'); + if (tip.empty()) { + tip = d3 + .select(d3Node) + .append('div') + .attr('class', 'tooltip') + .style('opacity', 0); + } + var xAxis = d3.svg.axis().orient('bottom'); + var yAxis = d3.svg.axis().orient('left'); + bootcamps.forEach(function(camp) { + var x0 = 0; + var weeklyHousing; + if (camp.cities.indexOf(city) > -1) { + weeklyHousing = 0; + } else { + weeklyHousing = +camp.housing; + } + camp.mapping = [ + { + name: camp.name, + label: 'Tuition / Est. Wage Garnishing', + value: +camp.cost, + x0: x0, + x1: (x0 += +camp.cost) + }, + { + name: camp.name, + label: 'Financing Cost', + value: camp.finance ? +Math.floor(camp.cost * 0.09519) : 0, + x0: camp.finance ? +camp.cost : 0, + x1: camp.finance ? (x0 += +Math.floor(camp.cost * 0.09519)) : 0 + }, + { + name: camp.name, + label: 'Housing Cost', + value: +weeklyHousing * camp.weeks, + x0: camp.finance ? +Math.floor(camp.cost * 1.09519) : camp.cost, + x1: (x0 += weeklyHousing * camp.weeks) + }, + { + name: camp.name, + label: 'Lost Wages', + value: +Math.floor((camp.weeks * lastYearsIncome) / 50), + x0: camp.finance + ? +(Math.floor(camp.cost * 1.09519) + weeklyHousing * camp.weeks) + : +camp.cost + weeklyHousing * camp.weeks, + x1: (x0 += +(Math.floor(camp.weeks * lastYearsIncome) / 50)) + } + ]; + camp.total = camp.mapping[camp.mapping.length - 1].x1; + }); + bootcamps.sort(function(a, b) { + return a.total - b.total; + }); + var xStackMax = d3.max(bootcamps, function(d) { + return d.total; + }); + var margin = { + top: 30, + right: 60, + bottom: 60, + left: 155 + }, + width = 650 - margin.left - margin.right, + height = 1200 - margin.top - margin.bottom; + var xScale = d3.scale + .linear() + .domain([0, xStackMax]) + .rangeRound([0, width]); + var y0Scale = d3.scale + .ordinal() + .domain( + bootcamps.map(function(d) { + return d.name; + }) + ) + .rangeRoundBands([0, height], 0.1); + var color = d3.scale + .ordinal() + .range(['#215f1e', '#5f5c1e', '#1e215f', '#5c1e5f']) + .domain(categoryNames); + var svg = d3 + .select(d3Node) + .select('svg') + .select('g'); + var selection, rect; + if (svg.empty()) { + svg = d3 + .select(d3Node) + .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 + ')'); + // Legends + var legend = svg + .selectAll('.legend') + .data(categoryNames.slice().reverse()) + .enter() + .append('g') + .attr('class', 'legend') + .attr('transform', function(d, i) { + return 'translate(30,' + i * y0Scale.rangeBand() * 1.1 + ')'; + }); + legend + .append('rect') + .attr('x', width - y0Scale.rangeBand()) + .attr('width', y0Scale.rangeBand()) + .attr('height', y0Scale.rangeBand()) + .style('fill', color) + .style('stroke', 'white'); + legend + .append('text') + .attr('x', width - y0Scale.rangeBand() * 1.2) + .attr('y', 12) + .attr('dy', '.35em') + .style('text-anchor', 'end') + .text(function(d) { + return d; + }); + } + selection = svg.selectAll('.series').data(bootcamps); + if (!selection.empty()) { + selection.exit().remove(); + } + selection + .enter() + .append('g') + .attr('class', 'series') + .attr('transform', function(d) { + return 'translate(0,' + y0Scale(d.name) + ')'; + }); + rect = selection.selectAll('rect').data(function(d) { + return d.mapping; + }); + if (!rect.empty()) { + rect.exit().remove(); + } + rect + .enter() + .append('rect') + .attr('class', 'series-part') + .attr('x', 0) + .attr('width', 0) + .attr('height', y0Scale.rangeBand()) + .style('fill', function(d) { + return color(d.label); + }) + .style('stroke', 'white') + .on('mouseover', function(d) { + var component = d3.select('.d3-chart'); + var componentPos = component[0][0].getBoundingClientRect(); + var box = this.getBoundingClientRect(); + var tooltip = component.select('.tooltip'); + + component + .select('.tooltip') + .transition() + .duration(200) + .style('opacity', 0.9); + component + .select('.tooltip') + .html('
' + d.label + '
$' + d.value + '
') + .style( + 'left', + box.left + + box.width / 2 - + componentPos.left - + tooltip[0][0].offsetWidth / 2 + + 'px' + ) + .style( + 'top', + box.top - componentPos.top - tooltip[0][0].offsetHeight - 10 + 'px' + ); + }) + .on('mouseout', function() { + d3.select('.d3-chart') + .select('.tooltip') + .transition() + .duration(500) + .style('opacity', 0); + }); + rect + .transition() + .delay(function(d, i) { + return i * 10; + }) + .attr('x', function(d) { + return xScale(d.x0); + }) + .attr('width', function(d) { + return xScale(d.x1 - d.x0); + }); + + // Axes + var svgXAxis = svg.select('.x.axis'); + var svgYAxis = svg.select('.y.axis'); + xAxis.scale(xScale); + yAxis.scale(y0Scale); + if (svgXAxis.empty() || svgYAxis.empty()) { + svg + .append('g') + .attr('class', 'y axis') + .call(yAxis); + svg + .append('g') + .attr('class', 'x axis') + .attr('transform', 'translate(0,' + height + ')') + .call(xAxis) + .selectAll('text') + .style('text-anchor', 'end') + .attr('dx', '-.8em') + .attr('dy', '.15em') + .attr('transform', 'rotate(-45)'); + } else { + svgXAxis + .call(xAxis) + .selectAll('text') + .style('text-anchor', 'end') + .attr('dx', '-.8em') + .attr('dy', '.15em') + .attr('transform', 'rotate(-45)'); + svgYAxis.call(yAxis); + } + + return d3Node; +} diff --git a/mock-guide/english/tools/calculators/coding-bootcamp-cost-calculator/index.md b/mock-guide/english/tools/calculators/coding-bootcamp-cost-calculator/index.md new file mode 100644 index 0000000000..a656907a24 --- /dev/null +++ b/mock-guide/english/tools/calculators/coding-bootcamp-cost-calculator/index.md @@ -0,0 +1,344 @@ +--- +title: Coding Bootcamp Cost Calculator +component: calculators/CodingBootcampCostCalculator/CodingBootcampCostCalculator.js +--- +## Coding Bootcamp Cost Calculator + + +
+ + +
+ +### Coming from , and making , your true costs will be: +
+ + + +### Notes: +
    +
  1. + We assumed an APR of 6% and a term of 3 years. If you happen + to have around $15,000 in cash set aside for a coding + bootcamp, please ignore this cost. +
  2. +
  3. + We assume a cost of living of $500 for cities like San + Francisco and New York City, and $400 per week for + everywhere else. +
  4. +
  5. + The most substantial cost for most people is lost wages. A + 40-hour-per-week job at the US Federal minimum wage would + pay at least $15,000 per year. You can read more about + economic cost + + here + . +
  6. +
+ +### Built by Suzanne Atkinson +
+ Suzanne Atkinson selfie in front of the pool +
+
+

+ Suzanne is an emergency medicine physician, triathlon + coach and web developer from Pittsburgh. You should +   + + follow her on Twitter + . +

+
+ + diff --git a/package-lock.json b/package-lock.json index 5b1e6e74e1..fff26c4c1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5304,8 +5304,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -5326,14 +5325,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5348,20 +5345,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5478,8 +5472,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5491,7 +5484,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5506,7 +5498,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5514,14 +5505,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5540,7 +5529,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5621,8 +5609,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5634,7 +5621,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5720,8 +5706,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5757,7 +5742,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5777,7 +5761,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5821,14 +5804,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } },