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