block by shimizu 7ddaab2b272cd51bf15ee728be0dba9e

Enter, Update, Exit

Full Screen

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

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

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

Built with blockbuilder.org

index.js

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);
}

index.html

<!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>