Band Members Timeline v0.2.0
3 years ago in JavaScript
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RYM Band Members Timeline v0.2.0</title>
<!-- note: moment.js must be loaded before vis-timeline-graph2d or the embedded version of moment.js is used -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.css" type="text/css" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
<link rel="shortcut icon" href="https://rateyourmusic.com/favicon.ico" type="image/x-icon"/>
<script>
function setTimeline() {
var members = $('#members').val();
var formed = $('#formed').val();
var disbanded = $('#disbanded').val();
var visualization = $('#visualization')[0];
visualization.innerHTML = '';
var band = new BandSerializer(formed, disbanded, members).createBand();
band.orderBandMembers();
var timeline = new BandTimeline(band, visualization);
timeline.makeTimeline();
$('html, body').animate({scrollTop: visualization.offsetTop}, 500);
}
class BandSerializer {
constructor(formed='', disbanded='', members='') {
this.formed = formed;
this.disbanded = disbanded;
this.members = members;
};
createBand() {
const pattern_band_members = /,(?![^\(]*\))/g;
var band = new Band('', this.formed, this.disbanded);
var members = this.members.split(pattern_band_members);
var bs = this;
$.each(members, function(index, value) {
band.members.push(bs.createMember(value));
});
return band;
};
createMember(member_string) {
const pattern_member = / *(.+?)( \[(aka )?(.+?)\])?( \((.+?)\)|^)/;
const pattern_dates = /[0-9]{4}(\-([0-9]+|present))?/;
var member = new Member();
var member_data = member_string.match(pattern_member);
if (member_data) {
member.name = member_data[1];
member.aka = member_data[4] ? member_data[4] : '';
}
var extra_data = member_data[6].split(', ');
var bs = this;
$.each(extra_data, function(index, value) {
if (value.match(pattern_dates)) {
member.periods.push(bs.createPeriod(value));
} else {
member.instruments.push(value);
}
});
if (member.periods.length == 0) {
member.periods.push(bs.createPeriod(''));
}
return member;
};
createPeriod(period_string) {
var period = new MemberPeriod(period_string);
var period_split = period_string.split('-');
if (period_string.length == '') {
period.period_from = this.formed ? new Date(this.formed, 0, 1) : new Date(1900, 0, 1);
period.period_to = this.disbanded ? new Date(this.disbanded, 0, 1) : new Date();
period.period = period.period_from.getFullYear() + '-' + period.period_to.getFullYear();
period.present = true;
} else if (period_split.length == 1) {
period.period_from = new Date(period_split[0], 0, 1);
period.period_to = new Date(Number(period_split[0])+1, 0, 1);
} else if (period_split.length == 2){
period.period_from = new Date(period_split[0], 0, 1);
if (period_split[1].length == 2) {
var year_to = period_split[0].substring(0, 2) + period_split[1];
period.period_to = new Date(year_to, 0, 1);
} else if (period_split[1].length == 4){
period.period_to = new Date(period_split[1], 0, 1);
} else if (period_split[1] == 'present'){
period.period_to = new Date();
period.present = true;
} else if (period_split[1] == '?'){
period.period_to = this.disbanded ? new Date(this.disbanded, 0, 1) : new Date();
}
}
return period;
};
}
class BandTimeline {
constructor(band=null, container=null) {
this.band = band;
this.container = container;
this.options = {selectable: false};
this.groups = new vis.DataSet();
this.items = new vis.DataSet();
this.timeline= new vis.Timeline(this.container);
}
joinBySize(list, size) {
var response = list.reduce(function (a, b) {if (a[a.length-1].length < size) {a[a.length-1].push(b)} else {a.push([b])}; return a;}, [[]]);
response = response.reduce(function (a, b) {a.push(b.join(', ')); return a}, []);
response = response.join(',</BR>');
return response;
};
makeTimeline() {
this.groups = new vis.DataSet();
this.items = new vis.DataSet();
var btl = this;
$.each(btl.band.members, function(member_idx, member) {
var member_aka = member.aka ? ' <i class="fas fa-info-circle" style="color: #003c7d;" title="' + member.aka+ '"></i>' : '';
btl.groups.add({
id: 'member_' + member_idx,
content: '<div class="member_name">' + member.name + member_aka + ' </div><div class="member_instruments">' + member.instruments.join(', ') + '</div>',
style: "max-width: 250px;",
});
var final_member = member.isFinalMember(btl.band.disbanded);
$.each(member.periods, function (period_idx, period) {
btl.items.add({
id: 'period_' + member_idx + '_' + period_idx,
group: 'member_' + member_idx,
content: period.period,
start: period.period_from,
end: period.period_to,
title: period.period,
className: final_member ? 'final_member' : '',
});
});
});
this.items.add({
id: 'band_period',
content: '',
start: this.band.formed ? this.band.formed + '-01-01' : new Date(),
end: this.band.disbanded ? this.band.disbanded + '-01-01' : new Date(),
type: 'background'
});
this.timeline.setOptions(this.options);
this.timeline.setGroups(this.groups);
this.timeline.setItems(this.items);
};
}
class Band {
constructor(name='', formed='', disbanded='', members=[]) {
this.name = name;
this.formed = formed;
this.disbanded = disbanded;
this.members = members;
};
orderBandMembers() {
var disbanded = this.disbanded;
this.members = this.members.sort(function(a, b) {
if (a.isFinalMember(disbanded) != b.isFinalMember(disbanded)) {
return (b.isFinalMember(disbanded) - a.isFinalMember(disbanded));
} else if (b.yearsActive() != a.yearsActive()) {
return (b.yearsActive() - a.yearsActive());
} else {
return (b.lastYearActive() - a.lastYearActive());
}
});
};
}
class Member {
constructor(name='', aka='', instruments=[], periods=[]) {
this.name = name;
this.aka = '';
this.instruments = instruments;
this.periods = periods;
var pattern_name = /(.+?)( \[(aka )?(.+?)\])?$/;
var splitted_name = this.name.match(pattern_name);
if (splitted_name) {
this.name = splitted_name[1];
this.aka = splitted_name[4] ? splitted_name[4] : '';
}
};
isFinalMember(disbanded=null) {
return this.periods.reduce(function (total, value){
return total || value.present || disbanded == value.period_to.getFullYear();
}, false);
};
yearsActive() {
return this.periods.reduce(function (total, value){
return total + value.period_to.getFullYear() - value.period_from.getFullYear();
}, 0);
};
lastYearActive() {
return this.periods.reduce(function (total, value){
var response = total < value.period_to.getFullYear() ? value.period_to.getFullYear() : total;
return response;
}, 0);
}
}
class MemberPeriod {
constructor(period='') {
this.period = String(period);
this.period_from = '';
this.period_to = '';
this.present = false;
};
}
</script>
<style>
body, html {
font-family: arial, sans-serif;
font-size: 11pt;
}
#visualization {
box-sizing: border-box;
width: 100%;
height: 100%;
}
.vis-item.final_member {
background-color: greenyellow;
border-color: green;
}
.member_name {
font-weight: bold;
font-size: larger;
}
.member_instruments {
font-family: Verdana,Arial,Sans-Serif;
font-size: 12px;
word-wrap: break-word;
}
.title-version{
color: lightgray;
}
</style>
</head>
<body>
<div class="container">
<h2>Rate Your Music<br/>Band Members Timeline <span class="title-version">v0.2.0</span></h2>
<form class="form-horizontal">
<div class="form-row">
<div class="col-2">
<label for="formed" class="mb-2 mr-sm-2">Formed:</label>
<input type="text" class="form-control mb-2 mr-sm-2" id="formed" placeholder="Enter year" name="formed">
</div>
<div class="col-2">
<label for="disbanded" class="mb-2 mr-sm-2">Disbanded:</label>
<input type="text" class="form-control mb-2 mr-sm-2" id="disbanded" placeholder="Enter year" name="disbanded">
</div>
</div>
<div class="row">
<div class="col-12">
<div class="form-group">
<label for="members">Members:</label>
<textarea class="form-control" rows="5" id="members" placeholder="Enter band members as is on RYM band page..."></textarea>
</div>
</div>
</div>
<div class="form-row">
<div class="col-12">
<button type="button" onclick="setTimeline()" class="btn btn-primary"><i class="fas fa-stream"></i> Make Timeline</button>
</div>
</div>
</form>
<hr>
<div id="visualization"></div>
</div>
</body>
</html>