Skip to content

Instantly share code, notes, and snippets.

@mmmatthew
Last active December 22, 2015 12:05
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 mmmatthew/3d53a3fbeb1df4b1588e to your computer and use it in GitHub Desktop.
Save mmmatthew/3d53a3fbeb1df4b1588e to your computer and use it in GitHub Desktop.
SuperSlider
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>#########</title>
<!-- <link rel="stylesheet" href="d3.slider.css" /> -->
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="wrapper">
<h1>Slider DEV</h1>
<h2>Reference date: <span class='showDate ref'></span></h2>
<h3>Start date: <span class='showDate left'></span></h3>
<h3>End date: <span class='showDate right'></span></h3>
<input type="radio" name="mode" value="obs" onchange="updateApp(this.value)">OBS<br>
<input type="radio" name="mode" value="prev" onchange="updateApp(this.value)">PREV
<div id="slider"></div>
<div id="sliderState"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<!-- // <script src="d3.slider.js"></script> -->
<script src="script.js"></script>
<script>
</script>
</body>
</html>
var opts = {
handle:{
height: 12,
width:14
},
upper:{
height:35
},
lower:{
height:60
},
margin:{
side:20,
top:20,
bottom:10
},
axis:{
height:30
},
dataBar:{
height:10,
hydro:{
},
meteo:{
}
},
workingPeriod:{
height:5
}
};
var dFormat = d3.time.format('%a %d %b @ %H:%M:%S');
opts.main={
width: d3.select('#slider')[0][0].offsetWidth-opts.margin.side*2,
height:opts.lower.height + opts.upper.height
};
opts.holder = {
width: opts.main.width+2*opts.margin.side,
height: opts.main.height + opts.margin.top + opts.margin.bottom
};
var appMode = 'prev';
var slider = {};
var prevs = {};
// init dates
var dates = {
now: new Date(),
ref: new Date(),
left: new Date(),
right: d3.time.day.offset(new Date(), 1),
max: d3.time.day.floor(d3.time.day.offset(new Date(), 3)),
min: d3.time.day.floor(d3.time.day.offset(new Date(),-6)),
minHydro: d3.time.day.floor(d3.time.day.offset(new Date(),-3))
};
var bounds ={
obs:{
left:{
min:function(){return Math.max(+dates.min,+d3.time.day.offset(dates.ref,-3));},
max:function(){return +d3.time.hour.offset(dates.ref,-1);}
},
right:{
min:function(){return Math.max(+dates.minHydro,+d3.time.hour.offset(dates.left,1));},
max:function(){return +dates.now;}
},
ref:{
min:function(){return Math.max(+dates.right,+dates.minHydro);},
max:function(){return Math.min(+dates.right,+dates.now);}
}
},
prev:{
left:{
min:function(){return Math.max(+dates.minHydro,Math.min(+prevs.hydro[0].refTime,+prevs.meteo[0].refTime));},
max:function(){return +d3.time.hour.offset(dates.right,-1);}
},
right:{
min:function(){return +d3.time.hour.offset(Math.max(dates.left,Math.min(prevs.hydro[0].refTime,prevs.meteo[0].refTime)) ,1);},
max:function(){return Math.min(+dates.max, Math.max(+prevs.hydro[0].endTime,+prevs.meteo[0].endTime));}
},
ref:{
min:function(){return +dates.minHydro;},
max:function(){return +dates.now;}
}
}
}
var sliderState = {}
// date functions
dates.startDate = function() {
return new Date(Math.min(dates.right, dates.left));
}
dates.endDate = function() {
return new Date(Math.max(dates.right, dates.left));
}
// Generate dummy data
var data = generateTestData();
// Define scale
var scale = d3.time.scale()
.domain([dates.min, dates.max])
.range([0,opts.main.width]);
// create slider
slider.holder = d3.select('#slider')
.append('svg')
.attr('width', opts.main.width+2*opts.margin.side)
.attr('height', opts.main.height + opts.margin.top + opts.margin.bottom)
slider.main = slider.holder
.append('g')
.attr('transform', 'translate(' + opts.margin.side + ',' + opts.margin.top + ')')
// background
var background = slider.holder.append('rect')
.attr('class', 'background')
.attr('y', opts.margin.top+opts.upper.height)
.attr('width', opts.holder.width)//+2*opts.margin.side)
.attr('height', opts.main.height)// + opts.margin.top + opts.margin.bottom)
// workingPeriod
var workingPeriod = slider.main.append('rect')
.attr('class', 'workingPeriod')
.attr('width', scale(dates.now)-scale(dates.minHydro))//
.attr('height', opts.workingPeriod.height)//
.attr('x', scale(dates.minHydro))
.attr('y', opts.upper.height-opts.workingPeriod.height);
slider.upper = slider.main
.append('g')
.attr('transform', 'translate(' + 0 + ',' + 0 + ')')
slider.lower = slider.main
.append('g')
.attr('transform', 'translate(' + 0 + ',' + opts.upper.height + ')')
// axis
var xDateAxis = d3.svg.axis()
.scale(scale)
.orient('top')
.ticks(d3.time.day)
.tickSize(15, 10,0)
.tickFormat(d3.time.format('%a %d'));
var xDateAxisMinor = d3.svg.axis()
.scale(scale)
.orient('bottom')
.ticks(d3.time.hour)
.tickSize(5, 10,0)
.tickFormat(function(){return '';});
slider.upper.append('g')
.attr('transform', 'translate(0,' + opts.upper.height + ')')
.attr('class', 'axis date minor')
.call(xDateAxisMinor);
slider.upper.selectAll('.axis.date.minor line')
.attr('y2',function(d){
return -Math.sin(Math.PI*(d.getHours()-6)/12)*6;
});
slider.upper.append('g')
.attr('transform', 'translate(0,' + opts.upper.height + ')')
.attr('class', 'axis date major')
.call(xDateAxis);
// reference Period
var refPeriod = slider.lower.append('rect').attr('class', 'refPeriod');
// group to hold bars
var barGroup = slider.lower.append('g');
// hydro text
barGroup.append('text').attr('class', 'hydro label');
// meteo text
barGroup.append('text').attr('class', 'meteo label');
// reference date text
slider.holder.append('text').attr('class', 'refTime label')
.attr('y', opts.margin.top-5)
.attr('text-anchor','center');
// NOW LINE
var nowline = slider.main.selectAll('.nowline').data([dates.now]);
nowline.enter()
.append('line')
.attr('y2',opts.main.height)
.attr('x2',0)
.attr('transform', function(d){return 'translate(' + scale(d) + ', 0)'})
.attr('class','nowline');
// triangles
slider.lower.append('path').attr('class', 'handle left');
slider.lower.append('path').attr('class', 'handle right');
slider.upper.append('path').attr('class', 'handle ref');
// brushing
var brushes = {};
slider.brushes = {};
slider.brush = slider.holder.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('opacity', 0.0)
.attr('fill', 'red')
.attr('class', 'brush')
.attr('width', opts.holder.width)
.attr('height', opts.holder.height)
.on('mousedown',brushInit)
// .on('mouseout',brushEnd)
.on('mousemove',brush);
brushes.fine = d3.svg.brush()
.x(scale)
.extent([0, 0])
.on("brush", brushed);
slider.brush.call(brushes.fine);
updateSlider();
function brush(){
sliderState.mouseX = d3.mouse(this)[0];
sliderState.mouseY = d3.mouse(this)[1];
if (sliderState.mouseY<opts.upper.height+opts.margin.top) {
sliderState.mode = 'ref';
}else if(sliderState.mouseX<= scale(dates.left)+opts.margin.side){
sliderState.mode = 'left';
}else if(sliderState.mouseX<= scale(dates.right)+opts.margin.side){
sliderState.mode = 'center';
}else if (sliderState.mouseX > scale(dates.right)+opts.margin.side){
sliderState.mode = 'right';
}
updateHandles();
}
function brushed() {
var value = d3.mouse(this)[0];
var mouseYrel = d3.mouse(this)[1]/opts.main.height;
var downGear = Math.max( Math.pow(mouseYrel,2), 1)
// if (mouseYrel >1){
// downGear = Math.pow(mouseYrel,2)
// }
// console.log('pos: '+mouseYrel+' | downGear: '+downGear)
if (sliderState.mode=='ref') {
if (appMode=='obs') {
dates.right = d3.time.hour.round(scale.invert(sliderState.right +(value-sliderState.mouseX)/downGear));
dates.left = d3.time.hour.round(scale.invert(sliderState.left +(value-sliderState.mouseX)/downGear));
} else{
dates.ref = d3.time.hour.round(scale.invert(sliderState.ref +(value-sliderState.mouseX)/downGear));
};
checkDates(['right','left','ref']);
}else if(sliderState.mode == 'left'){
dates.left = d3.time.hour.round(scale.invert(sliderState.left +(value-sliderState.mouseX)/downGear));
checkDates(['left','ref','right']);
}else if(sliderState.mode == 'center'){
dates.left = d3.time.hour.round(scale.invert(sliderState.left +(value-sliderState.mouseX)/downGear));
checkDates(['left','ref','right']);
dates.right = d3.time.hour.round(scale.invert(sliderState.right +(value-sliderState.mouseX)/downGear));
checkDates(['right','ref','left']);
}else if (sliderState.mode == 'right'){
dates.right = d3.time.hour.round(scale.invert(sliderState.right +(value-sliderState.mouseX)/downGear));
checkDates(['right','ref','left']);
};
// console.log(dFormat(new Date(dates.left)) + ' comp'+ dFormat(new Date(bounds.prev.left.min()))+' - '+dFormat(new Date(bounds.prev.left.max())));
// console.log(dates.left + ' comp'+ bounds.prev.left.min()+' - '+bounds.prev.left.max());
// TODO: Check for chaanges before updating
updateSlider();
updateLabels();
}
function checkDates (order) {
order.forEach(function(sliderName){
// todo: use unupdates values to avoid impossible situations
dates[sliderName] = Math.min(Math.max(dates[sliderName], bounds[appMode][sliderName].min()), bounds[appMode][sliderName].max());
})
}
function brushInit () {
sliderState.left = scale(dates.left);
sliderState.right = scale(dates.right);
sliderState.ref = scale(dates.ref);
}
function brushEnd () {
sliderState.mode = 'none';
updateHandles();
}
function updateSlider () {
// filter data
if(appMode=='prev'){
prevs.hydro = data.hydro.prev.filter(function(f){
return f.refTime < dates.ref
}).slice(-1);
var hydro = prevs.hydro;
prevs.meteo = data.meteo.prev.filter(function(f){
return f.refTime < dates.ref
}).slice(-1);
var meteo = prevs.meteo;
}else{
var hydro = data.hydro.obs;
var meteo = data.meteo.obs;
}
// Bars
var meteoBar = barGroup.selectAll('.meteo.bar').data(meteo, function(d){return d.refTime});
var hydroBar = barGroup.selectAll('.hydro.bar').data(hydro, function(d){return d.refTime});
meteoBar.exit().remove();
hydroBar.exit().remove();
meteoBar.enter()
.append('rect')
.attr('height',opts.dataBar.height)
.attr('y', 30)
.attr('class','meteo bar');
meteoBar
.attr('width',function(d){return scale(d.endTime)-scale(d.refTime)})
.attr('x', function(d){return scale(d.refTime)});
hydroBar.enter()
.append('rect')
.attr('height',opts.dataBar.height)
.attr('y', 10)
.attr('class','hydro bar');
hydroBar
.attr('width',function(d){return scale(d.endTime)-scale(d.refTime)})
.attr('x', function(d){return scale(d.refTime)});
// forecast Bar text
barGroup.selectAll('.label.meteo').data(meteo.slice(0,1))
.text(function(d){return (appMode=='obs'?'COMBIPRECIP':(d.type.toUpperCase() + ' - ' + d3.time.format('%d %b %H:%M')(d.refTime)))})
.attr('x', function(d){return (appMode=='obs'?scale(dates.now):scale(d.refTime))})
.attr('y', 30+opts.dataBar.height)
.attr('text-anchor',function(){return (appMode=='obs'?'start':'end')});
barGroup.selectAll('.label.hydro').data(hydro.slice(0,1))
.text(function(d){return (appMode=='obs'?'MESURES HYDRO':(d.type.toUpperCase() + ' - ' + d3.time.format('%d %b %H:%M')(d.refTime)))})
.attr('x', function(d){return (appMode=='obs'?scale(dates.now):scale(d.refTime))})
.attr('y', 10+opts.dataBar.height)
.attr('text-anchor',function(){return (appMode=='obs'?'start':'end')});
// Date labels
slider.holder.selectAll('.label.refTime')
.text(function(){return d3.time.format('%d %b %H:%M')(new Date(dates.ref))})
.attr('x', function(d){return scale(dates.ref)});
// range indicator
slider.lower.selectAll('.refPeriod')
.attr('height',opts.lower.height)
.attr('width',function(d){return scale(dates.endDate())-scale(dates.startDate())})
.attr('x', function(d){return scale(dates.startDate())})
.attr('y', 0);
// triangles
var triangle = 'm 0 0 l '+opts.handle.width/2+' 0 l -'+opts.handle.width/2+' -'+opts.handle.height+' l -'+opts.handle.width/2+' '+opts.handle.height+' z'
slider.lower.selectAll('.handle.left')
.attr('transform', function(){
return 'translate(' + scale(dates.left) +', '+ opts.lower.height+')'
})
.attr('d', function(d) {
return triangle;
});
slider.lower.selectAll('.handle.right')
.attr('d', function(d) {
return triangle;
})
.attr('transform', function(){
return 'translate(' + scale(dates.right) +', '+ opts.lower.height+')'
});
slider.upper.selectAll('.handle.ref')
.attr('d', function(d) {
return triangle;
})
.attr('transform', function(){
return 'translate(' + scale(dates.ref) +', 0) rotate(180)'
});
}
function updateApp (val) {
appMode = val;
checkDates(['left','right','ref'])
updateSlider();
}
function updateLabels () {
elements = document.getElementsByClassName('showDate');
Array.prototype.forEach.call(elements,function(el){
var type = el.className.split(' ')[1]
el.innerHTML = d3.time.format('%a %d %b @ %H:%M:%S')(new Date(dates[type]))
})
}
function updateHandles(){
var mode = sliderState.mode.replace('center','left,.handle.right')
slider.main.selectAll('.handle')
.classed('active', false);
slider.main.selectAll('.handle.'+mode)
.classed('active', true);
}
function generateTestData () {
// dummy forecasts
var forecasts = d3.time.scale()
.domain([dates.min, dates.now])
.ticks(d3.time.hour, 6)
.filter(function(time){
return time.getHours()<=12
})
.map(function (time) {
return {
refTime: time,
endTime: d3.time.hour.offset(time,72),
durationHours: 72,
type: 'cosmo7',
mode:'prev'
}
});
// dummy combiprecip
var cpc = d3.time.scale()
.domain([dates.min, dates.now])
.ticks(d3.time.hour, 1)
.filter(function (time){
return Math.random()>0.1 && time<d3.time.hour.offset(dates.now,-1);
})
.map(function (time) {
return {
refTime: time,
endTime: d3.time.hour.offset(time,1),
durationHours: 1,
type: 'combiprecip',
mode:'obs'
}
})
// dummy hydroObs
var hydro = [{
refTime: dates.minHydro,
endTime: dates.now,
durationHours: 1,
type: 'mesures hydro',
mode:'obs'
}];
// merge data
return {
hydro: {
obs:hydro,
prev:forecasts.slice(0,-1)
},
meteo: {
prev:forecasts,
obs:cpc
},
}
}
function generateMetaData () {
return [
{
type: 'hydro',
mode: 'obs',
name: 'mesures hydro',
sourceId: ''
},{
type: 'meteo',
mode: 'obs',
name: 'COMBIPRECIP',
sourceId: ''
},{
type: 'hydro',
mode: 'prev',
name: 'RS MINERVE',
sourceId: ''
},{
type: 'hydro',
mode: 'prev',
name: 'SwissRivers',
sourceId: ''
},{
type: 'hydro',
mode: 'prev',
name: 'WASIM',
sourceId: ''
},{
type: 'meteo',
mode: 'prev',
name: 'COSMO-7',
sourceId: ''
},{
type: 'meteo',
mode: 'prev',
name: 'COSMO-2',
sourceId: ''
},{
type: 'meteo',
mode: 'obs',
name: 'MODIS',
sourceId: ''
},
]
}
body {
font-family: Verdana,Arial,sans-serif;
}
h2 {
font-size: 1.2em;
margin: 60px 0 5px 0;
}
.wrapper {
width: 80%;
margin-left: auto;
margin-right: auto;
}
.wrapper > div {
margin: 35px 0;
}
#slider {
width: 100%;
}
/* SLIDER PREFERENCES */
.chart {
shape-rendering: crispEdges;
}
.mini text {
font: 9px sans-serif;
}
.main text {
font: 12px sans-serif;
}
.month text {
text-anchor: start;
}
.todayLine {
stroke: blue;
stroke-width: 1.5;
}
.axis line, .axis path {
stroke: black;
}
.miniItem {
stroke-width: 6;
}
.future {
stroke: gray;
fill: #ddd;
}
.past {
stroke: green;
fill: lightgreen;
}
.brush .extent {
stroke: gray;
fill: blue;
fill-opacity: .165;
}
.date .tick {
font-size: 9px;
}
.nowline {
stroke: red;
stroke-width: 0.5;
}
.refPeriod{
fill: grey;
opacity: 0.3;
}
.workingPeriod{
fill: grey;
opacity: 0.3;
}
.background {
fill: grey;
opacity: 0.1;
}
.label {
font-size: 9px;
font-style: italic;
}
.label.hydro {
}
.meteo.bar {
fill:#6F9768;
stroke:#1D4219;
stroke-width: 0.5;
}
.hydro.bar {
fill: #618CBD;
stroke:#191F42;
stroke-width: 0.5;
}
path.handle {
fill: #9A9A9A;
stroke: #6A6A6A;
stroke-width: 2;
stroke-linejoin: round;
}
path.handle.active {
stroke-width: 3.2 ;
fill:#E7E7E7;
stroke:#DD2727;
}
rect.brush {
cursor:ew-resize;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment