feat: add bootcamp cost calc guide (#35278)
* feat: add bootcamp cost calc guide * feat: use ReactJS portals * feat: fix eslint errors * feat: update with change requests * feat: update package-lock.json
This commit is contained in:
committed by
Quincy Larson
parent
9e4abad1cb
commit
869f5edfc5
56
client/package-lock.json
generated
56
client/package-lock.json
generated
@ -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="
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
@ -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 (
|
||||
<select
|
||||
className='form-control'
|
||||
defaultValue=''
|
||||
onChange={this.handleCitySelector}
|
||||
>
|
||||
<option disabled='true' value=''>
|
||||
Select City
|
||||
</option>
|
||||
{this.state.cities.map((city, idx) => {
|
||||
let cityLabel =
|
||||
typeof city !== 'undefined'
|
||||
? city.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
||||
: '';
|
||||
return (
|
||||
<option key={idx} value={city}>
|
||||
{cityLabel}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
renderSelectIncome() {
|
||||
return (
|
||||
<select
|
||||
className='form-control'
|
||||
defaultValue=''
|
||||
onChange={this.handleIncomeSelector}
|
||||
>
|
||||
<option disabled='true' value=''>
|
||||
Select Income
|
||||
</option>
|
||||
{this.state.incomes.map((income, idx) => {
|
||||
let incomeLabel =
|
||||
typeof income !== 'undefined'
|
||||
? parseInt(income, 10).toLocaleString()
|
||||
: '0';
|
||||
return (
|
||||
<option key={idx} value={income}>
|
||||
${incomeLabel}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
renderChartComponent() {
|
||||
return (
|
||||
<CostCalculatorChart
|
||||
bootcamps={this.state.bootcamps}
|
||||
city={this.state.city}
|
||||
lastYearsIncome={this.state.lastYearsIncome}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.init) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className='CodingBootcampCostCalculator'>
|
||||
{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
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const CodingBootcampCostCalculator = props => {
|
||||
const {
|
||||
data: {
|
||||
markdownRemark: { html }
|
||||
}
|
||||
} = props;
|
||||
return (
|
||||
<ArticleLayout {...props}>
|
||||
<article
|
||||
className='article'
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
id='article'
|
||||
tabIndex='-1'
|
||||
/>
|
||||
<CostCalculator />
|
||||
</ArticleLayout>
|
||||
);
|
||||
};
|
||||
|
||||
CodingBootcampCostCalculator.displayName = 'CodingBootcampCostCalculator';
|
||||
CodingBootcampCostCalculator.propTypes = propTypes;
|
||||
export default CodingBootcampCostCalculator;
|
||||
|
||||
export const pageQuery = graphql`
|
||||
query CodingBootcampCostCalculator($id: String!) {
|
||||
markdownRemark(id: { eq: $id }) {
|
||||
html
|
||||
...ArticleLayout
|
||||
}
|
||||
}
|
||||
`;
|
@ -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 <div ref={this.rootRef} />;
|
||||
}
|
||||
}
|
||||
|
||||
CostCalculatorChart.propTypes = propTypes;
|
||||
|
||||
export default CostCalculatorChart;
|
@ -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('<div>' + d.label + '<br />$' + d.value + '</div>')
|
||||
.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;
|
||||
}
|
@ -0,0 +1,344 @@
|
||||
---
|
||||
title: Coding Bootcamp Cost Calculator
|
||||
component: calculators/CodingBootcampCostCalculator/CodingBootcampCostCalculator.js
|
||||
---
|
||||
## Coding Bootcamp Cost Calculator
|
||||
|
||||
<label class='h4' id='select-city'>
|
||||
Where do you <strong>live</strong>?
|
||||
</label>
|
||||
<div id='select-city'></div>
|
||||
|
||||
<label class='h4' id='select-income'>
|
||||
How much <strong>money</strong> did you make last year (in USD)?
|
||||
</label>
|
||||
<div id='select-income'></div>
|
||||
|
||||
### Coming from <span id='city-label'></span>, and making <span id='last-years-income-label'></span>, your true costs will be:
|
||||
<div id='chart-component'></div>
|
||||
|
||||
<div class='text-center'>
|
||||
<a
|
||||
href='javascript:void(0)'
|
||||
download='bootcamps.json'
|
||||
id='bootcamps-data-link'
|
||||
>
|
||||
Save Data Source JSON
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Notes:
|
||||
<ol>
|
||||
<li>
|
||||
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.
|
||||
</li>
|
||||
<li>
|
||||
We assume a cost of living of $500 for cities like San
|
||||
Francisco and New York City, and $400 per week for
|
||||
everywhere else.
|
||||
</li>
|
||||
<li>
|
||||
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
|
||||
<a href='https://en.wikipedia.org/wiki/Economic_cost' rel='noopener noreferrer' target='_blank'>
|
||||
here
|
||||
</a>.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
### Built by Suzanne Atkinson
|
||||
<div class='col-sm-4 col-md-4'>
|
||||
<img alt='Suzanne Atkinson selfie in front of the pool' class='img-responsive testimonial-image img-center' src='https://www.evernote.com/l/AHRIBndcq-5GwZVnSy1_D7lskpH4OcJcUKUB/image.png' />
|
||||
</div>
|
||||
<div class='col-sm-8 col-md-8'>
|
||||
<p>
|
||||
Suzanne is an emergency medicine physician, triathlon
|
||||
coach and web developer from Pittsburgh. You should
|
||||
 
|
||||
<a href='https://twitter.com/intent/user?screen_name=SteelCityCoach' rel='noopener noreferrer' target='_blank'>
|
||||
follow her on Twitter
|
||||
</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script type="application/json" id="bootcamps">
|
||||
[
|
||||
{
|
||||
"name": "Hack Reactor",
|
||||
"cost": "17780",
|
||||
"housing": "500",
|
||||
"weeks": "12",
|
||||
"finance": true,
|
||||
"cities": [
|
||||
"new-york-city",
|
||||
"san-francisco"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Hack Reactor Online",
|
||||
"cost": "17780",
|
||||
"housing": "0",
|
||||
"weeks": "12",
|
||||
"finance": true,
|
||||
"cities": [
|
||||
"online"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Hackbright Academy",
|
||||
"cost": "15000",
|
||||
"housing": "500",
|
||||
"weeks": "10",
|
||||
"finance": true,
|
||||
"cities": [
|
||||
"san-francisco"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Dev Bootcamp",
|
||||
"cost": "13950",
|
||||
"finance": true,
|
||||
"housing": "500",
|
||||
"weeks": "19",
|
||||
"cities": [
|
||||
"new-york-city",
|
||||
"san-francisco",
|
||||
"chicago"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "General Asssembly",
|
||||
"cost": "11500",
|
||||
"housing": "500",
|
||||
"finance": true,
|
||||
"weeks": "12",
|
||||
"cities": [
|
||||
"washington-dc",
|
||||
"austin",
|
||||
"boston",
|
||||
"chicago",
|
||||
"hong-kong",
|
||||
"london",
|
||||
"los-angeles",
|
||||
"melbourne",
|
||||
"new-york-city",
|
||||
"san-francisco",
|
||||
"seattle",
|
||||
"singapore"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Angel Hack",
|
||||
"cost": "14250",
|
||||
"housing": "500",
|
||||
"finance": true,
|
||||
"weeks": "12",
|
||||
"cities": [
|
||||
"san-francisco"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Bitmaker Labs",
|
||||
"cost": "12000",
|
||||
"housing": "500",
|
||||
"finance": true,
|
||||
"weeks": "12",
|
||||
"cities": [
|
||||
"toronto"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CoderVox",
|
||||
"cost": "9980",
|
||||
"housing": "400",
|
||||
"finance": true,
|
||||
"weeks": "12",
|
||||
"cities": [
|
||||
"austin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Coding Dojo",
|
||||
"cost": "12500",
|
||||
"housing": "500",
|
||||
"finance": true,
|
||||
"weeks": "12",
|
||||
"cities": [
|
||||
"new-york-city",
|
||||
"san-francisco",
|
||||
"chicago"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DevMountain",
|
||||
"cost": "8900",
|
||||
"housing": "0",
|
||||
"finance": true,
|
||||
"weeks": "12",
|
||||
"cities": [
|
||||
"provo",
|
||||
"salt-lake-city",
|
||||
"dallas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Epicodus",
|
||||
"cost": "4500",
|
||||
"housing": "400",
|
||||
"finance": false,
|
||||
"weeks": "15",
|
||||
"cities": [
|
||||
"portland"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Flat Iron School",
|
||||
"cost": "15000",
|
||||
"housing": "500",
|
||||
"finance": true,
|
||||
"weeks": "12",
|
||||
"cities": [
|
||||
"new-york-city"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Galvanize",
|
||||
"cost": "21000",
|
||||
"housing": "500",
|
||||
"finance": true,
|
||||
"weeks": "24",
|
||||
"cities": [
|
||||
"boulder",
|
||||
"denver",
|
||||
"seattle",
|
||||
"san-francisco"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "The Iron Yard",
|
||||
"cost": "12000",
|
||||
"housing": "500",
|
||||
"finance": true,
|
||||
"weeks": "19",
|
||||
"cities": [
|
||||
"austin",
|
||||
"washington-dc",
|
||||
"raleigh-durham",
|
||||
"atlanta"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Launch Academy",
|
||||
"cost": "12500",
|
||||
"housing": "500",
|
||||
"finance": true,
|
||||
"weeks": "10",
|
||||
"cities": [
|
||||
"boston"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Maker Square",
|
||||
"cost": "16920",
|
||||
"housing": "500",
|
||||
"finance": true,
|
||||
"weeks": "12",
|
||||
"cities": [
|
||||
"los-angeles",
|
||||
"san-francisco",
|
||||
"austin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Refactor U",
|
||||
"cost": "13500",
|
||||
"housing": "400",
|
||||
"finance": true,
|
||||
"weeks": "10",
|
||||
"cities": [
|
||||
"boulder"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Rocket U",
|
||||
"cost": "12500",
|
||||
"housing": "500",
|
||||
"finance": true,
|
||||
"weeks": "12",
|
||||
"cities": [
|
||||
"new-york-city",
|
||||
"san-francisco",
|
||||
"chicago"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Sabio",
|
||||
"cost": "13450",
|
||||
"housing": "500",
|
||||
"finance": true,
|
||||
"weeks": "12",
|
||||
"cities": [
|
||||
"los-angeles"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Shillington School",
|
||||
"cost": "12950",
|
||||
"housing": "500",
|
||||
"finance": true,
|
||||
"weeks": "12",
|
||||
"cities": [
|
||||
"new-york-city",
|
||||
"sydney",
|
||||
"brisbane",
|
||||
"london",
|
||||
"manchester",
|
||||
"melbourne"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "The Tech Academy",
|
||||
"cost": "9000",
|
||||
"housing": "400",
|
||||
"finance": true,
|
||||
"weeks": "20",
|
||||
"cities": [
|
||||
"portland"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Viking Code School",
|
||||
"cost": "18000",
|
||||
"housing": "0",
|
||||
"finance": false,
|
||||
"weeks": "16",
|
||||
"cities": [
|
||||
"online"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "App Academy",
|
||||
"cost": "18000",
|
||||
"housing": "500",
|
||||
"finance": false,
|
||||
"weeks": "12",
|
||||
"cities": [
|
||||
"san-francisco"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Turing School",
|
||||
"cost": "17500",
|
||||
"housing": "400",
|
||||
"finance": true,
|
||||
"weeks": "27",
|
||||
"cities": [
|
||||
"denver"
|
||||
]
|
||||
}
|
||||
]
|
||||
</script>
|
41
package-lock.json
generated
41
package-lock.json
generated
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Reference in New Issue
Block a user