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 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