Skip to content

Instantly share code, notes, and snippets.

@Hirosaji
Last active February 19, 2020 02:25
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 Hirosaji/e8c44272a3863482ef27f0fd76e1589d to your computer and use it in GitHub Desktop.
Save Hirosaji/e8c44272a3863482ef27f0fd76e1589d to your computer and use it in GitHub Desktop.
d3v4 - Selective Table
# d3v4 - Selective Table
license: mit
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style type="text/css">
html, body,
.wrapper {
margin: 10px;
}
.selective-table__job, .selective-table__date, .selective-table__result {
display: table-cell;
}
table {
border: 1px #E3E3E3 solid;
border-collapse: collapse;
border-spacing: 0;
}
table th {
padding: 5px;
border: #E3E3E3 solid;
border-width: 0 0 1px 1px;
background: #F5F5F5;
font-weight: bold;
line-height: 120%;
text-align: center;
}
table td {
padding: 5px;
border: 1px #E3E3E3 solid;
border-width: 0 0 1px 1px;
text-align: center;
}
ul, ol {
border-top: solid gray 1px;
border-bottom: solid gray 1px;
padding: 0.5em 0 0.5em 1.5em;
margin: 0 20px 0 0;
}
ul li, ol li {
padding: 0.1em 0;
}
.__selected {
color: red;
}
.__checked {
color: red;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="selective-table">
<div class="selective-table__job"></div>
<div class="selective-table__date"></div>
<div class="selective-table__result"></div>
</div>
<script type="text/javascript">
d3.queue()
.defer(d3.tsv, 'job_category.tsv')
.await(function(error, job) {
drawMultiTable(job);
function drawMultiTable(inputData) {
// クラス一覧
const tableClassName = 'selective-table';
const tableClass = '.'+tableClassName;
const jobClassName = tableClassName+'__job';
const jobClass = tableClass+'__job';
const jobItemClassName = jobClassName+'__item';
const jobItemClass = '.'+jobItemClassName;
const dateClassName = tableClassName+'__date';
const dateClass = tableClass+'__date';
const dateItemClassName = dateClassName+'__item';
const dateItemClass = '.'+dateItemClassName;
const resultClassName = tableClassName+'__result';
const resultClass = tableClass+'__result';
const resultHeaderClassName = resultClassName+'__header';
const resultHeaderClass = '.'+resultHeaderClassName;
// 業種・都道府県のリストを取得
const jobs = Object.keys(inputData[0]);
jobs.some(function(e, i){ if(e==='index') jobs.splice(i, 1); });
const dates = inputData.map(function(e){ return e.index; });
let jobOrders = []; let dateOrders = []; // 各要素を追加順で管理
let lastJobOrder = 0;
// 業種・都道府県のリストタグを生成
const jobUL = d3.select(jobClass).append('ul');
const dateUL = d3.select(dateClass).append('ul');
jobUL.selectAll('li').data(jobs).enter().append('li').attr('class', jobItemClassName).text(function(d){ return d; });
dateUL.selectAll('li').data(dates).enter().append('li').attr('class', dateItemClassName).text(function(d){ return d; });
// 業種・都道府県のリストタグにクリックイベントを付与
d3.selectAll(jobItemClass).on('click', function(d){ clearTable(resultClass); setChecked(d, this); drawTable(inputData, null); });
d3.selectAll(dateItemClass).on('click', function(d){ clearTable(resultClass); setChecked(d, this); drawTable(inputData, null); });
// チェッククラスを付与・除去する関数
function setChecked(checkedLi, that){
const selector = d3.select(that);
if(selector.classed('__checked')) {
if(jobs.indexOf(checkedLi)>-1) jobOrders.some(function(e, i){ if(e===checkedLi) jobOrders.splice(i, 1); });
else dateOrders.some(function(e, i){ if(e===checkedLi) dateOrders.splice(i, 1); });
selector.classed('__checked', false);
} else {
if(jobs.indexOf(checkedLi)>-1) jobOrders.push(checkedLi);
else dateOrders.push(checkedLi);
selector.classed('__checked', true);
}
}
// ソートクラスを付与・除去する関数
function updateSelected(headers, i, clickedHeader){
const selector = d3.selectAll(resultHeaderClass);
selector.classed('__selected', false); // 古いクラスをクリア
selector.classed('__selected', function(_,j){ if(i===j) return true; });
if(clickedHeader!==null) {
var moveTarget = function(targetList) {
let target = targetList.indexOf(headers[i]);
targetList.splice(target, 1);
targetList.push(headers[i]);
}
if(jobOrders.length>0) moveTarget(jobOrders);
else moveTarget(dateOrders);
}
}
// チェック済みの要素を検出して、テーブルを描画する関数
function drawTable(inputData, clickedHeader){
let checkedJob = []; let checkedDate = [];
d3.selectAll(jobItemClass+'.__checked').attr('_', function(d){ checkedJob.push(d); });
d3.selectAll(dateItemClass+'.__checked').attr('_', function(d){ checkedDate.push(d); });
const table = d3.select(resultClass).append('table');
if(checkedJob.length > 0 && checkedDate.length > 0){
createTable(table, inputData, checkedJob, checkedDate, "job_date", clickedHeader);
}
else if(checkedJob.length > 0 && checkedDate.length === 0){
createTable(table, inputData, checkedJob, checkedDate, "job", clickedHeader);
}
else if(checkedJob.length === 0 && checkedDate.length > 0){
createTable(table, inputData, checkedDate, checkedJob, "date", clickedHeader);
}
}
// テーブルを生成する関数
function createTable(selector, inputData, headers, indexes, category, clickedHeader){
const theadText = '<tr><th></th><th class='+resultHeaderClassName+'>' + headers.join('</th><th class='+resultHeaderClassName+'>') + '</tr>';
selector.append('thead').html(theadText);
const indexList = (category==='date') ? jobs : dates;
const filterList = (category==='job_date') ? indexes : headers;
const jobList = (category==='date') ? indexes : headers;
// ソートの対象を設定(追加順が最も新しい要素でソートする)
if(clickedHeader!==null) var sortTarget = clickedHeader;
else if(jobOrders.length>0) var sortTarget = jobOrders[jobOrders.length-1];
else var sortTarget = dateOrders[dateOrders.length-1];
var sortOrder = headers.indexOf(sortTarget);
updateSelected(headers, sortOrder, clickedHeader);
const shapedData = returnShapedData(inputData, filterList, category);
sortedData(sortTarget, shapedData, indexList);
const tbody = selector.append('tbody');
const selectedTrs = tbody.selectAll("tr").exit().remove().data(shapedData);
const newTrs = selectedTrs.enter().append("tr");
const trs = selectedTrs.merge(newTrs);
trs.selectAll('td')
.data(function(d){
let filteredData = headers.map(function(e){ return d[e]; });
filteredData.unshift(d.index);
return filteredData;
})
.enter()
.append('td')
.text(function(d){
return d;
});
// 生成したテーブルのヘッダーにクリックイベントを付与
d3.selectAll(resultHeaderClass)
.on('click', function(_, i){
const that = this;
clearTable(resultClass);
sortedData(that.textContent, inputData, indexList);
drawTable(inputData, that.textContent);
});
}
// テーブルの種類ごとにデータを加工する関数
function returnShapedData(inputData, filterList, checkedCategory){
if(checkedCategory==='job') return inputData;
let filteredData = inputData.filter(function(e){ return (filterList.indexOf(e.index) > -1); });
if(checkedCategory==='job_date') return filteredData;
else if(checkedCategory==='date') {
let outputData = Object.keys(filteredData[0]).map(function(key){
let tempDict = { 'index': key };
filteredData.forEach(function(e) { tempDict[e.index] = e[key]; });
return tempDict;
});
outputData.shift();
return outputData;
}
}
// ソート機能
function sortedData(targetKey, targetData, targetList){
targetData.sort(function(a, b) { return (a[targetKey] > b[targetKey]) ? 1 : -1; });
}
// クリア機能
function clearTable(targetClass){
d3.select(targetClass).select("table").remove();
}
}
})
</script>
</div>
</body>
</html>
index 2018年5月 2018年4月 2018年3月 2018年2月 2018年1月
技術系(IT/通信) 7.75 7.72 8.02 8.01 7.98
専門職 6.68 6.84 7.04 7.36 6.69
技術系(電気/機械) 4.72 4.72 4.99 5.03 4.57
技術系(建築/土木) 3.98 3.68 4.15 4.48 4.12
営業系 2.34 2.43 2.56 2.56 2.41
技術系(メディカル) 2.11 2 2.17 2.24 2.22
クリエイティブ系 1.97 2.01 2.05 2.18 1.96
企画・事務系 1.76 1.74 1.75 1.73 1.63
販売/サービス系 1.27 1.12 1.19 1.16 1.02
技術系(化学/食品) 1.21 1.16 1.43 1.44 1.28
事務・アシスタント系 0.21 0.2 0.2 0.2 0.18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment