|
|
|
<!DOCTYPE html> |
|
<meta charset='utf-8'> |
|
|
|
<script src='//d3js.org/d3.v4.min.js'></script> |
|
<script> |
|
|
|
d3.csv('data.csv', function(data) { |
|
|
|
// For each row, calculate the 'finishing' position |
|
// first sort, year, then points, then goals_for |
|
/** 排序:按照 year, points, goals_for倒叙 */ |
|
data.sort(function(a, b) { |
|
if(b['year'] != a['year']) { |
|
return b['year'] - a['year']; |
|
} |
|
if(b['points'] != a['points']) { |
|
return b['points'] - a['points']; |
|
} |
|
if(b['goals_for'] != a['goals_for']) { |
|
return b['goals_for'] - a['goals_for']; |
|
} |
|
}); |
|
|
|
// Then add the position with a simple integer increment |
|
// now that the data is 'in order' |
|
/** 对在同一年的球队根据上面的排序结果排位, 不在同一年的排序重置为1,循环往下 */ |
|
let pos = 1; |
|
data[0].position = pos; |
|
for(let i=1; i<data.length; i++) { |
|
// this is a new year, so start over |
|
if(data[i - 1].year != data[i].year) { |
|
pos = 1; |
|
} else { |
|
pos++; |
|
} |
|
data[i].position = pos; |
|
} |
|
|
|
/** 给每一条数据添加一个同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轴比例尺 |
|
* |
|
* a. x轴序数比例尺 |
|
* b. 序数比例尺的domain范围数组可以不需要去重(新知识点) |
|
*/ |
|
const x = d3.scaleBand() |
|
.domain(data.map(d => d.year).reverse()) |
|
.rangeRound([25, width - 15]); |
|
|
|
/** |
|
* 4. 创建y轴比例尺 |
|
* |
|
* a. d3.extent等同于 d3.min 和 d3.max |
|
*/ |
|
const y = d3.scaleLinear() |
|
.domain(d3.extent(data, d => d.position)) |
|
.range([20, height - 30]); |
|
|
|
|
|
/** |
|
* 5. 生成x轴y轴 |
|
*/ |
|
const xAxis = d3.axisBottom(x); |
|
const yAxis = d3.axisLeft(y); |
|
|
|
|
|
/*** |
|
* 疑问:关于x轴的偏移 |
|
*/ |
|
chart.append('g') |
|
.attr('class', 'x axis') |
|
.attr('transform', `translate(-${x.bandwidth()/2.0}, ${height})`) |
|
.call(xAxis); |
|
|
|
chart.append('g') |
|
.attr('class', 'y axis') |
|
.call(yAxis); |
|
|
|
/** |
|
* 6. 生成标题 / y轴标题 |
|
*/ |
|
chart.append('text') |
|
.text('MLS: Position 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('Final Position') |
|
.attr('text-anchor', 'middle') |
|
.attr('class', 'graph-title') |
|
.attr('y', -35) |
|
.attr('x', width / -4.0) |
|
.attr('transform', 'rotate(-90)'); |
|
|
|
|
|
/*** |
|
* a. d3.map生成一个map类型, .keys()生成key组成的数组 |
|
* b. d3.map, 下面将d.club的值作为key值,d作为value值构成map结构 |
|
*/ |
|
const clubs = d3.map(data, d => d.club).keys(); |
|
|
|
/** |
|
* 7. 绘制折线 |
|
*/ |
|
clubs.forEach(function(club) { |
|
|
|
/** 将总数据按照俱乐部分组 */ |
|
const currData = data.filter(d => d.club === club); |
|
|
|
const className = currData[0].class; |
|
|
|
/** |
|
* d3.line 创建一个新的线生成器 |
|
* line.x 设置x访问器 |
|
* line.y 设置y访问器 |
|
*/ |
|
const line = d3.line() |
|
.x(d => x(d.year)) |
|
.y(d => y(d.position)); |
|
|
|
/** |
|
* selection.datum - 获取或设置元素的数据(不绑定)(datum方法对于选择集中的每一个元素,都为其增加一个__data__属性) |
|
* datum方法和data()的区别参考 http://www.ourd3js.com/wordpress/747/ http://www.ourd3js.com/wordpress/797/ |
|
* stroke-linejoin 属性指明路径的转角处使用的形状或者绘制的基础形状 |
|
* stroke-linecap 属性制定了,在开放子路径被设置描边的情况下,用于开放自路径两端的形状 |
|
* d: 该属性定义了一个路径 |
|
*/ |
|
chart.append('path') |
|
.datum(currData) |
|
.attr('class', className) // 过滤空格变为中划线,过滤点号,然后将club设置为css class |
|
.attr('style', 'fill:none !important') |
|
.attr('stroke-linejoin', 'round') |
|
.attr('stroke-linecap', 'round') |
|
.attr('stroke-width', 2) |
|
.attr('stroke-opacity', 0.1) |
|
.attr('d', line); |
|
}); |
|
|
|
/** |
|
* 8. 绘制各个节点 |
|
*/ |
|
const node = chart.append('g') |
|
.selectAll('circle') |
|
.data(data) |
|
.enter().append('circle') |
|
.attr('class', 'point') |
|
.attr('cx', d => x(d.year)) |
|
.attr('cy', d => y(d.position)) |
|
.attr('fill', 'blue') |
|
.attr('class', d => d.class) // 同上面一样添加一个同clue名的class |
|
.attr('r', 6) // 固定圆的半径 |
|
.attr('stroke-width', 1.5) |
|
.attr('opacity', '0.6'); |
|
|
|
/** |
|
* 9. 添加tooltip, 绑定监听 |
|
*/ |
|
const 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, .active { |
|
opacity: 1.0; |
|
stroke-opacity: 1.0; |
|
z-index: 1000; |
|
/*r: 8;*/ |
|
} |
|
|
|
path.click-active { |
|
stroke-width: 3.0; |
|
} |
|
|
|
path.active { |
|
stroke-width: 3.0; |
|
} |
|
|
|
.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> |