|
|
|
<!DOCTYPE html> |
|
<meta charset='utf-8'> |
|
|
|
<script src='//d3js.org/d3.v4.min.js'></script> |
|
<script> |
|
|
|
d3.csv('data.csv', function(inData) { |
|
|
|
/** 筛选2005年后的数据, 避免数据太密集不能看 */ |
|
const data = inData.filter(d => parseFloat(d['year']) > 2005); |
|
|
|
/** 给每一条数据添加一个同club名的class, 用于后面样式 */ |
|
data.forEach(d => d['class'] = d['club'].toLowerCase().replace(/ /g, '-').replace(/\./g,'')); |
|
|
|
|
|
/** |
|
* 1. 初始化配置 |
|
*/ |
|
const margin = { top: 35, right: 0, bottom: 30, left: 70 }; |
|
const width = 960 - margin.left - margin.right; |
|
const height = 500 - margin.top - margin.bottom; |
|
|
|
|
|
/** |
|
* 2. 选中图表容器 |
|
*/ |
|
const chart = d3.select('.chart') |
|
.attr('width', 960) |
|
.attr('height', 500) |
|
.append('g') |
|
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); |
|
|
|
|
|
/** |
|
* 3. 创建x轴比例尺 |
|
*/ |
|
const x = d3.scaleBand() |
|
.domain(data.map(d => d['year'])) |
|
.rangeRound([25, width - 15]); |
|
|
|
/** |
|
* 4. 创建y轴比例尺 |
|
*/ |
|
const y = d3.scaleLinear() |
|
.domain([d3.min(data, d => d['points']) / 1.1, d3.max(data, d => d['points']) * 1.05]) |
|
.range([height, 0]); |
|
|
|
/** |
|
* 5. 创建原圆比例尺 goals_for 有效的 |
|
*/ |
|
const size = d3.scaleLinear() |
|
.domain(d3.extent(data, d=> d['goals_for'])) |
|
.range([3, 10]); |
|
|
|
/** |
|
* 6. 生成x轴y轴 |
|
*/ |
|
const xAxis = d3.axisBottom(x); |
|
const yAxis = d3.axisLeft(y); |
|
|
|
chart.append('g') |
|
.attr('class', 'x axis') |
|
.attr('transform', `translate(0, ${height})`) |
|
.call(xAxis); |
|
|
|
chart.append('g') |
|
.attr('class', 'y axis') |
|
.call(yAxis); |
|
|
|
/** |
|
* 7. 生成标题 / y轴标题 |
|
*/ |
|
chart.append('text') |
|
.text('MLS: Points per Season (hover over a dot to focus, click to keep focus)') |
|
.attr('text-anchor', 'middle') |
|
.attr('class', 'graph-title') |
|
.attr('y', -10) |
|
.attr('x', width / 2.0); |
|
|
|
chart.append('text') |
|
.text('Points Per Season') |
|
.attr('text-anchor', 'middle') |
|
.attr('class', 'graph-title') |
|
.attr('y', -35) |
|
.attr('x', width / -4.0) |
|
.attr('transform', 'rotate(-90)'); |
|
|
|
|
|
/** |
|
* 8. 绘制各个节点 |
|
*/ |
|
data.forEach(d => { |
|
d.radius = size(d['goals_for']); |
|
d.x = x(d['year']) + x.bandwidth() / 2.0; |
|
d.y = y(d['points']); |
|
}); |
|
|
|
// do each year separately so the forces don't effect other years and cause the |
|
// chart to 'bulge' out |
|
/** x.domain()获取所有的年份 */ |
|
x.domain().forEach(function(year) { |
|
|
|
/** 将总数据按照俱乐部分组 */ |
|
const currData = data.filter(d => d['year'] === year); |
|
|
|
const node = chart.append('g') |
|
.attr('class', 'year-' + year) // 根据年份生成g |
|
.selectAll('circle') |
|
.data(currData) |
|
.enter().append('circle') |
|
.attr('class', 'point') |
|
.attr('cx', d => d.x) |
|
.attr('cy', d => d.y) |
|
.attr('fill', 'blue') |
|
.attr('class', d => d['class']) |
|
.attr('r', function(d) { return size(d['goals_for']) }) |
|
.attr('opacity', '0.3'); |
|
|
|
/** |
|
* d3.forceSimulation - 创建一个力模拟 |
|
* simulation.force - 添加或移除力 |
|
* d3.forceManyBody - 创建多体力 |
|
* manyBody.strength - 设置力强度 |
|
* simulation.nodes - 设置力模拟的节点 |
|
* 这里的目的是是的circle有一定的分离度,是的没那么重叠 |
|
*/ |
|
// Apply default forces to simulation |
|
const simulation = d3.forceSimulation().force('charge', d3.forceManyBody().strength(function(d) { |
|
// base it on the radius of the node |
|
const multiplier = -0.04; |
|
return Math.pow((d.radius), 1.5) * multiplier; |
|
})); |
|
|
|
// Add the nodes to the simulation, and specify how to draw |
|
simulation.nodes(currData) |
|
.on('tick', function() { |
|
// The d3 force simulation updates the x & y coordinates |
|
// of each node every tick/frame, based on the various active forces. |
|
// It is up to us to translate these coordinates to the screen. |
|
node.attr('cx', d => d.x).attr('cy', d => d.y); |
|
}); |
|
}); |
|
|
|
/** |
|
* 9. 添加tooltip, 绑定监听 |
|
*/ |
|
var tooltip = d3.select('body').append('div') |
|
.attr('class', 'tooltip'); |
|
|
|
chart.selectAll('circle') |
|
.on('mouseover', function(d) { |
|
chart.selectAll('.' + d['class']) |
|
.classed('active', true); |
|
|
|
var tooltip_str = 'Club: ' + d['club'] + |
|
'<br/>' + 'Year: ' + d['year'] + |
|
'<br/>' + 'Points: ' + d['points'] + |
|
'<br/>' + 'W/L/T: ' + d['wins'] + ' / ' + d['losses'] + ' / ' + d['ties'] + |
|
'<br/>' + 'Goals F/A: ' + d['goals_for'] + ' / ' + d['goals_against']; |
|
|
|
if(d['alias'] != '') { |
|
tooltip_str += '<br/>(aka: ' + d['alias'] + ')'; |
|
} |
|
|
|
tooltip.html(tooltip_str) |
|
.style('visibility', 'visible'); |
|
}) |
|
.on('mousemove', function(d) { |
|
tooltip.style('top', event.pageY - (tooltip.node().clientHeight + 5) + 'px') |
|
.style('left', event.pageX - (tooltip.node().clientWidth / 2.0) + 'px'); |
|
}) |
|
.on('mouseout', function(d) { |
|
chart.selectAll('.'+d['class']) |
|
.classed('active', false); |
|
|
|
tooltip.style('visibility', 'hidden'); |
|
}) |
|
.on('click', function(d) { |
|
chart.selectAll('.' + d['class']) |
|
.classed('click-active', function(d) { |
|
// toggle state |
|
return !d3.select(this).classed('click-active'); |
|
}); |
|
}) |
|
|
|
}); |
|
</script> |
|
|
|
<style> |
|
|
|
.click-active { |
|
opacity: 1.0; |
|
z-index: 1000; |
|
r: 8; |
|
} |
|
|
|
.active { |
|
opacity: 1.0; |
|
z-index: 1000; |
|
r: 8; |
|
} |
|
|
|
.axis text { |
|
font: 10px sans-serif; |
|
} |
|
|
|
.axis path, |
|
.axis line { |
|
fill: none; |
|
stroke: #000; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
.x.axis path { |
|
display: none; |
|
} |
|
|
|
.tooltip { |
|
position: absolute; |
|
padding: 10px; |
|
font: 12px sans-serif; |
|
background: #222; |
|
color: #fff; |
|
border: 0px; |
|
border-radius: 8px; |
|
pointer-events: none; |
|
opacity: 0.9; |
|
visibility: hidden; |
|
} |
|
|
|
/* soccer team colors */ |
|
/* http://teamcolors.arc90.com/ */ |
|
.chicago-fire { |
|
fill: #AF2626; |
|
stroke: #0A174A; |
|
} |
|
.colorado-rapids { |
|
fill: #91022D; |
|
stroke: #85B7EA; |
|
} |
|
.columbus-crew-sc { |
|
fill: #FFDB00; |
|
stroke: #000000; |
|
} |
|
.dc-united { |
|
fill: #DD0000; |
|
stroke: #000000; |
|
} |
|
.fc-dallas { |
|
fill: #CF0032; |
|
stroke: #07175C; |
|
} |
|
.houston-dynamo { |
|
fill: #F36600; |
|
stroke: #85b7EA; |
|
} |
|
.la-galaxy { |
|
fill: #00245D; |
|
stroke: #FFD200; |
|
} |
|
.montreal-impact { |
|
fill: #122089; |
|
stroke: #7A878F; |
|
} |
|
.new-england-revolution { |
|
fill: #0A2141; |
|
stroke: #D80016; |
|
} |
|
.new-york-city-fc { |
|
fill: #6CADDF; |
|
stroke: #00285E; |
|
} |
|
.new-york-red-bulls { |
|
fill: #FFFFFF; |
|
stroke: #D50031; |
|
} |
|
.orlando-city-sc { |
|
fill: #633492; |
|
stroke: #FDE192; |
|
} |
|
.philadelphia-union { |
|
fill: #B18500; |
|
stroke: #348AE1; |
|
} |
|
.portland-timbers { |
|
fill: #004812; |
|
stroke: #EBE72B; |
|
} |
|
.real-salt-lake { |
|
fill: #F2D11A; |
|
stroke: #A50531; |
|
} |
|
.san-jose-earthquakes { |
|
fill: #0051BA; |
|
stroke: #000000; |
|
} |
|
.seattle-sounders-fc { |
|
fill: #4F8A10; |
|
stroke: #11568C; |
|
} |
|
.sporting-kansas-city { |
|
fill: #91B0D5; |
|
stroke: #002B5C; |
|
} |
|
.toronto-fc { |
|
fill: #D80016; |
|
stroke: #313F49; |
|
} |
|
.vancouver-whitecaps-fc { |
|
fill: #12264C; |
|
stroke: #85B7EA; |
|
} |
|
|
|
/* defunct teams :( */ |
|
.tampa-bay-mutiny { /* Using tampa bay rays colors */ |
|
fill: #092C5C; |
|
stroke: #8FBCE6; |
|
} |
|
.miami-fusion { /* Using miami marlins colors */ |
|
fill: #0077C8; |
|
stroke: #FFD100; |
|
} |
|
.cd-chivas-usa { |
|
fill: #FFF; |
|
stroke: #0A2141; /* blue of new england and ny red bulls */ |
|
} |
|
|
|
</style> |
|
|
|
<body> |
|
<svg class='chart'></svg> |
|
</body> |