wip on chart implementation using d3

This commit is contained in:
Danny 2018-05-22 12:23:17 +02:00
parent da19c116b7
commit 7adc5d3bec
10 changed files with 458 additions and 3 deletions

134
assets/js/components/Chart.js vendored Normal file
View File

@ -0,0 +1,134 @@
'use strict';
import { h, Component } from 'preact';
import Client from '../lib/client.js';
import { bind } from 'decko';
import * as d3 from 'd3';
function padData(data) {
for(let i=0; i<data.length; i++) {
data[i].Date = data[i].Date.substring(0, 10);
}
return data;
}
class Chart extends Component {
constructor(props) {
super(props)
this.state = {
loading: false,
data: [],
}
}
componentWillReceiveProps(newProps, prevState) {
if(newProps.before == prevState.before && newProps.after == prevState.after) {
return;
}
this.fetchData(newProps.before, newProps.after);
}
componentDidMount() {
var padding = 20,
h = 300,
w = this.base.clientWidth - ( padding * 3);
this.vis = d3.select(this.base)
.append('svg')
.attr('width', w + (padding * 2))
.attr('height', h + (padding * 2))
.append('g')
.attr('transform', 'translate(' + (2*padding) + ',' + padding + ')');
}
@bind
redrawChart() {
var data = this.state.data;
var max = d3.max(data, (d) => d.Pageviews);
var h = 300,
padding = 20,
w = this.base.clientWidth - ( padding * 3),
x = d3.scaleBand().range([0, w]).padding(0.2).round(true),
y = d3.scaleLinear().range([h, 0]),
yAxis = d3.axisLeft().scale(y),
xAxis = d3.axisBottom().scale(x);
x.domain(data.map((d) => d.Date))
y.domain([0, (max * 1.1)])
// clear all previous data
this.vis.selectAll('*').remove();
// axes
this.vis.append("g")
.attr("class", "y axis")
.call(yAxis);
this.vis.append("g")
.attr("class", "x axis")
.attr('transform', 'translate(0,' + h + ')')
.call(xAxis)
.selectAll('g')
// bars
var bars = this.vis.selectAll('g.pageviews')
.data(data)
.enter()
.append('g')
.attr('class', 'pageviews')
.attr('transform', function (d, i) { return "translate(" + x(d.Date) + ", 0)" });
bars.append('rect')
.attr('width', x.bandwidth())
.attr('height', (d) => (h - y(d.Pageviews)) )
.attr('y', (d) => y(d.Pageviews))
// visitors
var visitorBars = this.vis.selectAll('g.visitors')
.data(data)
.enter()
.append('g')
.attr('class', 'visitors')
.attr('transform', function (d, i) { return "translate(" + ( x(d.Date) + 0.25 * x.bandwidth() ) + ", 0)" });
visitorBars.append('rect')
.attr('width', x.bandwidth() * 0.5)
.attr('height', (d) => (h - y(d.Visitors)) )
.attr('y', (d) => y(d.Visitors))
}
@bind
fetchData(before, after) {
this.setState({ loading: true })
Client.request(`/stats/site/groupby/day?before=${before}&after=${after}`)
.then((d) => {
// request finished; check if timestamp range is still the one user wants to see
if( this.props.before != before || this.props.after != after ) {
return;
}
this.setState({
loading: false,
data: padData(d),
});
this.redrawChart();
})
}
render(props, state) {
return (
<div></div>
)
}
}
export default Chart

View File

@ -6,7 +6,7 @@ import Realtime from '../components/Realtime.js';
import DatePicker from '../components/DatePicker.js';
import CountWidget from '../components/CountWidget.js';
import Table from '../components/Table.js';
import Chart from '../components/Chart.js';
import { bind } from 'decko';
class Dashboard extends Component {
@ -46,6 +46,12 @@ class Dashboard extends Component {
<DatePicker onChange={this.changePeriod} value={state.period} />
</nav>
<div class="boxes">
<div class="box box-graph animated fadeInUp delayed_03s" style="padding: 0;">
<Chart before={state.before} after={state.after} />
</div>
</div>
<div class="boxes">
<div class="box box-totals animated fadeInUp delayed_03s">
<CountWidget title="Unique visitors" endpoint="stats/site/visitors" before={state.before} after={state.after} />
@ -56,8 +62,8 @@ class Dashboard extends Component {
<Table endpoint="stats/pages" headers={["Top pages", "Views", "Uniques"]} before={state.before} after={state.after} />
<Table endpoint="stats/referrers" headers={["Top referrers", "Views", "Uniques"]} before={state.before} after={state.after} showHostname="true" />
</div>
</section>
<footer class="section"></footer>

12
assets/sass/chart.scss Normal file
View File

@ -0,0 +1,12 @@
g.pageviews {
fill: #88ffc6;
}
g.visitors {
fill: #533feb;
}
.axis {
font-size: 12px;
fill: #6c7680;
}

View File

@ -103,7 +103,6 @@ body {
.main-nav ul { display: inline-block; padding: 0; }
.spacer { color: #98a0a6; padding: 0 8px; }
svg { width: 24px; height: 24px; display: inline-block; vertical-align: top; }
.header div, .date-nav a, .total-heading { font-size: 12px; text-transform: uppercase; color: #98a0a6; }
@ -178,3 +177,4 @@ body {
@import "util";
@import "pikaday";
@import "chart";

282
package-lock.json generated
View File

@ -1576,6 +1576,270 @@
"array-find-index": "^1.0.1"
}
},
"d3": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/d3/-/d3-5.4.0.tgz",
"integrity": "sha1-CQGZqFadHeI9BKP/B/4TYJX+p04=",
"requires": {
"d3-array": "1",
"d3-axis": "1",
"d3-brush": "1",
"d3-chord": "1",
"d3-collection": "1",
"d3-color": "1",
"d3-contour": "1",
"d3-dispatch": "1",
"d3-drag": "1",
"d3-dsv": "1",
"d3-ease": "1",
"d3-fetch": "1",
"d3-force": "1",
"d3-format": "1",
"d3-geo": "1",
"d3-hierarchy": "1",
"d3-interpolate": "1",
"d3-path": "1",
"d3-polygon": "1",
"d3-quadtree": "1",
"d3-random": "1",
"d3-scale": "2",
"d3-scale-chromatic": "1",
"d3-selection": "1",
"d3-shape": "1",
"d3-time": "1",
"d3-time-format": "2",
"d3-timer": "1",
"d3-transition": "1",
"d3-voronoi": "1",
"d3-zoom": "1"
}
},
"d3-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.1.tgz",
"integrity": "sha512-CyINJQ0SOUHojDdFDH4JEM0552vCR1utGyLHegJHyYH0JyCpSeTPxi4OBqHMA2jJZq4NH782LtaJWBImqI/HBw=="
},
"d3-axis": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.8.tgz",
"integrity": "sha1-MacFoLU15ldZ3hQXOjGTMTfxjvo="
},
"d3-brush": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.0.4.tgz",
"integrity": "sha1-AMLyOAGfJPbAoZSibUGhUw/+e8Q=",
"requires": {
"d3-dispatch": "1",
"d3-drag": "1",
"d3-interpolate": "1",
"d3-selection": "1",
"d3-transition": "1"
}
},
"d3-chord": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.4.tgz",
"integrity": "sha1-fexPC6iG9xP+ERxF92NBT290yiw=",
"requires": {
"d3-array": "1",
"d3-path": "1"
}
},
"d3-collection": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.4.tgz",
"integrity": "sha1-NC39EoN8kJdPM/HMCnha6lcNzcI="
},
"d3-color": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.2.0.tgz",
"integrity": "sha512-dmL9Zr/v39aSSMnLOTd58in2RbregCg4UtGyUArvEKTTN6S3HKEy+ziBWVYo9PTzRyVW+pUBHUtRKz0HYX+SQg=="
},
"d3-contour": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.2.0.tgz",
"integrity": "sha512-nDzZ2KDnrgTrhMjV8TH0RNrljk6uPNAGkG/v/1SKNVvJa2JU8szjh7o2ZYTX8yufA2oCI5HyeMqbzwiB+oDoIA==",
"requires": {
"d3-array": "^1.1.1"
}
},
"d3-dispatch": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.3.tgz",
"integrity": "sha1-RuFJHqqbWMNY/OW+TovtYm54cfg="
},
"d3-drag": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.1.tgz",
"integrity": "sha512-Cg8/K2rTtzxzrb0fmnYOUeZHvwa4PHzwXOLZZPwtEs2SKLLKLXeYwZKBB+DlOxUvFmarOnmt//cU4+3US2lyyQ==",
"requires": {
"d3-dispatch": "1",
"d3-selection": "1"
}
},
"d3-dsv": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.0.8.tgz",
"integrity": "sha512-IVCJpQ+YGe3qu6odkPQI0KPqfxkhbP/oM1XhhE/DFiYmcXKfCRub4KXyiuehV1d4drjWVXHUWx4gHqhdZb6n/A==",
"requires": {
"commander": "2",
"iconv-lite": "0.4",
"rw": "1"
}
},
"d3-ease": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.3.tgz",
"integrity": "sha1-aL+8NJM4o4DETYrMT7wzBKotjA4="
},
"d3-fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.0.tgz",
"integrity": "sha512-j+V4vtT6dceQbcKYLtpTueB8Zvc+wb9I93WaFtEQIYNADXl0c1ZJMN3qQo0CssiTsAqK8pePwc7f4qiW+b0WOg==",
"requires": {
"d3-dsv": "1"
}
},
"d3-force": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.1.0.tgz",
"integrity": "sha512-2HVQz3/VCQs0QeRNZTYb7GxoUCeb6bOzMp/cGcLa87awY9ZsPvXOGeZm0iaGBjXic6I1ysKwMn+g+5jSAdzwcg==",
"requires": {
"d3-collection": "1",
"d3-dispatch": "1",
"d3-quadtree": "1",
"d3-timer": "1"
}
},
"d3-format": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.0.tgz",
"integrity": "sha512-ycfLEIzHVZC3rOvuBOKVyQXSiUyCDjeAPIj9n/wugrr+s5AcTQC2Bz6aKkubG7rQaQF0SGW/OV4UEJB9nfioFg=="
},
"d3-geo": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.10.0.tgz",
"integrity": "sha512-VK/buVGgexthTTqGRNXQ/LSo3EbOFu4p2Pjud5drSIaEnOaF2moc8A3P7WEljEO1JEBEwbpAJjFWMuJiUtoBcw==",
"requires": {
"d3-array": "1"
}
},
"d3-hierarchy": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.6.tgz",
"integrity": "sha512-nn4bhBnwWnMSoZgkBXD7vRyZ0xVUsNMQRKytWYHhP1I4qHw+qzApCTgSQTZqMdf4XXZbTMqA59hFusga+THA/g=="
},
"d3-interpolate": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.2.0.tgz",
"integrity": "sha512-zLvTk8CREPFfc/2XglPQriAsXkzoRDAyBzndtKJWrZmHw7kmOWHNS11e40kPTd/oGk8P5mFJW5uBbcFQ+ybxyA==",
"requires": {
"d3-color": "1"
}
},
"d3-path": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.5.tgz",
"integrity": "sha1-JB6xhJvZ6egCHA0KeZ+KDo5EF2Q="
},
"d3-polygon": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.3.tgz",
"integrity": "sha1-FoiOkCZGCTPysXllKtN4Ik04LGI="
},
"d3-quadtree": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.3.tgz",
"integrity": "sha1-rHmH4+I/6AWpkPKOG1DTj8uCJDg="
},
"d3-random": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.0.tgz",
"integrity": "sha1-ZkLlBsb6OmSFldKyRpeIqNElKdM="
},
"d3-scale": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.0.0.tgz",
"integrity": "sha512-Sa2Ny6CoJT7x6dozxPnvUQT61epGWsgppFvnNl8eJEzfJBG0iDBBTJAtz2JKem7Mb+NevnaZiDiIDHsuWkv6vg==",
"requires": {
"d3-array": "^1.2.0",
"d3-collection": "1",
"d3-format": "1",
"d3-interpolate": "1",
"d3-time": "1",
"d3-time-format": "2"
}
},
"d3-scale-chromatic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.3.0.tgz",
"integrity": "sha512-YwMbiaW2bStWvQFByK8hA6hk7ToWflspIo2TRukCqERd8isiafEMBXmwfh8c7/0Z94mVvIzIveRLVC6RAjhgeA==",
"requires": {
"d3-color": "1",
"d3-interpolate": "1"
}
},
"d3-selection": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.3.0.tgz",
"integrity": "sha512-qgpUOg9tl5CirdqESUAu0t9MU/t3O9klYfGfyKsXEmhyxyzLpzpeh08gaxBUTQw1uXIOkr/30Ut2YRjSSxlmHA=="
},
"d3-shape": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.2.0.tgz",
"integrity": "sha1-RdAVOPBkuv0F6j1tLLdI/YxB93c=",
"requires": {
"d3-path": "1"
}
},
"d3-time": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.8.tgz",
"integrity": "sha512-YRZkNhphZh3KcnBfitvF3c6E0JOFGikHZ4YqD+Lzv83ZHn1/u6yGenRU1m+KAk9J1GnZMnKcrtfvSktlA1DXNQ=="
},
"d3-time-format": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.1.tgz",
"integrity": "sha512-8kAkymq2WMfzW7e+s/IUNAtN/y3gZXGRrdGfo6R8NKPAA85UBTxZg5E61bR6nLwjPjj4d3zywSQe1CkYLPFyrw==",
"requires": {
"d3-time": "1"
}
},
"d3-timer": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.7.tgz",
"integrity": "sha512-vMZXR88XujmG/L5oB96NNKH5lCWwiLM/S2HyyAQLcjWJCloK5shxta4CwOFYLZoY3AWX73v8Lgv4cCAdWtRmOA=="
},
"d3-transition": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.1.1.tgz",
"integrity": "sha512-xeg8oggyQ+y5eb4J13iDgKIjUcEfIOZs2BqV/eEmXm2twx80wTzJ4tB4vaZ5BKfz7XsI/DFmQL5me6O27/5ykQ==",
"requires": {
"d3-color": "1",
"d3-dispatch": "1",
"d3-ease": "1",
"d3-interpolate": "1",
"d3-selection": "^1.1.0",
"d3-timer": "1"
}
},
"d3-voronoi": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.2.tgz",
"integrity": "sha1-Fodmfo8TotFYyAwUgMWinLDYlzw="
},
"d3-zoom": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.7.1.tgz",
"integrity": "sha512-sZHQ55DGq5BZBFGnRshUT8tm2sfhPHFnOlmPbbwTkAoPeVdRTkB4Xsf9GCY0TSHrTD8PeJPZGmP/TpGicwJDJQ==",
"requires": {
"d3-dispatch": "1",
"d3-drag": "1",
"d3-interpolate": "1",
"d3-selection": "1",
"d3-transition": "1"
}
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@ -2819,6 +3083,14 @@
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=",
"dev": true
},
"iconv-lite": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ieee754": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.11.tgz",
@ -4631,6 +4903,11 @@
"inherits": "^2.0.1"
}
},
"rw": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
"integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
},
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
@ -4645,6 +4922,11 @@
"ret": "~0.1.10"
}
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sass-graph": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz",

View File

@ -22,6 +22,7 @@
},
"dependencies": {
"cookies-js": "^1.2.3",
"d3": "^5.4.0",
"decko": "^1.2.0",
"gulp-uglify": "^3.0.0",
"pikaday": "^1.7.0",

View File

@ -13,6 +13,7 @@ func (api *API) Routes() *mux.Router {
r.Handle("/api/session", HandlerFunc(api.LoginHandler)).Methods(http.MethodPost)
r.Handle("/api/session", HandlerFunc(api.LogoutHandler)).Methods(http.MethodDelete)
r.Handle("/api/stats/site/groupby/day", api.Authorize(HandlerFunc(api.GetSiteStatsPerDayHandler))).Methods(http.MethodGet)
r.Handle("/api/stats/site/pageviews", api.Authorize(HandlerFunc(api.GetSiteStatsPageviewsHandler))).Methods(http.MethodGet)
r.Handle("/api/stats/site/visitors", api.Authorize(HandlerFunc(api.GetSiteStatsVisitorsHandler))).Methods(http.MethodGet)
r.Handle("/api/stats/site/duration", api.Authorize(HandlerFunc(api.GetSiteStatsDurationHandler))).Methods(http.MethodGet)

View File

@ -52,3 +52,13 @@ func (api *API) GetSiteStatsRealtimeHandler(w http.ResponseWriter, r *http.Reque
}
return respond(w, envelope{Data: result})
}
// URL: /api/stats/site/groupby/day
func (api *API) GetSiteStatsPerDayHandler(w http.ResponseWriter, r *http.Request) error {
params := GetRequestParams(r)
result, err := api.database.GetSiteStatsPerDay(params.StartDate, params.EndDate)
if err != nil {
return err
}
return respond(w, envelope{Data: result})
}

View File

@ -18,6 +18,7 @@ type Datastore interface {
// site stats
GetSiteStats(time.Time) (*models.SiteStats, error)
GetSiteStatsPerDay(time.Time, time.Time) ([]*models.SiteStats, error)
InsertSiteStats(*models.SiteStats) error
UpdateSiteStats(*models.SiteStats) error
GetTotalSiteViews(time.Time, time.Time) (int, error)

View File

@ -28,6 +28,14 @@ func (db *sqlstore) UpdateSiteStats(s *models.SiteStats) error {
return err
}
func (db *sqlstore) GetSiteStatsPerDay(startDate time.Time, endDate time.Time) ([]*models.SiteStats, error) {
results := []*models.SiteStats{}
sql := `SELECT * FROM daily_site_stats WHERE date >= ? AND date <= ?`
query := db.Rebind(sql)
err := db.Select(&results, query, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
return results, err
}
func (db *sqlstore) GetTotalSiteViews(startDate time.Time, endDate time.Time) (int, error) {
sql := `SELECT COALESCE(SUM(pageviews), 0) FROM daily_site_stats WHERE date >= ? AND date <= ?`
query := db.Rebind(sql)