mirror of https://github.com/status-im/fathom.git
add chart group options to navbar (hour, day, month).
we should probably enforce 1st of month date when choosing 'month'
This commit is contained in:
parent
cae987cfbf
commit
d62c9b9f81
|
@ -8,32 +8,38 @@ import * as d3 from 'd3';
|
||||||
import 'd3-transition';
|
import 'd3-transition';
|
||||||
d3.tip = require('d3-tip');
|
d3.tip = require('d3-tip');
|
||||||
|
|
||||||
const
|
const formatMonth = d3.timeFormat("%b"),
|
||||||
formatHour = d3.timeFormat("%H"),
|
formatMonthDay = d3.timeFormat("%b %e");
|
||||||
formatDay = d3.timeFormat("%e"),
|
|
||||||
formatMonth = d3.timeFormat("%b"),
|
|
||||||
formatMonthDay = d3.timeFormat("%b %e"),
|
|
||||||
formatYear = d3.timeFormat("%Y");
|
|
||||||
|
|
||||||
const t = d3.transition().duration(600).ease(d3.easeQuadOut);
|
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) {
|
if(i === 0 || i === len-1) {
|
||||||
return function(d, i) {
|
return formatMonthDay(d);
|
||||||
if( days <= 1 ) {
|
}
|
||||||
return formatHour(d);
|
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
|
||||||
|
day: (d, i) => {
|
||||||
|
if(i === 0 || i === len-1) {
|
||||||
|
return formatMonthDay(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
month: (d, i) => {
|
||||||
|
if(len>24) {
|
||||||
|
return d.getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.getMonth() === 0 ? d.getFullYear() : formatMonth(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,38 +50,34 @@ class Chart extends Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: false,
|
loading: false,
|
||||||
data: [],
|
data: [],
|
||||||
|
chartData: [],
|
||||||
diffInDays: 1,
|
diffInDays: 1,
|
||||||
hoursPerTick: 24,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(newProps, newState) {
|
componentWillReceiveProps(newProps) {
|
||||||
if(!this.paramsChanged(this.props, newProps)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let daysDiff = Math.round((newProps.dateRange[1]-newProps.dateRange[0])/1000/24/60/60);
|
let daysDiff = Math.round((newProps.dateRange[1]-newProps.dateRange[0])/1000/24/60/60);
|
||||||
let stepHours = daysDiff > 1 ? 24 : 1;
|
|
||||||
this.setState({
|
this.setState({
|
||||||
diffInDays: daysDiff,
|
diffInDays: daysDiff,
|
||||||
hoursPerTick: stepHours,
|
tickStep: newProps.tickStep,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.fetchData(newProps)
|
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) {
|
||||||
paramsChanged(o, n) {
|
this.chartData()
|
||||||
return o.siteId != n.siteId || o.dateRange != n.dateRange;
|
this.redrawChart()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
prepareData(data) {
|
chartData() {
|
||||||
let startDate = this.props.dateRange[0];
|
let startDate = this.props.dateRange[0];
|
||||||
let endDate = this.props.dateRange[1];
|
let endDate = this.props.dateRange[1];
|
||||||
let newData = [];
|
let newData = [];
|
||||||
|
|
||||||
// instantiate JS Date objects
|
// instantiate JS Date objects
|
||||||
data = data.map((d) => {
|
let data = this.state.data.map(d => {
|
||||||
d.Date = new Date(d.Date);
|
d.Date = new Date(d.Date);
|
||||||
return d
|
return d
|
||||||
})
|
})
|
||||||
|
@ -90,11 +92,23 @@ class Chart extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
nextDate = new Date(currentDate)
|
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
|
// grab data that falls between currentDate & nextDate
|
||||||
for(let i=data.length-offset-1; i>=0; i--) {
|
for(let i=data.length-offset-1; i>=0; i--) {
|
||||||
|
|
||||||
// Because 9AM should be included in 9AM-10AM range, check for equality here
|
// Because 9AM should be included in 9AM-10AM range, check for equality here
|
||||||
if( data[i].Date >= nextDate) {
|
if( data[i].Date >= nextDate) {
|
||||||
break;
|
break;
|
||||||
|
@ -117,11 +131,11 @@ class Chart extends Component {
|
||||||
currentDate = nextDate;
|
currentDate = nextDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
return newData;
|
this.setState({
|
||||||
|
chartData: newData,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
prepareChart() {
|
prepareChart() {
|
||||||
let padding = { top: 12, right: 12, bottom: 24, left: 40 };
|
let padding = { top: 12, right: 12, bottom: 24, left: 40 };
|
||||||
|
@ -145,7 +159,7 @@ class Chart extends Component {
|
||||||
this.tip = d3.tip().attr('class', 'd3-tip').html((d) => {
|
this.tip = d3.tip().attr('class', 'd3-tip').html((d) => {
|
||||||
let title = d.Date.toLocaleDateString();
|
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`
|
title += ` ${d.Date.getHours()}:00 - ${d.Date.getHours() + 1}:00`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +179,7 @@ class Chart extends Component {
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
redrawChart() {
|
redrawChart() {
|
||||||
let data = this.state.data;
|
let data = this.state.chartData;
|
||||||
|
|
||||||
if( ! this.ctx ) {
|
if( ! this.ctx ) {
|
||||||
this.prepareChart()
|
this.prepareChart()
|
||||||
|
@ -177,14 +191,12 @@ class Chart extends Component {
|
||||||
const max = d3.max(data, d => d.Pageviews);
|
const max = d3.max(data, d => d.Pageviews);
|
||||||
let x = this.x.domain(data.map(d => d.Date))
|
let x = this.x.domain(data.map(d => d.Date))
|
||||||
let y = this.y.domain([0, max*1.1])
|
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 yAxis = d3.axisLeft().scale(y).ticks(3).tickSize(-innerWidth).tickFormat(v => numbers.formatPretty(v))
|
||||||
let xAxis = d3.axisBottom().scale(x).tickFormat(timeFormatPicker(data.length, this.state.diffInDays))
|
let xAxis = d3.axisBottom().scale(x).tickFormat(xTickFormat(data.length)[this.state.tickStep])
|
||||||
|
|
||||||
// hide all "day" ticks if we're watching more than 31 items of data
|
// only show first and last tick if we have more than 24 ticks to show
|
||||||
if(data.length > 31) {
|
if(data.length > 24) {
|
||||||
xAxis.tickValues(data.filter(d => d.Date.getDate() === 1).map(d => d.Date))
|
xAxis.tickValues(data.map(d => d.Date).filter((d, i) => i === 0 || i === data.length-1))
|
||||||
} 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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// empty previous graph
|
// empty previous graph
|
||||||
|
@ -251,17 +263,14 @@ class Chart extends Component {
|
||||||
let after = props.dateRange[0]/1000;
|
let after = props.dateRange[0]/1000;
|
||||||
|
|
||||||
Client.request(`/sites/${props.siteId}/stats/site?before=${before}&after=${after}`)
|
Client.request(`/sites/${props.siteId}/stats/site?before=${before}&after=${after}`)
|
||||||
.then((d) => {
|
.then(data => {
|
||||||
// request finished; check if params changed in the meantime
|
|
||||||
if( this.paramsChanged(props, this.props)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let chartData = this.prepareData(d);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
data: chartData,
|
data: data,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.chartData()
|
||||||
this.redrawChart()
|
this.redrawChart()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,7 @@ class DatePicker extends Component {
|
||||||
period: window.location.hash.substring(2) || window.localStorage.getItem('period') || defaultPeriod,
|
period: window.location.hash.substring(2) || window.localStorage.getItem('period') || defaultPeriod,
|
||||||
startDate: now,
|
startDate: now,
|
||||||
endDate: now,
|
endDate: now,
|
||||||
|
groupBy: 'day',
|
||||||
}
|
}
|
||||||
this.updateDatesFromPeriod(this.state.period)
|
this.updateDatesFromPeriod(this.state.period)
|
||||||
}
|
}
|
||||||
|
@ -106,7 +107,6 @@ class DatePicker extends Component {
|
||||||
@bind
|
@bind
|
||||||
setDateRange(start, end, period) {
|
setDateRange(start, end, period) {
|
||||||
// don't update state if start > end. user may be busy picking dates.
|
// don't update state if start > end. user may be busy picking dates.
|
||||||
// TODO: show error
|
|
||||||
if(start > end) {
|
if(start > end) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -115,10 +115,15 @@ class DatePicker extends Component {
|
||||||
start.setHours(0, 0, 0);
|
start.setHours(0, 0, 0);
|
||||||
end.setHours(23, 59, 59);
|
end.setHours(23, 59, 59);
|
||||||
|
|
||||||
|
let diff = Math.round((end - start) / 1000 / 60 / 60 / 24)
|
||||||
|
let groupBy = diff >= 31 ? 'month' : 'day';
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
period: period || '',
|
period: period || '',
|
||||||
startDate: start,
|
startDate: start,
|
||||||
endDate: end,
|
endDate: end,
|
||||||
|
diff: diff,
|
||||||
|
groupBy: groupBy,
|
||||||
});
|
});
|
||||||
|
|
||||||
// use slight delay for updating rest of application to allow this function to be called again
|
// 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) {
|
render(props, state) {
|
||||||
const links = Object.keys(availablePeriods).map((id) => {
|
const presets = Object.keys(availablePeriods).map((id) => {
|
||||||
let p = availablePeriods[id];
|
let p = availablePeriods[id];
|
||||||
return (
|
return (
|
||||||
<li class={classNames({ current: id == state.period })}>
|
<li class={classNames({ current: id == state.period })}>
|
||||||
|
@ -201,11 +214,16 @@ class DatePicker extends Component {
|
||||||
return (
|
return (
|
||||||
<nav class="date-nav sm ac">
|
<nav class="date-nav sm ac">
|
||||||
<ul>
|
<ul>
|
||||||
{links}
|
{presets}
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li><Pikadayer value={this.dateValue(state.startDate)} onSelect={this.setStartDate} /> <span>›</span> <Pikadayer value={this.dateValue(state.endDate)} onSelect={this.setEndDate} /></li>
|
<li><Pikadayer value={this.dateValue(state.startDate)} onSelect={this.setStartDate} /> <span>›</span> <Pikadayer value={this.dateValue(state.endDate)} onSelect={this.setEndDate} /></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
<ul>
|
||||||
|
{state.diff < 30 ? (<li class={classNames({ current: 'hour' === state.groupBy })}><a href="#" data-value="hour" onClick={this.setGroupBy}>Hourly</a></li>) : ''}
|
||||||
|
<li class={classNames({ current: 'day' === state.groupBy })}><a href="#" data-value="day" onClick={this.setGroupBy}>Daily</a></li>
|
||||||
|
{state.diff >= 30 ? (<li class={classNames({ current: 'month' === state.groupBy })}><a href="#" data-value="month" onClick={this.setGroupBy}>Monthly</a></li>) : ''}
|
||||||
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ class Sidebar extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
paramsChanged(o, n) {
|
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
|
@bind
|
||||||
|
|
|
@ -32,7 +32,7 @@ class Table extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
paramsChanged(o, n) {
|
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
|
@bind
|
||||||
|
|
|
@ -26,6 +26,7 @@ class Dashboard extends Component {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
dateRange: [],
|
dateRange: [],
|
||||||
|
groupBy: 'day',
|
||||||
isPublic: document.cookie.indexOf('auth') < 0,
|
isPublic: document.cookie.indexOf('auth') < 0,
|
||||||
site: defaultSite,
|
site: defaultSite,
|
||||||
sites: [],
|
sites: [],
|
||||||
|
@ -69,6 +70,7 @@ class Dashboard extends Component {
|
||||||
this.setState({
|
this.setState({
|
||||||
dateRange: [ s.startDate, s.endDate ],
|
dateRange: [ s.startDate, s.endDate ],
|
||||||
period: s.period,
|
period: s.period,
|
||||||
|
groupBy: s.groupBy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +167,7 @@ class Dashboard extends Component {
|
||||||
<Sidebar siteId={state.site.id} dateRange={state.dateRange} />
|
<Sidebar siteId={state.site.id} dateRange={state.dateRange} />
|
||||||
|
|
||||||
<div class="box box-graph">
|
<div class="box box-graph">
|
||||||
<Chart siteId={state.site.id} dateRange={state.dateRange} />
|
<Chart siteId={state.site.id} dateRange={state.dateRange} tickStep={state.groupBy} />
|
||||||
</div>
|
</div>
|
||||||
<div class="box box-pages">
|
<div class="box box-pages">
|
||||||
<Table endpoint="pages" headers={["Top pages", "Views", "Uniques"]} siteId={state.site.id} dateRange={state.dateRange} />
|
<Table endpoint="pages" headers={["Top pages", "Views", "Uniques"]} siteId={state.site.id} dateRange={state.dateRange} />
|
||||||
|
|
Loading…
Reference in New Issue