|
const data = { |
|
Issue: [ |
|
'Race Relations', |
|
'Education', |
|
'Terrorism', |
|
'Energy Policy', |
|
'Foreign Affairs', |
|
'Environment', |
|
'Situation in Iraq', |
|
'Taxes', |
|
'Healthcare Policy', |
|
'Economy', |
|
'Situation in Afghanistan', |
|
'Federal Budget Deficit', |
|
'Immigration', |
|
], |
|
Approve: [52, 49, 48, 47, 44, 43, 41, 41, 40, 38, 36, 31, 29], |
|
Disapprove: [38, 40, 45, 42, 48, 51, 53, 54, 57, 59, 57, 64, 62], |
|
None: [10, 11, 7, 11, 8, 6, 6, 5, 3, 3, 7, 5, 9], |
|
}; |
|
|
|
const width = 500, |
|
height = 400, |
|
margin = { top: 20, right: 100, bottom: 100, left: 40 }, |
|
chartWidth = width - margin.left - margin.right, |
|
chartHeight = height - margin.top - margin.bottom; |
|
|
|
// convert to a list of { Issue, Approve, Disapprove, None } |
|
const transformedData = data.Issue.map((Issue, index) => ({ |
|
Issue, |
|
Approve: data.Approve[index], |
|
Disapprove: data.Disapprove[index], |
|
None: data.None[index], |
|
})); |
|
|
|
const answers = ['Approve', 'Disapprove', 'None']; |
|
|
|
const x = d3 |
|
.scaleBand() |
|
.paddingInner(0.2) |
|
.range([0, chartWidth]) |
|
.domain(data.Issue); |
|
const y = d3.scaleLinear().range([chartHeight, 0]).domain([0, 100]); |
|
const z = d3 |
|
.scaleOrdinal() |
|
.range(['#809ead', '#b1c0c9', '#d7d6cb']) |
|
.domain(answers); |
|
|
|
const stack = d3.stack().keys(answers)(transformedData); |
|
|
|
const svg = d3 |
|
.select('#container') |
|
.append('svg') |
|
.attr('width', width) |
|
.attr('height', height); |
|
|
|
const chart = svg |
|
.append('g') |
|
.attr('width', chartWidth) |
|
.attr('height', chartHeight) |
|
.attr('transform', `translate(${margin.left}, ${margin.top})`); |
|
|
|
// Add the X-axis |
|
const xAxis = d3.axisBottom().scale(x).tickSize(0); |
|
|
|
chart |
|
.append('g') |
|
.attr('class', 'x axis') |
|
.attr('transform', `translate(0, ${chartHeight})`) |
|
.call(xAxis) |
|
.selectAll('text') |
|
.style('text-anchor', 'end') |
|
.attr('dy', '-.35em') |
|
.attr('transform', 'translate(0, 10), rotate(-45)'); |
|
|
|
// Add the Y-axis |
|
const yAxis = d3 |
|
.axisLeft() |
|
.scale(y) |
|
.tickFormat(d => (d === 100 ? '100%' : d)) |
|
.tickSize(15); |
|
|
|
chart |
|
.append('g') |
|
.attr('class', 'y axis') |
|
.attr('transform', 'translate(-10, 0)') |
|
.call(yAxis) |
|
.selectAll('text') |
|
.attr('transform', 'translate(16, -10)'); |
|
|
|
const serieColor = d => z(d.key); |
|
|
|
const serie = chart |
|
.selectAll('.serie') |
|
.data(stack) |
|
.enter() |
|
.append('g') |
|
.attr('class', 'serie') |
|
.attr('fill', serieColor) |
|
.on('mouseover', (d, i, nodes) => d3.select(nodes[i]).attr('fill', '#555')) |
|
.on('mouseout', (d, i, nodes) => |
|
d3.select(nodes[i]).attr('fill', serieColor) |
|
); |
|
|
|
const bar = serie |
|
.selectAll('.bar') |
|
.data((data, i, nodes) => |
|
data.map(d => { |
|
// use the parent node to get the value |
|
d.value = d.data[nodes[i].__data__.key]; |
|
return d; |
|
}) |
|
) |
|
.enter() |
|
.append('g') |
|
.attr('class', 'bar'); |
|
|
|
bar |
|
.append('rect') |
|
.attr('x', d => x(d.data.Issue)) |
|
.attr('width', x.bandwidth()) |
|
.attr('y', d => y(d[1])) |
|
.attr('height', d => y(d[0]) - y(d[1])); |
|
|
|
bar |
|
.append('text') |
|
.attr('x', d => x(d.data.Issue) + x.bandwidth() / 2) |
|
.attr('y', d => y(d[1]) + (y(d[0]) - y(d[1])) / 2) |
|
.attr('dy', '0.35em') |
|
.attr('text-anchor', 'middle') |
|
.attr('fill', '#fff') |
|
.text(d => (d.value > 11 ? d.value : '')); |
|
|
|
bar.append('title').text(d => `${d.value}%`); |
|
|
|
const legend = serie |
|
.append('g') |
|
.attr('class', 'legend') |
|
.append('text') |
|
.attr('x', chartWidth + 8) |
|
.attr('y', d => { |
|
const last = d[d.length - 1]; |
|
return y(last[1]) + (y(last[0]) - y(last[1])) / 2; |
|
}) |
|
.attr('dy', '0.35em') |
|
.attr('fill', '#000') |
|
.text(d => d.key === 'None' ? 'No Opinion': d.key); |