Skip to content

Instantly share code, notes, and snippets.

@cieloazul310
Last active July 16, 2017 06:19
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/21cb615cae477e359f20c932770ca5f0 to your computer and use it in GitHub Desktop.
Save cieloazul310/21cb615cae477e359f20c932770ca5f0 to your computer and use it in GitHub Desktop.
Bar Chart (Stacked, Sortable and Responsive)

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%;
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment