Skip to content

Instantly share code, notes, and snippets.

@cieloazul310
Last active May 18, 2017 15:04
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/65db28c5b06649c9e1616401db170cee to your computer and use it in GitHub Desktop.
Save cieloazul310/65db28c5b06649c9e1616401db170cee to your computer and use it in GitHub Desktop.
Responsive Sortable Bar Chart
license: gpl-3.0
height: 600

Responsive Sortable Bar Chart

D3.jsで描写したレスポンシブな棒グラフです。 ウィンドウの大きさに合わせて横幅が変化します。 棒グラフの各パーツをクリックすると、クリックした要素が一番左に配置され、その値の大きい順にグラフがソートされます。

<!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: 500px;
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;
}
.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-bottom .tick line {
stroke: silver;
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: 25%;/*
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>
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 = ["国語", "英語", "数学", "社会", "理科"];
const barColor = d3.scaleOrdinal(d3.schemeCategory10)
.domain(types);
const sumColor = d3.scaleLinear()
.range(["red", "gray", "#00cd77"]);
const data = [
{name: "浦和"}, {name: "川崎"}, {name: "G大阪"}, {name: "新潟"},
{name: "柏"}, {name: "仙台"}, {name: "札幌"}, {name: "横浜FM"},
{name: "東京"}, {name: "甲府"}, {name: "C大阪"}, {name: "神戸"},
{name: "広島"}, {name: "鳥栖"}, {name: "大宮"}, {name: "清水"},
{name: "磐田"}, {name: "鹿島"}
];
const valueExtent = {min: 0, max: 100};
let yDomain = true;
for (let x of data){
const values = d3.range(types.length)
.map(d => Math.round(Math.random() * valueExtent.max));
x.sum = d3.sum(values);
x.values = d3.range(types.length)
.map(function(d, i){return {name: x.name, type: barColor.domain()[i], value: values[i], sum: x.sum};});
calcLeft(x);
}
xScale.domain([valueExtent.min, 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)/*"black"*/)
.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 + "点";
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){
d3.select(".xaxis")
.transition().duration(duration).delay(delay)
.attr("transform", "translate(0," + margin.top + ")")
.call(xAxis);
xAxisBottom.tickSize(-(size.height-margin.top-margin.bottom));
d3.select(".xaxis-bottom")
.transition().duration(duration).delay(delay)
.attr("transform", "translate(0," + (size.height - margin.bottom) + ")")
.call(xAxisBottom);
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment