Skip to content

Commit

Permalink
add chart group options to navbar (hour, day, month).
Browse files Browse the repository at this point in the history
we should probably enforce 1st of month date when choosing 'month'
  • Loading branch information
dannyvankooten committed Nov 23, 2018
1 parent cae987c commit d62c9b9
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 66 deletions.
129 changes: 69 additions & 60 deletions assets/src/js/components/Chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand All @@ -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
})
Expand All @@ -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;
Expand All @@ -117,10 +131,10 @@ class Chart extends Component {
currentDate = nextDate;
}

return newData;
this.setState({
chartData: newData,
})
}



@bind
prepareChart() {
Expand All @@ -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 (`<div class="tip-heading">${title}</div>
<div class="tip-content">
Expand All @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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()
})
}
Expand Down
24 changes: 21 additions & 3 deletions assets/src/js/components/DatePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -106,19 +107,23 @@ 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;
}

// 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
Expand Down Expand Up @@ -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 (
<li class={classNames({ current: id == state.period })}>
Expand All @@ -201,11 +214,16 @@ class DatePicker extends Component {
return (
<nav class="date-nav sm ac">
<ul>
{links}
{presets}
</ul>
<ul>
<li><Pikadayer value={this.dateValue(state.startDate)} onSelect={this.setStartDate} /> <span></span> <Pikadayer value={this.dateValue(state.endDate)} onSelect={this.setEndDate} /></li>
</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>
)

Expand Down
2 changes: 1 addition & 1 deletion assets/src/js/components/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion assets/src/js/components/Table.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion assets/src/js/pages/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Dashboard extends Component {

this.state = {
dateRange: [],
groupBy: 'day',
isPublic: document.cookie.indexOf('auth') < 0,
site: defaultSite,
sites: [],
Expand Down Expand Up @@ -69,6 +70,7 @@ class Dashboard extends Component {
this.setState({
dateRange: [ s.startDate, s.endDate ],
period: s.period,
groupBy: s.groupBy,
})
}

Expand Down Expand Up @@ -165,7 +167,7 @@ class Dashboard extends Component {
<Sidebar siteId={state.site.id} dateRange={state.dateRange} />

<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 class="box box-pages">
<Table endpoint="pages" headers={["Top pages", "Views", "Uniques"]} siteId={state.site.id} dateRange={state.dateRange} />
Expand Down

0 comments on commit d62c9b9

Please sign in to comment.