D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
areologist
Full window
Github gist
trend 2
Built with
blockbuilder.org
<!DOCTYPE html> <head> <meta charset="utf-8"> <script src="https://d3js.org/d3.v4.min.js"></script> <style> body { margin: 0; } svg { border: 1px solid gray; margin: 15px; } svg text { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; cursor: default; user-select: none; } svg text::selection { background: none; } .y-axis .domain { display: none; } input:checked { border: 6px solid black; } fieldset { border: 1px solid black; margin: 15px 15px 0; } label { margin-left: 15px; } input { -webkit-appearance: none; -moz-appearance: none; border-radius: 50%; width: 16px; height: 16px; border: 2px solid #999; transition: 0.2s all linear; outline: none; margin-right: 5px; position: relative; top: 4px; } .options { display: grid; } fieldset { grid-row: 1; } </style> </head> <body> <div class="options"> <fieldset id="sourceList"> <legend>Source</legend> <div> <label for="radioNet">Net</label> <input type="radio" id="radioNet" name="trendType" value="net" checked /> <label for="radioIn">In</label> <input type="radio" id="radioIn" name="trendType" value="in" /> <label for="radioOut">Out</label> <input type="radio" id="radioOut" name="trendType" value="out" /> </div> </fieldset> <fieldset id="planList"> <legend>Plan affects</legend> <div> <label for="radioPlanIn">In</label> <input type="radio" id="radioPlanIn" name="planAffects" value="in" checked /> <label for="radioPlanOut">Out</label> <input type="radio" id="radioPlanOut" name="planAffects" value="out" /> </div> </fieldset> </div> <script> const margin = { top: 20, right: 80, bottom: 40, left: 80 }; const width = 720; const height = 340; class TrendChart { constructor() { this.dataSource = 'net'; this.planSource = 'in'; this.svg = d3.select('body') .append('svg') .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom); } init() { this.container = this.svg .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`); return new Promise((resolve, reject) => { d3.json('trend-data.json', (err, data) => { if (err) { return reject(err); } data.forEach(year => year.months.forEach(d => { d.net = parseFloat(d.actualNet); d.out = parseFloat(d.actualTotalPay); d.in = parseFloat(d.actualTotalReceived); d.plan = { in: isNumber(d.plannedReceived) ? parseFloat(d.plannedReceived) : null, out: isNumber(d.plannedPay) ? parseFloat(d.plannedPay) : null }; })); return resolve(data); }); }); } start(years) { this.setup(years); this.render(years); this._data = years; } setup(years) { this._min = d3.min(years, year => d3.min(year.months, d => d[this.dataSource])); this._max = d3.max(years, year => d3.max(year.months, d => d[this.dataSource])); this.yScale = d3.scaleLinear() .domain([this._min, this._max]) .range([height, 0]); this.xScale = d3.scaleLinear() .domain([1, 12]) .range([0, width]); } render(years) { const line = this._getLine(); const colors = this._getColors(); const pastYears = this.container .append('g') .call(this.drawPastYears.bind(this, years)); const plan = this.container .append('g') .call(this.drawPlanned.bind(this, years)); const currentYear = this.container .append('g') .call(this.drawCurrentYear.bind(this, years)); this.createAxes(); } drawPastYears(years, container) { const line = this._getLine(this.dataSource); const colors = this._getColors().slice(1).reverse(); const data = years.length > 1 ? years.slice(1) : []; const selection = container .selectAll('.past-years') .data(data, d => d.year); selection .enter() .append('path') .attr('class', 'past-years') .attr('d', d => line(d.months)) .attr('fill', 'transparent') .attr('stroke', (d, i) => colors[i]) .attr('stroke-width', 2); selection.exit().remove(); } drawCurrentYear(years, container) { const line = this._getLine(this.dataSource); const colors = this._getColors(); const data = years.length > 0 ? years.slice(0, 1) : []; container .append('path') .attr('class', 'current-year') .attr('d', line(data[0].months)) .attr('fill', 'transparent') .attr('stroke', colors[0]) .attr('stroke-width', 3); } drawPlanned(years, container) { // get 'planned' const y = 2017; const planned = years .filter(d => d.year === y) .map(d => d.months.filter(m => m.plan[this.planSource] !== null)); const line = d3.line() .x(d => this.xScale(d.month)) .y(d => this.yScale(d.plan[this.planSource])) .curve(d3.curveCatmullRom); container .append('path') .attr('class', 'planned') .attr('d', line(planned[0])) .attr('fill', 'transparent') .attr('stroke', 'black') .attr('stroke-width', 2) .attr('stroke-dasharray', '4, 4'); } changeSource(source) { this.dataSource = source; const years = this._data; this.setup(years); // update the lines const line = this._getLine(this.dataSource); this.svg .select('.current-year') .transition() .duration(750) .attr('d', line(years[0].months)); this.svg .selectAll('.past-years') .data(years.length > 1 ? years.slice(1) : []) .transition() .duration(550) .attr('d', d => line(d.months)); this.changePlan(this.planSource); this.updateAxes(); } changePlan(plan) { this.planSource = plan; const y = 2017; const planned = this._data .filter(d => d.year === y) .map(d => d.months.filter(m => m.plan[this.planSource] !== null)); const line = d3.line() .x(d => this.xScale(d.month)) .y(d => this.yScale(d.plan[this.planSource])) .curve(d3.curveCatmullRom); this.svg .select('.planned') .transition() .duration(550) .attr('d', line(planned[0])) } createAxes() { const yTicks = this._min < 0 ? [this._min, 0, this._max] : [this._min, this._max]; const yAxis = d3.axisLeft(this.yScale) .tickValues(yTicks) .tickFormat(d3.format('($,.0f')); this.container .append('g') .attr('class', 'y-axis') .attr('transform', 'translate(-10, 0)') .call(yAxis); const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec']; const xAxis = d3.axisBottom(this.xScale) .tickFormat((d, i) => months[i]); this.container .append('g') .attr('transform', `translate(0, ${height})`) .call(xAxis) .select('.domain') .remove(); } updateAxes() { const yTicks = this._min < 0 ? [this._min, 0, this._max] : [this._min, this._max]; const yAxis = d3.axisLeft(this.yScale) .tickValues(yTicks) .tickFormat(d3.format('($,.0f')); this.svg .select('.y-axis') .transition() .duration(1000) .call(yAxis); } _getLine(key) { return d3.line() .x(d => this.xScale(d.month)) .y(d => this.yScale(d[key])) .curve(d3.curveCatmullRom); } _getColors() { return ['#5998c4', '#687b86', '#b8bec0', '#dbdce0', '#f1f6fe']; } } // utility functions function isNumber(val) { return typeof val === 'number'; } window.addEventListener('load', () => { const chart = new TrendChart(); chart.init() .then((years) => chart.start(years)) .catch(err => console.error(err)); document.querySelectorAll('#sourceList input[type=radio]') .forEach(element => element.addEventListener('change', e => (chart.changeSource(e.target.value)))); document.querySelectorAll('#planList input[type=radio]') .forEach(element => element.addEventListener('change', e => (chart.changePlan(e.target.value)))); }); </script> </body>
https://d3js.org/d3.v4.min.js