/** * A chart showing the visitors (WiFi devices) over time */ function addAxesAndLegend (svg, xAxis, yAxis, y2Axis, margin, chartWidth, chartHeight, chartObj) { var legendWidth = 200, legendHeight = 100; var axes = chartObj.axesGroup || svg.append('g'); if(addLegend && !chartObj.legendClip) { // clipping to make sure nothing appears behind legend chartObj.legendClip = svg.append('clipPath') .attr('id', 'axes-clip') .append('polygon') .attr('points', (-margin.left) + ',' + (-margin.top) + ' ' + (chartWidth - legendWidth - 1) + ',' + (-margin.top) + ' ' + (chartWidth - legendWidth - 1) + ',' + legendHeight + ' ' + (chartWidth + margin.right) + ',' + legendHeight + ' ' + (chartWidth + margin.right) + ',' + (chartHeight + margin.bottom) + ' ' + (-margin.left) + ',' + (chartHeight + margin.bottom)); axes.attr('clip-path', 'url(#axes-clip)'); } chartObj.xAxisGroup = chartObj.xAxisGroup || axes.append('g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + chartHeight + ')'); chartObj.xAxisGroup.call(xAxis); chartObj.yAxisGroup = chartObj.yAxisGroup || axes.append('g') .attr('class', 'y axis'); chartObj.yAxisGroup.call(yAxis); chartObj.yAxisText = chartObj.yAxisText || chartObj.yAxisGroup .append('text') .attr('transform', 'rotate(-90)') .attr('y', 6) .attr('dy', '.71em') .style('text-anchor', 'end') .text('Time (s)'); // chartObj.yAxis2 = chartObj.yAxis2 || axes.append('g') // .attr('class', 'y2 axis'); // chartObj.yAxis2.call(y2Axis); if(!addLegend || chartObj.legendClip) return; var legend = svg.append('g') .attr('class', 'legend') .attr('transform', 'translate(' + (chartWidth - legendWidth) + ', 0)'); legend.append('rect') .attr('class', 'legend-bg') .attr('width', legendWidth) .attr('height', legendHeight); legend.append('rect') .attr('class', 'outer') .attr('width', 75) .attr('height', 20) .attr('x', 10) .attr('y', 10); legend.append('text') .attr('x', 115) .attr('y', 25) .text('5% - 95%'); legend.append('rect') .attr('class', 'inner') .attr('width', 75) .attr('height', 20) .attr('x', 10) .attr('y', 40); legend.append('text') .attr('x', 115) .attr('y', 55) .text('25% - 75%'); legend.append('path') .attr('class', 'median-line') .attr('d', 'M10,80L85,80'); legend.append('text') .attr('x', 115) .attr('y', 85) .text('Median'); } /** @param x, y, y2 are d3.scale objects */ function drawPaths (svg, data, x, y, y2, chartObj) { var upperOuterArea = chartObj.upperOuterArea = d3.svg.area() .interpolate('basis') .x (function (d) { return x(new Date(d.key)) || 1; }) .y0(function (d) { return y2(d.values.avgDuration); }) .y1(function (d) { return y2(0); }); // var upperInnerArea = d3.svg.area() // .interpolate('basis') // .x (function (d) { return x(new Date(d.key)) || 1; }) // .y0(function (d) { return y(0); }) // .y1(function (d) { return y(d.values.avgDuration); }); var medianLine = chartObj.medianLine = d3.svg.line() .interpolate('basis') .x(function (d) { return x(new Date(d.key)); }) .y(function (d) { return y(d.values.count); }); var lowerInnerArea = chartObj.lowerInnerArea = d3.svg.area() .interpolate('basis') .x (function (d) { return x(new Date(d.key)) || 1; }) .y0(function (d) { return y(d.values.vendorOther + d.values.vendorAndroid); }) .y1(function (d) { return y(d.values.vendorOther + d.values.vendorAndroid + d.values.vendorApple); }); var lowerOuterArea = chartObj.lowerOuterArea = d3.svg.area() .interpolate('basis') .x (function (d) { return x(new Date(d.key)) || 1; }) .y0(function (d) { return y(d.values.vendorOther + d.values.vendorAndroid); }) .y1(function (d) { return y(d.values.vendorOther); }); var lowerOuterArea2 = chartObj.lowerOuterArea2 = d3.svg.area() .interpolate('basis') .x (function (d) { return x(new Date(d.key)) || 1; }) .y0(function (d) { return y(0); }) .y1(function (d) { return y(d.values.vendorOther); }); svg.datum(data); // chartObj.upperOuterAreaPath = svg.append('path') // .attr('class', 'area upper outer') // .attr('d', upperOuterArea) // .attr('clip-path', 'url(#rect-clip)'); chartObj.lowerOuterAreaPath = svg.append('path') .attr('class', 'area lower outer') .attr('d', lowerOuterArea) .attr('clip-path', 'url(#rect-clip)'); chartObj.lowerOuterArea2Path = svg.append('path') .attr('class', 'area lower outer outer2') .attr('d', lowerOuterArea2) .attr('clip-path', 'url(#rect-clip)'); // chartObj.upperInnerAreaPath = svg.append('path') // .attr('class', 'area upper inner') // .attr('d', upperInnerArea) // .attr('clip-path', 'url(#rect-clip)'); chartObj.lowerInnerAreaPath = svg.append('path') .attr('class', 'area lower inner') .attr('d', lowerInnerArea) .attr('clip-path', 'url(#rect-clip)'); chartObj.medianLinePath = svg.append('path') .attr('class', 'median-line') .attr('d', medianLine) .attr('clip-path', 'url(#rect-clip)'); } function updatePaths(data, chartObj) { var els; // chartObj.upperOuterAreaPath.data(data) // .attr('d', chartObj.upperOuterArea(data)); els = chartObj.lowerOuterAreaPath.data(data); els.attr('d', chartObj.lowerOuterArea(data)); chartObj.lowerOuterArea2Path.data(data) .attr('d', chartObj.lowerOuterArea2(data)); // chartObj.upperInnerAreaPath.data(data) // .attr('d', chartObj.upperInnerArea(data)); chartObj.lowerInnerAreaPath.data(data) .attr('d', chartObj.lowerInnerArea(data)); chartObj.medianLinePath.data(data) .attr('d', chartObj.medianLine(data)); } function addMarker (marker, svg, chartHeight, x, chartObj) { var radius = 32, xPos = x(marker.date) - radius - 3, yPosStart = chartHeight - radius - 3, // Animation start for the group yPosEnd = (marker.type === 'Duration' ? 80 : 5) + radius - 3; // Final position // Use the marker.type as a key var update = chartObj['markerGroup_' + marker.type] ? true : false; // Add a group for the marker elements in the animation start state var markerG = chartObj['markerGroup_' + marker.type] = chartObj['markerGroup_' + marker.type] || svg.append('g') .attr('class', 'marker ' + marker.type.toLowerCase()) .attr('transform', 'translate(' + xPos + ', ' + yPosStart + ')') .attr('opacity', 0); // Animate the group to appear with opacity and translate animation if(!update) { markerG.transition() // Animate if just created .duration(1000); } markerG // Update the position .attr('transform', 'translate(' + xPos + ', ' + yPosEnd + ')') .attr('opacity', 1); if(!update) { // The marker elements are now relative positioned to the group // Assume only the x position of the marker changes on updates markerG.append('path') .attr('d', 'M' + radius + ',' + (chartHeight-yPosStart) + 'L' + radius + ',' + (chartHeight-yPosStart)) .transition() .duration(1000) .attr('d', 'M' + radius + ',' + (chartHeight-yPosEnd) + 'L' + radius + ',' + (radius*2)); markerG.append('path') .attr('class', 'marker-bg hexagon') .attr('d', hexagon(radius, radius, radius)); var markerImg = null; markerImg = marker.type === 'Android' ? 'android.svg' : markerImg; markerImg = marker.type === 'iPhones' ? 'apple.svg' : markerImg; markerImg = marker.type === 'other' ? 'laptop.svg' : markerImg; markerG.append('image') .attr('width', '' + radius * 2 * 0.6) .attr('height', '' + radius * 2 * 0.6) .attr('x', '' + radius * 2 * 0.2) .attr('y', '' + radius * 2 * 0.2) .attr('href', markerImg); markerG.append('text') .attr('x', radius) .attr('y', radius*0.9) .text(marker.type); } var margerVersionTxt = chartObj['markerVersion_' + marker.type] = chartObj['markerVersion_' + marker.type] || markerG.append('text') .attr('x', radius) .attr('y', radius*1.5); margerVersionTxt.text(marker.version); // Set or update } function startTransitionsAndAddMarkers (svg, chartWidth, chartHeight, rectClip, markers, x, chartObj) { if(!chartObj.lastUpdate) { rectClip // Show the chart (with a clipping rectangle) .transition() .duration(1000*markers.length) // Synchronise with marker add animation below .attr('width', chartWidth); } if(!addMarkers) return markers.forEach(function (marker, i) { if(chartObj['markerGroup_' + marker.type]) { setTimeout(function () { addMarker(marker, svg, chartHeight, x, chartObj); }, 1000 + 500*i); }else { // No animation on update addMarker(marker, svg, chartHeight, x, chartObj); } }); } function makeChart (data, markers, durationMeanSec, chartObj) { chartObj = chartObj || {}; var svgWidth = 760, svgHeight = 440, margin = { top: 20, right: 40, bottom: 40, left: 40 }, chartWidth = svgWidth - margin.left - margin.right, chartHeight = svgHeight - margin.top - margin.bottom; var x = chartObj.x = (chartObj.x || d3.time.scale()).range([0, chartWidth]) .domain(d3.extent(data, function (d) { return new Date(d.key); })); var y = chartObj.y = (chartObj.y || d3.scale.linear()).range([chartHeight, 0]) .domain([0, d3.max(data, function (d) { return d.values.count; })]); var y2 = chartObj.y2 = (chartObj.y2 || d3.scale.linear()).range([chartHeight, 0]) .domain([0, d3.max(data, function (d) { return d.values.avgDuration; })]); var yDomain = y.domain(); y.domain([yDomain[0], yDomain[1] * 1.3]); // Extend the size for the markers var xAxis = chartObj.xAxis = (chartObj.xAxis || d3.svg.axis()).scale(x).orient('bottom') .innerTickSize(-chartHeight).outerTickSize(0).tickPadding(10); var yAxis = chartObj.yAxis = (chartObj.yAxis || d3.svg.axis()).scale(y).orient('left') .innerTickSize(-chartWidth).outerTickSize(0).tickPadding(10); var y2Axis = chartObj.y2Axis = (chartObj.y2Axis || d3.svg.axis()).scale(y2).orient('right') .innerTickSize(chartWidth).outerTickSize(0).tickPadding(10); var svg = chartObj.svg = (chartObj.svg || d3.select('.visitorChart').append('svg') .attr('width', svgWidth) .attr('height', svgHeight) .append('g') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')); // clipping to start chart hidden and slide it in later var rectClip = chartObj.clipPath = (chartObj.clipPath || svg.append('clipPath') .attr('id', 'rect-clip') .append('rect') .attr('width', 0) .attr('height', chartHeight)); if(!chartObj.lastUpdate) { // TODO also re-draw drawDurationChart(durationMeanSec); } addAxesAndLegend(svg, xAxis, yAxis, y2Axis, margin, chartWidth, chartHeight, chartObj); if(!chartObj.lastUpdate) { drawPaths(svg, data, x, y, y2, chartObj); }else { updatePaths(data, chartObj); // update the base data } startTransitionsAndAddMarkers(svg, chartWidth, chartHeight, rectClip, markers, x, chartObj); chartObj.lastUpdate = new Date(); return chartObj; } function createUpdateVisitorChart(clientsData, chartObj) { var sumData = d3.nest().key(function(d) { var start = new Date(d.date); start.setHours(start.getHours(), Math.round(start.getMinutes() / 15) * 15,0,0); return start; }).rollup(function(v) { return { count: v.length, vendorAndroid: d3.sum(v, function(d) { return androidVendors.indexOf(d.vendor) !== -1 ? 1 : 0; }), vendorApple: d3.sum(v, function(d) { return d.vendor === 'Apple' ? 1 : 0; }), vendorOther: d3.sum(v, function(d) { return (d.vendor !== 'Apple' && androidVendors.indexOf(d.vendor) === -1) ? 1 : 0; }), avgDuration: d3.mean(v, function(d) { return d.durationMs / 1000.0; }) }; }).entries(clientsData); sumData.sort(function (a,b) { return d3.ascending(new Date(a.key), new Date(b.key)); }); console.log('sumData: ', sumData); console.log(clientsData.length + ' clients found'); // Create the marker data var xScale = d3.time.scale().range([0, 1]) .domain(d3.extent(sumData, function (d) { return new Date(d.key); })); var timeFormat = d3.time.format('%H:%M'); var markers = []; markers.push({ date: xScale.invert(0.10), type: 'Visitors', version: timeFormat(xScale.invert(0.10))}); markers.push({ date: xScale.invert(1.001), type: 'Visitors ', version: 'Now'}); markers.push({ date: xScale.invert(0.20), type: 'other', version: timeFormat(xScale.invert(0.20))}); markers.push({ date: xScale.invert(0.30), type: 'Android', version: timeFormat(xScale.invert(0.30))}); markers.push({ date: xScale.invert(0.40), type: 'iPhones', version: timeFormat(xScale.invert(0.40))}); // markers.push({ date: xScale.invert(0.8), // type: 'Duration', version: '01:38'}); // Create the duration data var durationMeanSec = d3.mean(clientsData, function(d) { return d.durationMs / 1000.0; }); return makeChart(sumData, markers, durationMeanSec, chartObj); }