add visitor graph

This commit is contained in:
Danny van Kooten 2016-11-22 17:03:29 +01:00
parent 5eae716a9c
commit 8d26212ddd
14 changed files with 670 additions and 26 deletions

20
ROADMAP.md Normal file
View File

@ -0,0 +1,20 @@
Ana Roadmap
===========
This is a general draft document for thoughts and todo's.
### Key metrics
- Unique visits per day (in period)
- Pageviews per day (in period)
- Demographic
- Country
- Browser + version
- Operating System
- Acquisition
### Admin themes
- Pages, http://pages.revox.io/dashboard/latest/html/index.html#usa
- Metronic, http://keenthemes.com/preview/metronic/

View File

@ -6,6 +6,7 @@ import (
)
func RegisterRoutes() {
http.HandleFunc("/api/visits/count/day", GetVisitsDayCountHandler)
http.HandleFunc("/api/visits/count/realtime", GetVisitsRealtimeCount)
http.HandleFunc("/api/visits", GetVisitsHandler)
http.HandleFunc("/api/pageviews", GetPageviewsHandler)

View File

@ -11,7 +11,8 @@ import (
func GetPageviewsHandler(w http.ResponseWriter, r *http.Request) {
stmt, err := core.DB.Prepare(`SELECT
path,
COUNT(DISTINCT(ip_address)) AS pageviews
COUNT(ip_address) AS pageviews,
COUNT(DISTINCT(ip_address)) AS pageviews_unique
FROM visits
GROUP BY path`)
checkError(err)
@ -24,7 +25,7 @@ func GetPageviewsHandler(w http.ResponseWriter, r *http.Request) {
defer rows.Close()
for rows.Next() {
var p models.Pageview
err = rows.Scan(&p.Path, &p.Count);
err = rows.Scan(&p.Path, &p.Count, &p.CountUnique);
checkError(err)
results = append(results, p)
}

View File

@ -5,6 +5,7 @@ import (
"github.com/dannyvankooten/ana/models"
"github.com/dannyvankooten/ana/core"
"encoding/json"
"strconv"
)
// URL: /api/visits
@ -18,12 +19,19 @@ func GetVisitsHandler(w http.ResponseWriter, r *http.Request) {
path,
COALESCE(screen_resolution, '') AS screen_resolution,
timestamp
FROM visits`)
FROM visits
ORDER BY timestamp DESC
LIMIT ?`)
checkError(err)
defer stmt.Close()
rows, err := stmt.Query()
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit == 0 {
limit = 10
}
rows, err := stmt.Query(&limit)
checkError(err)
results := make([]models.Visit, 0)
@ -44,10 +52,43 @@ func GetVisitsHandler(w http.ResponseWriter, r *http.Request) {
// URL: /api/visits/count/realtime
func GetVisitsRealtimeCount(w http.ResponseWriter, r *http.Request) {
row := core.DB.QueryRow(`SELECT COUNT(DISTINCT(id)) FROM visits WHERE timestamp >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 HOUR_MINUTE)`)
row := core.DB.QueryRow(`SELECT COUNT(DISTINCT(ip_address)) FROM visits WHERE timestamp >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 3 HOUR_MINUTE)`)
var result int
row.Scan(&result)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// URL: /api/visits/count/day
func GetVisitsDayCountHandler(w http.ResponseWriter, r *http.Request) {
stmt, err := core.DB.Prepare(`SELECT
COUNT(*) AS count, DATE_FORMAT(timestamp, '%Y-%m-%d') AS date_group
FROM visits
GROUP BY date_group`)
checkError(err)
defer stmt.Close()
rows, err := stmt.Query()
checkError(err)
type Datapoint struct {
Count int
Label string
}
results := make([]Datapoint, 0)
defer rows.Close()
for rows.Next() {
v := Datapoint{}
err = rows.Scan(&v.Count, &v.Label);
checkError(err)
results = append(results, v)
}
err = rows.Err();
checkError(err)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(results)
}

View File

@ -19,20 +19,22 @@ class PageviewsList extends React.Component {
const tableRows = this.state.records.map((p, i) =>
<tr key={i}>
<td>{i+1}</td>
<td>{p.Path}</td>
<td><a href="{p.Path}">{p.Path}</a></td>
<td>{p.Count}</td>
<td>{p.CountUnique}</td>
</tr>
);
return (
<div>
<div className="block">
<h2>Pageviews</h2>
<table className="table pageviews">
<thead>
<tr>
<th>#</th>
<th>Path</th>
<th>Count</th>
<th>URL</th>
<th>Pageviews</th>
<th>Unique</th>
</tr>
</thead>
<tbody>{tableRows}</tbody>

View File

@ -1,3 +1,5 @@
'use strict';
import React, { Component } from 'react'
class RealtimeVisitsCount extends React.Component {

View File

@ -0,0 +1,75 @@
'use strict';
import React, { Component } from 'react'
import Chart from 'chart.js'
function rand(min, max, num) {
var rtn = [];
while (rtn.length < num) {
rtn.push((Math.random() * (max - min)) + min);
}
return rtn;
}
class VisitsGraph extends React.Component {
constructor(props) {
super(props);
this.state = { data: [] }
this.refresh();
}
refresh() {
return fetch('/api/visits/count/day')
.then((r) => r.json())
.then((data) => {
this.setState({ data: data });
this.initChart(this.chartCtx);
});
}
initChart(ctx) {
if( ! this.state.data.length ) {
return;
}
console.log(this.state.data.map((d) => d.Label));
console.log(this.state.data.map((d) => d.Count ));
console.log("Init chart");
console.log(ctx);
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: this.state.data.map((d) => d.Label),
datasets: [{
label: '# of Visitors',
data: this.state.data.map((d) => d.Count),
backgroundColor: 'rgba(162, 52, 235, 0.2)'
}]
},
options: {
tooltips: {
xPadding: 12,
yPadding: 9,
},
scales: {
yAxes: [{
ticks: {
beginAtZero:true
}
}]
}
}
});
}
render() {
return (
<div className="block">
<canvas ref={(ctx) => { this.chartCtx = ctx; }} width="600" height="200"></canvas>
</div>
);
}
}
export default VisitsGraph;

View File

@ -25,7 +25,7 @@ class VisitsList extends React.Component {
);
return (
<div>
<div className="block">
<h2>Visits</h2>
<table className="visits-table">
<thead>

View File

@ -5,20 +5,14 @@ import ReactDOM from 'react-dom';
import RealtimeVisitsCount from './components/realtime-visits.js';
import VisitsList from './components/visits-list.js';
import PageviewsList from './components/pageviews.js';
import VisitsGraph from './components/visits-graph.js';
function tick() {
const element = (
<div className="container">
<h1>Ana</h1>
<RealtimeVisitsCount />
<PageviewsList />
<VisitsList />
</div>
);
ReactDOM.render(
element, document.getElementById('root')
);
}
tick() && window.setInterval(tick, 1000);
ReactDOM.render(
<div className="container">
<h1>Ana</h1>
<RealtimeVisitsCount />
<VisitsGraph />
<PageviewsList />
</div>,
document.getElementById('root')
);

461
assets/sass/_normalize.scss Normal file
View File

@ -0,0 +1,461 @@
/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
/**
* 1. Change the default font family in all browsers (opinionated).
* 2. Correct the line height in all browsers.
* 3. Prevent adjustments of font size after orientation changes in
* IE on Windows Phone and in iOS.
*/
/* Document
========================================================================== */
html {
font-family: sans-serif; /* 1 */
line-height: 1.15; /* 2 */
-ms-text-size-adjust: 100%; /* 3 */
-webkit-text-size-adjust: 100%; /* 3 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers (opinionated).
*/
body {
margin: 0;
}
/**
* Add the correct display in IE 9-.
*/
article,
aside,
footer,
header,
nav,
section {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* Add the correct display in IE 9-.
* 1. Add the correct display in IE.
*/
figcaption,
figure,
main { /* 1 */
display: block;
}
/**
* Add the correct margin in IE 8.
*/
figure {
margin: 1em 40px;
}
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* 1. Remove the gray background on active links in IE 10.
* 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
*/
a {
background-color: transparent; /* 1 */
-webkit-text-decoration-skip: objects; /* 2 */
}
/**
* Remove the outline on focused links when they are also active or hovered
* in all browsers (opinionated).
*/
a:active,
a:hover {
outline-width: 0;
}
/**
* 1. Remove the bottom border in Firefox 39-.
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Prevent the duplicate application of `bolder` by the next rule in Safari 6.
*/
b,
strong {
font-weight: inherit;
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font style in Android 4.3-.
*/
dfn {
font-style: italic;
}
/**
* Add the correct background and color in IE 9-.
*/
mark {
background-color: #ff0;
color: #000;
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
audio,
video {
display: inline-block;
}
/**
* Add the correct display in iOS 4-7.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Remove the border on images inside links in IE 10-.
*/
img {
border-style: none;
}
/**
* Hide the overflow in IE.
*/
svg:not(:root) {
overflow: hidden;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers (opinionated).
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
* controls in Android 4.
* 2. Correct the inability to style clickable types in iOS and Safari.
*/
button,
html [type="button"], /* 1 */
[type="reset"],
[type="submit"] {
-webkit-appearance: button; /* 2 */
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Change the border, margin, and padding in all browsers (opinionated).
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* 1. Add the correct display in IE 9-.
* 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Remove the default vertical scrollbar in IE.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10-.
* 2. Remove the padding in IE 10-.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in IE 9-.
* 1. Add the correct display in Edge, IE, and Firefox.
*/
details, /* 1 */
menu {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Scripting
========================================================================== */
/**
* Add the correct display in IE 9-.
*/
canvas {
display: inline-block;
}
/**
* Add the correct display in IE.
*/
template {
display: none;
}
/* Hidden
========================================================================== */
/**
* Add the correct display in IE 10-.
*/
[hidden] {
display: none;
}

View File

@ -1,7 +1,10 @@
@import "normalize";
body {
font-family: Arial, Verdana, sans-serif;
font-size: 14px;
color: #333;
background: #f5f5f5;
}
h1, h2, h3 {
@ -22,6 +25,16 @@ table {
}
}
.block {
width: auto;
background: white;
padding: 20px;
margin-bottom: 20px;
h1, h2, h3 {
margin-top: 0;
}
}
.count {
font-size: 24px;

View File

@ -3,4 +3,5 @@ package models
type Pageview struct {
Path string
Count int
CountUnique int
}

View File

@ -12,6 +12,7 @@
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"babelify": "^7.3.0",
"chart.js": "^2.4.0",
"gulp-util": "^3.0.7",
"react-dom": "^15.4.0"
}

View File

@ -842,6 +842,26 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
chart.js:
version "2.4.0"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.4.0.tgz#44198073f0f43e5e16662e108420d92652a3c9a3"
dependencies:
chartjs-color "^2.0.0"
moment "^2.10.6"
chartjs-color-string@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.4.0.tgz#57748d4530ae28d8db0a5492182ba06dfdf2f468"
dependencies:
color-name "^1.0.0"
chartjs-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.0.0.tgz#7f60c7256589b24914814ece757659117381e35b"
dependencies:
chartjs-color-string "^0.4.0"
color-convert "^0.5.3"
cipher-base@^1.0.0, cipher-base@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.3.tgz#eeabf194419ce900da3018c207d212f2a6df0a07"
@ -872,6 +892,14 @@ code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
color-convert@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
color-name@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.1.tgz#4b1415304cf50028ea81643643bd82ea05803689"
combine-source-map@~0.7.1:
version "0.7.2"
resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.7.2.tgz#0870312856b307a87cc4ac486f3a9a62aeccc09e"
@ -2276,6 +2304,10 @@ module-deps@^4.0.8:
through2 "^2.0.0"
xtend "^4.0.0"
moment@^2.10.6:
version "2.17.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.0.tgz#a4c292e02aac5ddefb29a6eed24f51938dd3b74f"
ms@0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.2.tgz#ae25cf2512b3885a1d95d7f037868d8431124765"