D3.jsで描写したレスポンシブな棒グラフです。ウィンドウの大きさに合わせて横幅が変化します。 棒グラフの各パーツをクリックすると、クリックした要素が一番左に配置され、その値の大きい順にグラフがソートされます。
表示しているデータはJクラブ個別経営情報開示資料のJ2各クラブの営業収入を2015年度を手打ちで入力したcsvファイルです。
D3.jsで描写したレスポンシブな棒グラフです。ウィンドウの大きさに合わせて横幅が変化します。 棒グラフの各パーツをクリックすると、クリックした要素が一番左に配置され、その値の大きい順にグラフがソートされます。
表示しているデータはJクラブ個別経営情報開示資料のJ2各クラブの営業収入を2015年度を手打ちで入力したcsvファイルです。
<!DOCTYPE html> | |
<html lang="ja"> | |
<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"> | |
<style> | |
.chart-container { | |
width: 100%; | |
max-width: 1000px; | |
margin: 0 auto; | |
} | |
#chart { | |
width: 100%; | |
height: 100%; | |
} | |
.color-controler ul{ | |
max-width: 1000px; | |
margin: 0 auto; | |
} | |
.color-controler li{ | |
display: inline-block; | |
margin: auto 0.4em; | |
} | |
.axis text{ | |
font-family: 'Trebuchet MS', Arial, sans-serif; | |
} | |
.yaxis text{ | |
font-size: 12px; | |
} | |
.base text{ | |
font-size: 14px; | |
font-family: 'Trebuchet MS', Arial, monospace; | |
} | |
text.part-values{ | |
font-size: 11px; | |
font-family: 'Trebuchet MS', Arial, monospace; | |
} | |
.parts rect { | |
opacity: 0.8; | |
} | |
.part-bars { | |
cursor: pointer; | |
} | |
.xaxis .tick line { | |
stroke: silver; | |
} | |
.xaxis-bottom .tick line { | |
stroke: #ddd; | |
stroke-dasharray: 2,2; | |
} | |
#info { | |
position: absolute; | |
display: none; | |
background: rgba(0,0,40,0.8); | |
font-size: 12px; | |
color: white; | |
padding: .6em; | |
-webkit-border-radius: 4px; | |
-moz-border-radius: 4px; | |
border-radius: 4px; | |
min-width: 40px; | |
max-width: 33%;/* | |
border: thin solid #aaa; | |
-webkit-box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2); | |
-moz-box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2); | |
box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2);*/ | |
} | |
#info h3, #info p { | |
margin: auto; | |
} | |
#info h3 { | |
text-align: center; | |
border-bottom: 1px gray solid; | |
} | |
</style> | |
<title>Document</title> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="color-controler"></div> | |
<div class="chart-container" id="container"> | |
<svg id="chart"></svg> | |
<div id="info"> | |
<h3>infoTitle</h3> | |
<div class="desc"></div> | |
</div> | |
</div> | |
</div> | |
</body> | |
<script> | |
d3.csv("./j1revenue2015.csv", function(err, data){ | |
if (err) throw err; | |
const size = {}; | |
const margin = {top: 30, right: 40, bottom: 20, left: 46}; | |
const svg = d3.select("#chart"); | |
const container = d3.select("#container"); | |
const win = d3.select(window); | |
const xScale = d3.scaleLinear(); | |
const yScale = d3.scaleBand().padding(0.25); | |
const xAxis = d3.axisTop(xScale); | |
const xAxisBottom = d3.axisBottom(xScale); | |
const yAxis = d3.axisLeft(yScale); | |
const types = data.columns.concat() | |
types.splice(0,1); | |
for (let x of data){ | |
const values = []; | |
for (let z of types){ | |
values.push({name: x.name, type: z, value: parseInt(x[z])}); | |
} | |
x.sum = d3.sum(values.map(d => d.value)); | |
for (let z of values){ | |
z.sum = x.sum; | |
} | |
x.values = values; | |
calcLeft(x); | |
} | |
const barColor = d3.scaleOrdinal(d3.schemeCategory10) | |
.domain(types); | |
const sumColor = d3.scaleLinear() | |
.range(["red", "gray", "#00cd77"]); | |
let yDomain = true; | |
xScale.domain([0, d3.max(data, d => d.sum)]).nice(); | |
yScale.domain(data.sort((a, b) => b.sum - a.sum).map(d => d.name)); | |
const mean = d3.mean(data, d => d.sum); | |
const far = d3.max(data, d => Math.abs(d.sum - mean)); | |
sumColor.domain([mean-far, mean, mean+far]); | |
const info = d3.select("#info"); | |
let infoMargin; | |
const g = svg.append("g").attr("class", "axis"); | |
const rows = svg.selectAll(".rows") | |
.data(data) | |
.enter() | |
.append("g") | |
.attr("class", "rows"); | |
rows.append("g") | |
.attr("class", "base") | |
.append("rect") | |
.attr("class", "base-bar") | |
.attr("y", 0) | |
.attr("stroke", "black") | |
.attr("fill", "none"); | |
rows.selectAll(".base") | |
.append("text") | |
.attr("fill", d => sumColor(d.sum)) | |
.attr("text-anchor", "end") | |
.attr("dominant-baseline", "middle") | |
.attr("dx", "-.2em") | |
.attr("dy", 1) | |
.text(d => d.sum); | |
rows.append("g") | |
.attr("class", "overlays") | |
.append("g") | |
.attr("class", "parts") | |
.selectAll(".part-bars") | |
.data(d => d.values) | |
.enter() | |
.append("g") | |
.attr("class", "part-bars") | |
.append("rect") | |
.attr("class", (v, i) => "parts-" + i) | |
.attr("fill", v => barColor(v.type)); | |
rows.selectAll(".part-bars") | |
.append("text") | |
.attr("class", "part-values") | |
.attr("fill", "white") | |
.attr("dominant-baseline", "ideographic") | |
.attr("dx", ".3em") | |
.attr("dy", -2) | |
.text(v => v.value); | |
svg.append("g") | |
.attr("class", "hovered"); | |
rows.selectAll(".part-bars") | |
.on("click", function(v){ | |
const primer = v.type === types[0]; | |
sortParts(v.type); | |
info.style("display", "none"); | |
svg.select(".hovered").selectAll("rect") | |
.transition().duration(primer ? 0 : 500) | |
.attr("x", margin.left + 1) | |
.transition().delay(primer ? 0 : 250).duration(500) | |
.attr("y", yScale(v.name)) | |
.transition().delay(500).duration(1000) | |
.remove(); | |
}) | |
.on("mouseover touchstart", function(v){ | |
info.style("display", "inline"); | |
const description = v.type + " " + (v.value / 100) + "億"; | |
createInfo(info, v.name, description, d3.format(".1%")(v.value / v.sum)); | |
svg.select(".hovered") | |
.append("rect") | |
.datum(v) | |
.attr("fill", "none") | |
.attr("stroke", "yellow") | |
.attr("stroke-width", 3) | |
.attr("x", xScale(v.left) + 1) | |
.attr("y", yScale(v.name)) | |
.attr("width", xScale(v.value) - margin.left - 1) | |
.attr("height", yScale.bandwidth()); | |
}) | |
.on("mousemove", function(){ | |
var mouse = d3.mouse(document.getElementById("chart")); | |
info.style("display", "inline") | |
.style("left", (mouse[0] + 45 + infoMargin[1]) + "px") | |
.style("top", (mouse[1] + 25 + infoMargin[0]) + "px"); | |
}) | |
.on("mouseout touchend", function(v){ | |
info.style("display", "none"); | |
svg.select(".hovered").selectAll("rect") | |
.remove(); | |
}); | |
g.append("g") | |
.attr("class", "axis xaxis"); | |
g.append("g") | |
.attr("class", "axis xaxis-bottom"); | |
g.append("g") | |
.attr("class", "axis yaxis"); | |
svg.call(createLegend) | |
.call(resize); | |
win.on("resize", resize); | |
function createLegend(){ | |
const colorControler = d3.select(".color-controler").append("ul"); | |
const markerSize = parseFloat(colorControler.style("font-size").slice(0,-2)); | |
colorControler.selectAll("li") | |
.data(barColor.domain()) | |
.enter() | |
.append("li") | |
.append("svg") | |
.attr("width", markerSize) | |
.attr("height", markerSize) | |
.attr("style", "inline") | |
.append("circle") | |
.attr("class", "color-switch active") | |
.style("cursor", "pointer") | |
.attr("cx", markerSize / 2) | |
.attr("cy", markerSize / 2) | |
.attr("r", markerSize / 2 - 1) | |
.attr("fill", d => barColor(d)) | |
.on("click", function(d, i){ | |
const channel = d3.select(this).classed("active"); | |
d3.select(this) | |
.classed("active", !channel) | |
.attr("fill", channel ? "silver" : barColor(d)); | |
rows.selectAll(".parts").selectAll(".parts-" + i) | |
.attr("fill", channel ? "none" : barColor(d)); | |
}); | |
colorControler.selectAll("li") | |
.append("span") | |
.text(d => d); | |
} | |
function resize(){ | |
const colorHeight = parseFloat(d3.select(".color-controler").style("height").slice(0,-2)); | |
size.width = parseFloat(svg.style("width").slice(0,-2));/* | |
size.height = parseFloat(svg.style("height").slice(0,-2));*/ | |
size.height = window.innerHeight - colorHeight; | |
svg.attr("width", size.width) | |
.attr("height", size.height); | |
xScale.range([margin.left, size.width - margin.right]); | |
yScale.range([margin.top, size.height - margin.bottom]); | |
moveAxis(); | |
moveBars(); | |
infoMargin = calcMargin(container); | |
} | |
function moveAxis(duration = 0, delay = 0){ | |
xAxis.tickSize(-(size.height-margin.top-margin.bottom)); | |
xAxisBottom.tickSize(-(size.height-margin.top-margin.bottom)); | |
yAxis.tickSize(0); | |
d3.select(".xaxis") | |
.transition().duration(duration).delay(delay) | |
.attr("transform", "translate(0," + margin.top + ")") | |
.call(xAxis); | |
const xAxisLength = (svg.select(".xaxis").selectAll("text").size() * 4); | |
xAxisBottom.ticks(xAxisLength); | |
d3.select(".xaxis-bottom") | |
.transition().duration(duration).delay(delay) | |
.attr("transform", "translate(0," + (size.height - margin.bottom) + ")") | |
.call(xAxisBottom); | |
svg.select(".xaxis-bottom").selectAll("text").remove(); | |
d3.select(".yaxis") | |
.transition().duration(duration).delay(delay) | |
.attr("transform", "translate(" + margin.left + ",0)") | |
.call(yAxis); | |
} | |
function moveBars(duration = 0, delay = 0){ | |
rows.transition().duration(duration).delay(delay) | |
.attr("transform", d => "translate(0," + yScale(d.name) + ")"); | |
rows.selectAll(".base").selectAll("rect") | |
.transition().duration(duration).delay(delay) | |
.attr("x", margin.left + 1) | |
.attr("width", d => xScale(d.sum) - margin.left - 1) | |
.attr("height", yScale.bandwidth()); | |
rows.selectAll(".base").selectAll("text") | |
.transition().duration(duration).delay(delay) | |
.attr("x", size.width) | |
.attr("y", yScale.bandwidth() / 2); | |
rows.selectAll(".parts").selectAll("rect") | |
.transition().duration(duration).delay(delay) | |
.attr("x", v => xScale(v.left) + 1) | |
.attr("y", "0") | |
.attr("width", v => Math.max((xScale(v.value) - margin.left - 1), 0)) | |
.attr("height", yScale.bandwidth()); | |
rows.selectAll(".parts").selectAll("text") | |
.transition().duration(duration).delay(delay) | |
.attr("transform", v => "translate(" + (xScale(v.left)) + ",0)") | |
.attr("x", 0) | |
.attr("y", yScale.bandwidth()) | |
.attr("display", v => xScale(v.value) - margin.left < 22 ? "none" : "inline"); | |
} | |
function sortParts(key){ | |
const prime = types[0]; | |
sortX(key, prime); | |
sortY(key, prime); | |
} | |
function sortX(key, prime){ | |
if(key !== prime){ | |
types.splice(types.indexOf(key), 1) | |
types.splice(0, 0, key); | |
for (let x of data){ | |
x.values.sort(function(a, b){return types.indexOf(a.type) - types.indexOf(b.type);}); | |
calcLeft(x); | |
} | |
rows.data(data); | |
moveBars(500);/* | |
moveAxis(500);*/ | |
} | |
} | |
function calcLeft(datum){ | |
let sum = 0; | |
for (let n of datum.values){ | |
n.left = sum; | |
sum += n.value; | |
} | |
} | |
function sortY(key, prime){ | |
const jake = key === prime; | |
const delay = jake ? 0 : 750; | |
const sortFunction = function(a, b){ | |
return jake && !yDomain ? b.sum - a.sum : | |
b.values[0].value - a.values[0].value; | |
}; | |
const domain = data.concat().sort(sortFunction).map(d => d.name); | |
yScale.domain(domain); | |
yDomain = jake ? !yDomain : false; | |
moveBars(500, delay); | |
moveAxis(500, delay); | |
} | |
function createInfo(element, title, ...args){ | |
element.select("h3").text(title); | |
const paraphs = element.select(".desc").selectAll("p").data(args); | |
paraphs.exit().remove(); | |
paraphs.enter().append("p").merge(paraphs) | |
.text(d => d); | |
}; | |
function calcMargin(parent){ | |
const marginArr = parent.style("margin").split(" "); | |
return marginArr.length === 1 ? [0, 0] : marginArr.map(d => parseFloat(d.slice(0,-2))); | |
} | |
}); | |
</script> | |
</html> |
name | 広告料収入 | 入場料収入 | 配分金 | その他収入 | |
---|---|---|---|---|---|
仙台 | 909 | 660 | 201 | 469 | |
山形 | 446 | 281 | 187 | 899 | |
鹿島 | 1861 | 788 | 222 | 1440 | |
浦和 | 2549 | 2174 | 270 | 1095 | |
柏 | 1928 | 518 | 186 | 387 | |
FC東京 | 1710 | 966 | 235 | 1767 | |
川崎F | 1569 | 777 | 197 | 1534 | |
横浜FM | 2256 | 948 | 204 | 1159 | |
湘南 | 573 | 335 | 193 | 460 | |
甲府 | 751 | 354 | 186 | 234 | |
松本 | 923 | 597 | 213 | 416 | |
新潟 | 1038 | 711 | 206 | 555 | |
清水 | 1417 | 511 | 220 | 954 | |
名古屋 | 2775 | 727 | 200 | 747 | |
G大阪 | 1907 | 795 | 270 | 1310 | |
神戸 | 2198 | 425 | 188 | 852 | |
広島 | 1469 | 638 | 236 | 1267 | |
鳥栖 | 1203 | 576 | 193 | 517 |