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