Skip to content

Instantly share code, notes, and snippets.

@shimizu
Last active March 27, 2019 04:45
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 shimizu/7ddaab2b272cd51bf15ee728be0dba9e to your computer and use it in GitHub Desktop.
Save shimizu/7ddaab2b272cd51bf15ee728be0dba9e to your computer and use it in GitHub Desktop.
Enter, Update, Exit
license: mit

D3.jsをセレクターを用いた差分レンダリング

D3のセレクターは、単にDOM上に存在しているエレメントを選択するだけでなく、エレメントに束縛したデータをチェックし差分を抽出する機能があります。 以下3種類のセレクターを用いることで、jsxのような差分レンダリングを行うことができます。

update - 束縛したデータに対してすでにDOM存在しているエレメントを選択するセレクター enter - これから新たに追加するエレメントを選択するセレクター exit - データの数に対して多すぎるエレメントを選択するセレクター

Built with blockbuilder.org

<!DOCTYPE html>
<html lang="jp">
<head>
<style>
html, body {
margin: 0px;
padding: 0px;
width:100%;
height:100%;
}
#chart {
width: 600px;
height: 450px;
border-left: none;
border-top:none;
cursor:all-scroll;
}
#chart svg{
width: 99%;
height: 99%;
cursor: default;
}
.grid .tick line {
stroke-dasharray:1;
}
.container {
display: -webkit-flex; /* Safari */
display: flex;
-webkit-flex-direction: row; /* Safari */
flex-direction: row;
}
#data {
margin-top: 20px;
}
#data table {
width: 300px;
}
#data th {
color: black;
}
#data td {
color: white;
}
#btn {
width: 100px;
margin: 0 auto;
color:white;
background-color:#666;
}
.label {
margin-left: 40px;
margin-top: 20px;
}
.label span:before{
content: "■";
}
.label span:nth-child(1){
color: blue;
}
.label span:nth-child(2){
color: green;
}
.label span:nth-child(3){
color: red;
}
</style>
<script src="//unpkg.com/d3@4.12.2/build/d3.min.js"></script>
</head>
<body>
<div class="container">
<div id="chart" >
<div class="label">
<span>Enter</span>
<span>Update</span>
<span>Exit</span>
</div>
<svg></svg>
</div>
<div id="data">
<button id="btn">new Data</button>
<table>
<thead><tr><th>ID</th><th>X</th><th>Y</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<script src="index.js"></script>
</body>
</html>
const MaxDataLength = 10;
const rnd = (n) => ~~(Math.random() * n);
const genData = () => {
return d3.range(rnd(MaxDataLength) + 1).map((d, i) => {
return { id: i, x: rnd(100), y: rnd(100) };
});
};
const updateBtn = d3.select('#btn');
const tableBody = d3.select('#data table tbody');
const svg = d3.select('#chart').select('svg');
const grid = svg.append('g').classed('grid', true);
const plot = svg.append('g').classed('plot', true);
const axis = svg.append('g').classed('axis', true);
const yScale = d3.scaleLinear().domain([ 100, 0 ]);
const xScale = d3.scaleLinear().domain([ 0, 100 ]);
const render = ((genData) => {
const data = genData();
renderChart(data);
renderTable(data);
}).bind(null, genData);
updateBtn.on('click', render);
render();
//散布図の更新
function renderChart(data) {
const w = svg.node().clientWidth || svg.node().parentNode.clientWidth;
const h = svg.node().clientHeight || svg.node().parentNode.clientHeight;
const m = { top: 20, left: 40, right: 40, bottom: 40 };
const pw = w - (m.left + m.right);
const ph = h - (m.top + m.bottom);
yScale.range([ 0, ph ]);
xScale.range([ 0, pw ]);
//axis layer
axis.attr('transform', `translate(${m.left}, ${m.top})`);
//y axis
const yAxisUpdate = axis.selectAll('.yAxis').data([ null ]);
const yAxisEnter = yAxisUpdate.enter().append('g').classed('yAxis', true);
yAxisUpdate.merge(yAxisEnter).call(d3.axisLeft().scale(yScale));
//x axis
const xAxisUpdate = axis.selectAll('.xAxis').data([ null ]);
const xAxisEnter = xAxisUpdate.enter().append('g').classed('xAxis', true);
xAxisUpdate.merge(xAxisEnter).call(d3.axisBottom().scale(xScale)).attr('transform', `translate(0, ${ph})`);
//grid layer
grid.attr('transform', `translate(${m.left}, ${m.top})`);
//y grid
const yGridUpdate = grid.selectAll('.yGrid').data([ null ]);
const yGridEnter = yGridUpdate.enter().append('g').classed('yGrid', true);
yGridUpdate.merge(yGridEnter).call(d3.axisLeft().scale(yScale).tickSizeInner(-pw).tickFormat(() => null));
//x grid
const xGridUpdate = grid.selectAll('.xGrid').data([ null ]);
const xGridEnter = xGridUpdate.enter().append('g').classed('xGrid', true);
xGridUpdate
.merge(xGridEnter)
.call(d3.axisBottom().scale(xScale).tickSizeInner(-ph).tickFormat(() => null))
.attr('transform', `translate(0, ${ph})`);
//plot layer
plot.attr('transform', `translate(${m.left}, ${m.top})`);
/*****************************************
* enter, update, exitセレクターを用いた差分レンダリング
*****************************************/
//すでにDOM上に存在しているエレメントにデータをidをキーにしてバインドしセレクターを返す
const update = plot.selectAll('.dot').data(data, (d) => d.id);
//データの数に対して必要な数だけgエレメントを追加してセレクターを返す
const enter = update.enter().append('g').classed('dot', true);
//データの数に対して多すぎるエレメントを指定するセレクターを返す
const exit = update.exit();
//新たに追加したgエメントの子要素にcircleエレメントを追加する
enter.append('circle').attr('r', 0).style('opacity', 1);
//新たに追加したgエメントの子要素にtetエレメントを追加する
enter
.append('text')
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle')
.attr('y', '0.1em')
.attr('fill', 'white')
.text((d) => d.id);
/* 各セレクターに対して属性値の更新を行う */
//新たに追加する要素
enter
.attr('fill', 'blue') //塗り色を青に
.attr('transform', (d) => `translate(${xScale(d.x)}, ${yScale(d.y)})`) //配置する
.select('circle') //こ要素のcircleを指定
.transition()
.duration(1000) //アニメーション設定
.attr('r', 12); //半径を12pxまで徐々に大きくする
//すでに存在する要素
update
.attr('fill', 'green') //塗り色を緑に
.transition()
.duration(1000) //アニメーション設定
.attr('transform', (d) => `translate(${xScale(d.x)}, ${yScale(d.y)})`); //移動する
//削除する要素
exit
.attr('fill', 'red') //塗り色を赤に
.transition()
.duration(1000) //アニメーション設定
.style('opacity', 0) //徐々に透明にする
.on('end', function() {
//アニメーションが終了したら発火
d3.select(this).remove(); //エレメントを削除する
});
}
//テーブルの更新
function renderTable(data) {
const updateTr = tableBody.selectAll('tr').data(data, (d) => d.id);
const enterTr = updateTr.enter().append('tr');
const exitTr = updateTr.exit();
//color update
enterTr.style('background-color', 'blue');
updateTr.style('background-color', 'green');
exitTr.style('background-color', 'red');
exitTr.transition().duration(1000).style('opacity', 0).on('end', function() {
d3.select(this).remove();
});
//merge selector
const tr = updateTr.merge(enterTr).style('opacity', 1);
const updateTD = tr.selectAll('td').data(function(d) {
return Object.keys(d).map((key) => d[key]);
});
const enterTD = updateTD.enter().append('td');
const td = updateTD.merge(enterTD);
td.text((d) => d);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment