Skip to content

Commit

Permalink
Implement full timestamp binning and charting
Browse files Browse the repository at this point in the history
  • Loading branch information
lelandbatey committed Oct 28, 2021
1 parent 0816528 commit a293619
Show file tree
Hide file tree
Showing 5 changed files with 400 additions and 39 deletions.
36 changes: 35 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,43 @@
<html>
<head>
<title>histogram_timestamps 000</title>

<style type="text/css">
#myChart {
width: 90%;
height: 90%
}
/* scraped from the Chart.js examples website */
/* https://codepen.io/jeremybradbury/pen/xxdddoB */
button {
transition: background .25s, border-color .25s;
background: rgba(40,44,52,.05);
border: 1px solid transparent;
border-radius: 6px;
color: #2f5fd0;
text-decoration: none !important;
display: inline-block;
font-size: .8rem;
padding: 8px 16px;
margin: 0 8px 8px 0;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
</style>
</head>
<body>
<canvas id="myChart" width="400" height="400"></canvas>
<script src="jsbuild/bundle.js"></script>
<script>
const CONTEXT = REPLACE_ME_WITH_JS_CONTEXT;
</script>
<script>
REPLACE_ME_WITH_BUNDLEJS
</script>
<!--<script src="jsbuild/bundle.js"></script>-->
<pre><code>
REPLACE_ME_WITH_JS_CONTEXT
</code></pre>
</body>
</html>
124 changes: 87 additions & 37 deletions jsbuild/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// var Chart = require('chart.js');
//import { Chart } from 'chart.js';
//import zoomPlugin from 'chartjs-plugin-zoom';
var Chart = require('chart.js');
require('chartjs-plugin-zoom');
var datefns = require('date-fns')
Expand Down Expand Up @@ -73,6 +70,7 @@ const zoomOptions = {
const panStatus = () => zoomOptions.pan.enabled ? 'enabled' : 'disabled';
const zoomStatus = () => zoomOptions.zoom.drag.enabled ? 'enabled' : 'disabled';

/*
const NUMBER_CFG = {count: 500, min: 0, max: 1000};
const data = {
datasets: [{
Expand Down Expand Up @@ -132,42 +130,94 @@ const config = {
},
}
};
*/

var ctx = document.getElementById('myChart').getContext('2d');
var myChart = new Chart(ctx, config);
/*
{
type: 'bar',
data: {
labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
backgroundColor: [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
],
borderColor: [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
],
borderWidth: 1
}]
},
const LABEL_LOCALTZ = 'Timeseries #1 - Local time zone ('+Intl.DateTimeFormat().resolvedOptions().timeZone+')';
const LABEL_UTC = 'Timeseries #1 - UTC';

const data = {
datasets: [
{
label: LABEL_LOCALTZ,
data: CONTEXT.data,
borderColor: 'rgb(255, 99, 132)',
}
],
};

const config = {
type: 'line',
data: data,
options: {
parsing: true,
responsive: false,
scales: {
y: {
beginAtZero: true
x: {
type: 'timeseries',
time: {unit: CONTEXT.unit},
},
},
plugins: {
zoom: zoomOptions,
title: {
display: true,
position: 'bottom',
text: (ctx) => 'Zoom: ' + zoomStatus() + ', Pan: ' + panStatus()
}
}
}
},
},
};

const ctx = document.getElementById('myChart').getContext('2d');
const myChart = new Chart(ctx, config);
function convertDateToUTC(date_) {
return new Date(date_.getUTCFullYear(), date_.getUTCMonth(), date_.getUTCDate(), date_.getUTCHours(), date_.getUTCMinutes(), date_.getUTCSeconds());
}
const actions = [
{
name: "Set TZ to local timezone",
handler(chart) {
let exp_label = LABEL_LOCALTZ;
if (chart.data.datasets[0].label == exp_label) {
return;
}
chart.data.datasets[0] = {
label: exp_label,
data: CONTEXT.data,
borderColor: 'rgb(255, 99, 132)',
};
chart.update();
},
},
{
name: "Set TZ to UTC",
handler(chart) {
let exp_label = LABEL_UTC;
if (chart.data.datasets[0].label == exp_label) {
return;
}
var nd = [];
for (var i = 0; i < CONTEXT.data.length; i++) {
nd.push({
x: convertDateToUTC(new Date(CONTEXT.data[i].x)).getTime(),
y: CONTEXT.data[i].y,
});
}
console.log(nd);
chart.data.datasets[0] = {
label: exp_label,
data: nd,
borderColor: 'rgb(255, 99, 132)',
};
chart.update();
},
},
];

actions.forEach((a, i) => {
let button = document.createElement("button");
button.id = "button"+i;
button.innerText = a.name;
button.onclick = () => a.handler(myChart);
document.querySelector(".buttons").appendChild(button);
});
*/
78 changes: 77 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package main

import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"

_ "github.com/itchyny/timefmt-go"
isatty "github.com/mattn/go-isatty"

"github.com/lelandbatey/histogram_timestamps/tbin"
)

func main() {
Expand All @@ -26,5 +34,73 @@ func main() {
// 5. Render the HTML/JS+JSON data to a tmp file
// 6. Serve the tmp file from a port
// 7. Launch a web-browser to view the localhost port
fmt.Printf("hello world")

tss, err := tbin.SimpleRandomTimestamps(10000, 12)
if err != nil {
fmt.Printf("cannot generate random timestamps: %q", err.Error())
os.Exit(2)
}

bins, err := tbin.BinTimestamps(tss, "hour")
if err != nil {
fmt.Printf("cannot divide timestamps into bins: %q", err.Error())
os.Exit(2)
}

ctx, err := tbin.FormatBinDataForChartJS(bins)
if err != nil {
fmt.Printf("cannot convert binned timestamp data into ChartJS data: %q", err.Error())
os.Exit(2)
}
ctxjson, err := json.Marshal(ctx)
if err != nil {
fmt.Printf("cannot marshal ChartJS data into JSON format: %q", err.Error())
os.Exit(2)
}

// Asterisk tell CreateTemp where to put a random filename component, which
// we want to avoid collisions.
tmpfn := fmt.Sprintf("%d_*_histogram_timestamps.html", time.Now().Unix())
//tmpdir := os.TempDir()
tmpdir := "./"
f, err := os.CreateTemp(tmpdir, tmpfn)
if err != nil {
fmt.Printf("cannot open temporary file for recording HTML: %q", err.Error())
os.Exit(2)
}
defer f.Close()

jslib := MustAssetString("bundle.js")
html_tmplfile := MustAssetString("index.html")

strings.ReplaceAll(html_tmplfile, "REPLACE_ME_WITH_JS_CONTEXT", string(ctxjson))
strings.ReplaceAll(html_tmplfile, "REPLACE_ME_WITH_BUNDLEJS", jslib)

fmt.Fprintf(f, "%s\n", html_tmplfile)
fmt.Printf("Wrote new HTML view file to file %q at path %q\n", f.Name(), tmpdir)
f.Close()

mux := http.NewServeMux()
{
absp, err := filepath.Abs(tmpdir)
if err != nil {
fmt.Printf("cannot determine abs path to temporary directory: %q", err.Error())
os.Exit(2)
}
fullFP := filepath.Join(absp, f.Name())
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
http.ServeFile(w, req, fullFP)
})
}

listener, err := net.Listen("tcp", ":0")
if err != nil {
panic(err)
}
fmt.Printf("Visit the newly generated graph of timestamps at URL: http://localhost:%d\n", listener.Addr().(*net.TCPAddr).Port)
err = http.Serve(listener, mux)
if err != nil {
fmt.Printf("error when serving a directory: %q", err.Error())
os.Exit(2)
}
}
66 changes: 66 additions & 0 deletions tbin/rand_timestamps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package tbin

import (
"fmt"
"math/rand"
)

/*
# Start time:
# 1572347470840
# Tuesday, October 29, 2019 11:11:10.840 AM (UTC)
def gen_rand_dates(count=100, seed=1, start_ts=1572347470840, stop_ts=None, dist=None):
if dist is None:
dist = 'rand'
if stop_ts is None:
stop_ts = start_ts + (12 * _1_hr)
rnd = random.Random(seed)
range_ = stop_ts - start_ts
getrand = lambda : int(rnd.random() * range_) + start_ts
if dist == 'rand':
getrand = lambda : rnd.randint(start_ts, stop_ts)
elif dist == 'normal':
getrand = lambda : min(max(int(rnd.normalvariate(0.5, 0.15) * range_), 0), range_) + start_ts
for _ in range(count):
yield getrand()
*/

func clamp(v, lo, hi int64) int64 {
rv := v
if v > hi {
rv = hi
}
if v < lo {
rv = lo
}
return rv
}

func GenRandomTimestamps(count int, seed int64, start_ts int64, stop_ts int64, dist string) ([]int64, error) {
rnd := rand.New(rand.NewSource(seed))
range_ := stop_ts - start_ts
var getrand func() int64
if dist == "random" {
getrand = func() int64 { return clamp(int64(rnd.Float64()*float64(range_))+start_ts, start_ts, stop_ts) }
} else if dist == "normal" {
getrand = func() int64 { return clamp(int64(rnd.NormFloat64()*float64(range_/4))+start_ts, start_ts, stop_ts) }
} else {
return nil, fmt.Errorf("distribution %q specified in dist is not valid", dist)
}

tss := []int64{}
for i := 0; i < count; i++ {
tss = append(tss, getrand())
}
return nil, nil
}

func SimpleRandomTimestamps(count int, hours_duration int) ([]int64, error) {
// Start time:
// 1572347470840
// Tuesday, October 29, 2019 11:11:10.840 AM (UTC)
var start_ts int64 = 1572347470840
stop_ts := start_ts + (int64(hours_duration) * TD_1_hr)
return GenRandomTimestamps(count, 1, start_ts, stop_ts, "normal")
}

0 comments on commit a293619

Please sign in to comment.