diff --git a/assets/src/js/components/Chart.js b/assets/src/js/components/Chart.js index 5e481a09..9bc59f6b 100644 --- a/assets/src/js/components/Chart.js +++ b/assets/src/js/components/Chart.js @@ -8,32 +8,38 @@ import * as d3 from 'd3'; import 'd3-transition'; d3.tip = require('d3-tip'); -const - formatHour = d3.timeFormat("%H"), - formatDay = d3.timeFormat("%e"), - formatMonth = d3.timeFormat("%b"), - formatMonthDay = d3.timeFormat("%b %e"), - formatYear = d3.timeFormat("%Y"); +const formatMonth = d3.timeFormat("%b"), + formatMonthDay = d3.timeFormat("%b %e"); const t = d3.transition().duration(600).ease(d3.easeQuadOut); +const xTickFormat = (len) => { + return { + hour: (d, i) => { + if(len <= 24 && d.getHours() == 0 || d.getHours() == 12) { + return d.getHours() + ":00"; + } -function timeFormatPicker(n, days) { - return function(d, i) { - if( days <= 1 ) { - return formatHour(d); - } - - if(d.getDate() === 1) { - return d.getMonth() === 0 ? formatYear(d) : formatMonth(d) - } - - if(i === 0) { - return formatMonthDay(d) - } else if(n < 32) { - return formatDay(d); - } + if(i === 0 || i === len-1) { + return formatMonthDay(d); + } + + return ''; + }, + + day: (d, i) => { + if(i === 0 || i === len-1) { + return formatMonthDay(d); + } - return ''; + return ''; + }, + month: (d, i) => { + if(len>24) { + return d.getFullYear(); + } + + return d.getMonth() === 0 ? d.getFullYear() : formatMonth(d); + } } } @@ -44,38 +50,34 @@ class Chart extends Component { this.state = { loading: false, data: [], + chartData: [], diffInDays: 1, - hoursPerTick: 24, } } - componentWillReceiveProps(newProps, newState) { - if(!this.paramsChanged(this.props, newProps)) { - return; - } - + componentWillReceiveProps(newProps) { let daysDiff = Math.round((newProps.dateRange[1]-newProps.dateRange[0])/1000/24/60/60); - let stepHours = daysDiff > 1 ? 24 : 1; this.setState({ diffInDays: daysDiff, - hoursPerTick: stepHours, + tickStep: newProps.tickStep, }) - this.fetchData(newProps) - } - - paramsChanged(o, n) { - return o.siteId != n.siteId || o.dateRange != n.dateRange; + if( newProps.siteId != this.props.siteId || newProps.dateRange[0] != this.props.dateRange[0] || newProps.dateRange[1] != this.props.dateRange[1] ) { + this.fetchData(newProps) + } else if (newProps.tickStep != this.props.tickStep) { + this.chartData() + this.redrawChart() + } } @bind - prepareData(data) { + chartData() { let startDate = this.props.dateRange[0]; let endDate = this.props.dateRange[1]; let newData = []; // instantiate JS Date objects - data = data.map((d) => { + let data = this.state.data.map(d => { d.Date = new Date(d.Date); return d }) @@ -90,11 +92,23 @@ class Chart extends Component { }; nextDate = new Date(currentDate) - nextDate.setHours(nextDate.getHours() + this.state.hoursPerTick); + + switch(this.state.tickStep) { + case 'hour': + nextDate.setHours(nextDate.getHours() + 1); + break; + + case 'day': + nextDate.setDate(nextDate.getDate() + 1); + break; + + case 'month': + nextDate.setMonth(nextDate.getMonth() + 1); + break; + } // grab data that falls between currentDate & nextDate for(let i=data.length-offset-1; i>=0; i--) { - // Because 9AM should be included in 9AM-10AM range, check for equality here if( data[i].Date >= nextDate) { break; @@ -117,10 +131,10 @@ class Chart extends Component { currentDate = nextDate; } - return newData; + this.setState({ + chartData: newData, + }) } - - @bind prepareChart() { @@ -145,9 +159,9 @@ class Chart extends Component { this.tip = d3.tip().attr('class', 'd3-tip').html((d) => { let title = d.Date.toLocaleDateString(); - if(this.state.diffInDays <= 1) { + if(this.state.tickStep === 'hour') { title += ` ${d.Date.getHours()}:00 - ${d.Date.getHours() + 1}:00` - } + } return (`
${title}
@@ -165,7 +179,7 @@ class Chart extends Component { @bind redrawChart() { - let data = this.state.data; + let data = this.state.chartData; if( ! this.ctx ) { this.prepareChart() @@ -177,14 +191,12 @@ class Chart extends Component { const max = d3.max(data, d => d.Pageviews); let x = this.x.domain(data.map(d => d.Date)) let y = this.y.domain([0, max*1.1]) - let yAxis = d3.axisLeft().scale(y).ticks(3).tickSize(-innerWidth).tickFormat((v, i) => numbers.formatPretty(v)) - let xAxis = d3.axisBottom().scale(x).tickFormat(timeFormatPicker(data.length, this.state.diffInDays)) - - // hide all "day" ticks if we're watching more than 31 items of data - if(data.length > 31) { - xAxis.tickValues(data.filter(d => d.Date.getDate() === 1).map(d => d.Date)) - } else if(data.length > 15) { - xAxis.tickValues(data.filter((d, i) => d.Date.getDate() === 1 || i === 0 || i == Math.floor((data.length-1)/2)|| i === data.length-1).map(d => d.Date)) + let yAxis = d3.axisLeft().scale(y).ticks(3).tickSize(-innerWidth).tickFormat(v => numbers.formatPretty(v)) + let xAxis = d3.axisBottom().scale(x).tickFormat(xTickFormat(data.length)[this.state.tickStep]) + + // only show first and last tick if we have more than 24 ticks to show + if(data.length > 24) { + xAxis.tickValues(data.map(d => d.Date).filter((d, i) => i === 0 || i === data.length-1)) } // empty previous graph @@ -251,17 +263,14 @@ class Chart extends Component { let after = props.dateRange[0]/1000; Client.request(`/sites/${props.siteId}/stats/site?before=${before}&after=${after}`) - .then((d) => { - // request finished; check if params changed in the meantime - if( this.paramsChanged(props, this.props)) { - return; - } + .then(data => { - let chartData = this.prepareData(d); this.setState({ loading: false, - data: chartData, + data: data, }) + + this.chartData() this.redrawChart() }) } diff --git a/assets/src/js/components/DatePicker.js b/assets/src/js/components/DatePicker.js index d069f9ab..4b02ca23 100644 --- a/assets/src/js/components/DatePicker.js +++ b/assets/src/js/components/DatePicker.js @@ -82,6 +82,7 @@ class DatePicker extends Component { period: window.location.hash.substring(2) || window.localStorage.getItem('period') || defaultPeriod, startDate: now, endDate: now, + groupBy: 'day', } this.updateDatesFromPeriod(this.state.period) } @@ -106,7 +107,6 @@ class DatePicker extends Component { @bind setDateRange(start, end, period) { // don't update state if start > end. user may be busy picking dates. - // TODO: show error if(start > end) { return; } @@ -114,11 +114,16 @@ class DatePicker extends Component { // include start & end day by forcing time start.setHours(0, 0, 0); end.setHours(23, 59, 59); + + let diff = Math.round((end - start) / 1000 / 60 / 60 / 24) + let groupBy = diff >= 31 ? 'month' : 'day'; this.setState({ period: period || '', startDate: start, endDate: end, + diff: diff, + groupBy: groupBy, }); // use slight delay for updating rest of application to allow this function to be called again @@ -188,8 +193,16 @@ class DatePicker extends Component { } } + @bind + setGroupBy(e) { + this.setState({ + groupBy: e.target.getAttribute('data-value') + }) + this.props.onChange(this.state); + } + render(props, state) { - const links = Object.keys(availablePeriods).map((id) => { + const presets = Object.keys(availablePeriods).map((id) => { let p = availablePeriods[id]; return (
  • @@ -201,11 +214,16 @@ class DatePicker extends Component { return ( ) diff --git a/assets/src/js/components/Sidebar.js b/assets/src/js/components/Sidebar.js index a192bea9..7da2f6f7 100644 --- a/assets/src/js/components/Sidebar.js +++ b/assets/src/js/components/Sidebar.js @@ -25,7 +25,7 @@ class Sidebar extends Component { } paramsChanged(o, n) { - return o.siteId != n.siteId || o.dateRange != n.dateRange; + return o.siteId != n.siteId || o.dateRange[0] != n.dateRange[0] || o.dateRange[1] != n.dateRange[1]; } @bind diff --git a/assets/src/js/components/Table.js b/assets/src/js/components/Table.js index ce9b98a2..4cb9c061 100644 --- a/assets/src/js/components/Table.js +++ b/assets/src/js/components/Table.js @@ -32,7 +32,7 @@ class Table extends Component { } paramsChanged(o, n) { - return o.siteId != n.siteId || o.dateRange != n.dateRange; + return o.siteId !== n.siteId || o.dateRange[0] != n.dateRange[0] || o.dateRange[1] != n.dateRange[1]; } @bind diff --git a/assets/src/js/pages/dashboard.js b/assets/src/js/pages/dashboard.js index f6fc03eb..b0fd6d42 100644 --- a/assets/src/js/pages/dashboard.js +++ b/assets/src/js/pages/dashboard.js @@ -26,6 +26,7 @@ class Dashboard extends Component { this.state = { dateRange: [], + groupBy: 'day', isPublic: document.cookie.indexOf('auth') < 0, site: defaultSite, sites: [], @@ -69,6 +70,7 @@ class Dashboard extends Component { this.setState({ dateRange: [ s.startDate, s.endDate ], period: s.period, + groupBy: s.groupBy, }) } @@ -165,7 +167,7 @@ class Dashboard extends Component {
    - +