xxxxxxxxxx
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>泰坦尼克号船难事故乘客人口统计</title>
<style>
body {
width: 100%;
max-width: 800px;
margin: 20px auto;
text-align: center;
}
h1 {
text-align: center;
font-size: 22px;
margin: 40px auto 10px;
}
label {
margin-right: 10px;
}
.options {
margin: 30px 0;
}
.x.axis {
font-size: 16px;
}
</style>
</head>
<body>
<div class="options">
<label>分组选项:</label>
<label><input type="radio" name="groupName" value="AllPassengers" checked />所有乘客(AllPassengers)</label>
<label><input type="radio" name="groupName" value="Sex" />性别(Sex)</label>
<label><input type="radio" name="groupName" value="Pclass" />船票等级(Pclass)</label>
<label><input type="radio" name="groupName" value="HasFamily" />是否有家庭关系(HasFamily)</label>
</div>
<h3 class="caption">所有乘客(未分类)</h3>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
// 分组的常量
const GROUP_ALL = 'AllPassengers'
const GROUP_SEX = 'Sex'
const GROUP_PCLASS = 'Pclass'
const GROUP_HASFAMILY = 'HasFamily'
// 遇难生存变量
const SURVIVED_ALIVE = '生还'
const SURVIVED_DEAD = '遇难'
// 加载数据时转换数据
function dataConverter(d) {
d.Age = parseInt(d.Age)
d.Fare = parseFloat(d.Fare)
d.Pclass = parseInt(d.Pclass)
d.Survived = d.Survived === '0'? SURVIVED_DEAD : SURVIVED_ALIVE
d.HasFamily = (parseInt(d.SibSp) + parseInt(d.Parch)) > 0 ? 'Yes' : 'No'
return d
}
// 根据人口特征分类格式化加载后的数据供图表显示使用
// groupName 为分组字段 Sex, Pclass, HasFamily, AllPassengers
function formatDataForSurvivedGroup(data, groupName) {
let survivedByKeyGroup
let formatedData = []
let groupKeys = []
let GROUP_ALL_KEY = 'All passengers'
if (groupName === GROUP_ALL) {
survivedByKeyGroup = d3.nest()
.key(d => d.Survived)
.rollup(values => values.length)
.entries(data)
// properties SURVIVED_ALIVE and SURVIVED_DEAD
let survivedCount = {}
survivedByKeyGroup.forEach(element => {
survivedCount[element.key] = element.value
survivedCount[GROUP_ALL] = GROUP_ALL_KEY
});
formatedData.push(survivedCount)
groupKeys.push(GROUP_ALL_KEY)
}
if (groupName === GROUP_SEX || groupName === GROUP_PCLASS || groupName === GROUP_HASFAMILY) {
survivedByKeyGroup = d3.nest()
.key(d => d[groupName])
.key(d => d.Survived)
.rollup(values => values.length)
.entries(data)
// if you are confused by the entries data structure
// console log survivedByKeyGroup or search it on google
survivedByKeyGroup.forEach(keyGroup => {
groupKeys.push(keyGroup['key'])
let survivedCount = {}
keyGroup['values'].forEach(survived => {
survivedCount[groupName] = keyGroup['key']
survivedCount[survived['key']] = survived['value']
});
formatedData.push(survivedCount)
});
}
const groupKeyCompare = function(a, b) {
const sortedGroupKeys = [GROUP_ALL_KEY, 'male', 'female', '3', '2', '1', 'No', 'Yes']
return sortedGroupKeys.indexOf(a) - sortedGroupKeys.indexOf(b)
}
groupKeys.sort(groupKeyCompare)
return {
formatedData,
groupKeys
}
}
// 图表区域尺寸设置
const outerWidth = 600
const outerHeight = 400
const margin = { top: 20, right: 20, bottom: 30, left: 40 }
const width = outerWidth - margin.left - margin.right
const height = outerHeight - margin.top - margin.bottom
const svg = d3.select('body')
.append('svg')
.attr('width', outerWidth)
.attr('height', outerHeight)
// 图表区域
const chart = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.right})`)
// 动态缩放
const xScale = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(0.1)
const yScale = d3.scaleLinear()
.rangeRound([height, 0])
// 颜色设置对应到第三个变量的值
const zColor = d3.scaleOrdinal()
.range(['#8a89a6', '#d0743c'])
// 图表里出去 X 轴, y 轴的第三个变量 - 遇难生还情况 [SURVIVED_ALIVE, SURVIVED_DEAD]
const keys = [SURVIVED_DEAD, SURVIVED_ALIVE]
zColor.domain(keys)
// 加载数据绘图
d3.csv('titanic_data.csv', dataConverter, data => {
// 默认显示乘客未分类的遇难生还情况
let groupData = formatDataForSurvivedGroup(data, GROUP_ALL)
xScale.domain(groupData.groupKeys)
yScale.domain([0, d3.max(groupData.formatedData, d => d[SURVIVED_ALIVE] + d[SURVIVED_DEAD])]).nice()
const stack = d3.stack().keys(keys);
const series = stack(groupData.formatedData)
// stack bars
let stackBars = chart.append('g')
let keyGroups = stackBars.selectAll("g")
.data(series)
.enter()
.append('g')
.attr("fill", d => zColor(d.key))
let groupsItems = keyGroups.selectAll('rect')
.data(d => d)
.enter()
.append('rect')
.attr('x', d => xScale(d.data[GROUP_ALL]) + xScale.bandwidth() / 2 - 50)
.attr('y', d => yScale(d[1]))
.attr('height', d => yScale(d[0]) - yScale(d[1]))
.attr('width', 100)
// 数据标签
let dataLabels = keyGroups.selectAll('text')
.data(d => d)
.enter()
.append('text')
.text(d => d[1] - d[0])
.attr("text-anchor", "middle")
.attr("x", d => xScale(d.data[GROUP_ALL]) + xScale.bandwidth() / 2)
.attr("y", d => (yScale(d[0]) + yScale(d[1])) / 2)
.attr("font-family", "sans-serif")
.attr("font-size", "12px")
.attr("fill", "white");
// x axis
const xAxis = chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale))
// y axis
const yAxis = chart.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale).ticks(null, "s"))
.append("text")
.attr("x", 2)
.attr("y", yScale(yScale.ticks().pop()) + 0.5)
.attr("dy", "0.32em")
.attr("fill", "#000")
.attr("font-size", "12px")
.attr("text-anchor", "start")
.text("人数");
// legends
const legend = chart.append("g")
.attr("font-family", "SimSun")
.attr("font-size", "12px")
.attr("text-anchor", "end")
.selectAll("g")
.data(keys.slice().reverse())
.enter().append("g")
.attr("transform", function (d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 19)
.attr("width", 19)
.attr("height", 19)
.attr("fill", zColor);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9.5)
.attr("dy", "0.32em")
.text(function (d) { return d; });
// 选择选项的事件处理
d3.selectAll('[name="groupName"]')
.on('click', () => {
let groupData = undefined
const caption = d3.select('.caption')._groups[0][0]
const selectedGroup = d3.event.target.value
switch(selectedGroup) {
case GROUP_ALL:
groupData = formatDataForSurvivedGroup(data, GROUP_ALL)
caption.innerText = '所有乘客(未分类)'
break
case GROUP_SEX:
groupData = formatDataForSurvivedGroup(data, GROUP_SEX)
caption.innerText = '按性别分类'
break
case GROUP_PCLASS:
groupData = formatDataForSurvivedGroup(data, GROUP_PCLASS)
caption.innerText = '按船票等级分类(3级 < 2级 < 1级)'
break
case GROUP_HASFAMILY:
groupData = formatDataForSurvivedGroup(data, GROUP_HASFAMILY)
caption.innerText = '按是否有家庭成员同行分类'
break
default:
break
}
if (groupData) {
const series = stack(groupData.formatedData)
// update xScale and yScale
xScale.domain(groupData.groupKeys)
d3.select('.x.axis')
.call(d3.axisBottom(xScale))
let yMax = d3.max(groupData.formatedData, d => d[SURVIVED_ALIVE] + d[SURVIVED_DEAD])
yScale.domain([0, yMax]).nice()
d3.select('.y.axis')
.transition()
.call(d3.axisLeft(yScale).ticks(null, "s"))
// select keyGroup
let keyGroups = stackBars.selectAll('g').data(series)
// keyGroups enter
keyGroups.enter()
.append('g')
.attr("fill", d => zColor(d.key))
.merge(keyGroups) // update
// keyGroups exit
keyGroups.exit().remove()
// select groupsItems
let groupsItems = keyGroups.selectAll('rect').data(d => d)
// groupsItems enter
groupsItems.enter()
.append('rect')
.merge(groupsItems) // update
.attr('x', d => xScale(d.data[selectedGroup]) + xScale.bandwidth() / 2 - 50)
.attr('y', d => yScale(d[1]))
.attr('height', 0)
.attr('width', 100)
.transition()
.ease(d3.easeCircleIn)
.attr('height', d => yScale(d[0]) - yScale(d[1]))
// groupsItems exit
groupsItems.exit().remove()
// 数据标签
let dataLabels = keyGroups.selectAll('text').data(d => d)
// 数据标签 enter
dataLabels.enter()
.append('text')
.text(d => d[1] - d[0])
.merge(dataLabels)
.text(d => d[1] - d[0])
.attr("text-anchor", "middle")
.attr("x", d => xScale(d.data[selectedGroup]) + xScale.bandwidth() / 2)
.attr("y", d => yScale(d[1]))
.attr("font-family", "sans-serif")
.attr("font-size", "12px")
.attr("fill", "white")
.transition()
.ease(d3.easeCircleIn)
.attr("y", d => (yScale(d[0]) + yScale(d[1])) / 2)
// 数据标签 exit
dataLabels.exit().remove()
}
})
})
</script>
</body>
</html>
https://d3js.org/d3.v4.min.js