Skip to content

Instantly share code, notes, and snippets.

@cieloazul310
Last active May 19, 2017 17:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cieloazul310/a099d3407bb23017ba71e146ee6b4258 to your computer and use it in GitHub Desktop.
Save cieloazul310/a099d3407bb23017ba71e146ee6b4258 to your computer and use it in GitHub Desktop.
Responsive Sortable Bar Chart + Simple CSV data
license: gpl-3.0
height: 600

Responsive Sortable Bar Chart + Simple CSV data

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%;
height: 540px;
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("./j2revenue2015.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(resize)
.call(createLegend);
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(){
size.width = parseFloat(svg.style("width").slice(0,-2));
size.height = parseFloat(svg.style("height").slice(0,-2));
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 広告料収入 入場料収入 配分金 その他収入
水戸 190 81 82 208
札幌 613 424 100 283
栃木 486 128 80 242
群馬 290 71 81 106
大宮 2183 315 100 407
千葉 1641 360 93 410
東京V 564 204 82 461
横浜FC 560 156 86 200
金沢 221 74 93 196
磐田 1515 396 100 985
岐阜 516 131 85 267
京都 1133 193 89 482
C大阪 1505 464 109 634
岡山 535 176 89 374
讃岐 228 99 84 164
徳島 1098 145 84 414
愛媛 251 67 81 170
福岡 474 235 94 851
北九州 328 82 81 286
長崎 440 94 87 276
熊本 313 119 83 241
大分 497 245 84 132
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment