mirror of
https://github.com/elisspace/FxLifeSheet.git
synced 2026-08-29 15:44:00 +00:00
Implement initial bucketing
This commit is contained in:
@@ -45,52 +45,59 @@ class API
|
||||
return raw_data.group_and_count(:key).order_by(:count).reverse.to_a
|
||||
end
|
||||
|
||||
def bucket(by:, value:, start_date:)
|
||||
def bucket_options_list(by:, start_date:)
|
||||
raise "`start_date` must be in format '2019-04'" unless start_date.match(/\d\d\d\d\-\d\d/)
|
||||
start_timestamp = Date.strptime(start_date, "%Y-%m").strftime("%Q")
|
||||
other_key = "fishoilIntake"
|
||||
|
||||
buckets = {}
|
||||
|
||||
res = database.fetch("
|
||||
SELECT value, count(*)
|
||||
FROM raw_data
|
||||
WHERE key=? AND timestamp > ?
|
||||
GROUP BY value
|
||||
", by, start_timestamp)
|
||||
return res.to_a.reverse # have on by default
|
||||
end
|
||||
|
||||
def bucket(by:, start_date:)
|
||||
raise "`start_date` must be in format '2019-04'" unless start_date.match(/\d\d\d\d\-\d\d/)
|
||||
start_timestamp = Date.strptime(start_date, "%Y-%m").strftime("%Q")
|
||||
|
||||
flat = database.fetch("
|
||||
SELECT
|
||||
raw_data.value AS bucket,
|
||||
(
|
||||
SELECT
|
||||
rd.value
|
||||
rd.value AS bucket,
|
||||
nrd.key AS other_key,
|
||||
AVG(nrd.value::numeric) AS avg_value,
|
||||
COUNT(nrd.id) as count
|
||||
FROM raw_data rd
|
||||
WHERE key = 'fishoilIntake'
|
||||
AND timestamp > 1554076800000
|
||||
ORDER BY abs(rd.timestamp - raw_data.timestamp) ASC
|
||||
LIMIT 1
|
||||
)
|
||||
FROM raw_data raw_data
|
||||
WHERE key = 'gym' AND timestamp > 1554076800000
|
||||
")
|
||||
INNER JOIN raw_data nrd ON (
|
||||
(nrd.type != 'text') AND
|
||||
abs(rd.timestamp - nrd.timestamp) < 20000000 /* 10000 is one minute */
|
||||
)
|
||||
WHERE rd.key = ? AND rd.timestamp > ?
|
||||
GROUP BY bucket, other_key
|
||||
ORDER BY other_key, bucket
|
||||
", by, start_timestamp).to_a
|
||||
|
||||
# SELECT
|
||||
# rd.value AS bucket,
|
||||
# nrd.key AS other_key,
|
||||
# AVG(nrd.value::numeric) AS avg_value,
|
||||
# COUNT(nrd.id) as count
|
||||
# FROM raw_data rd
|
||||
# INNER JOIN raw_data nrd ON (
|
||||
# (nrd.type != 'text') AND
|
||||
# abs(rd.timestamp - nrd.timestamp) < 20000000 /* 10000 is one minute */
|
||||
# )
|
||||
# WHERE rd.key = 'headache' AND rd.timestamp > 1554076800000
|
||||
# GROUP BY bucket, other_key
|
||||
# ORDER BY other_key, bucket
|
||||
# Group it properly, easier to just do that in Ruby
|
||||
structured = {}
|
||||
flat.each do |row|
|
||||
next if row[:avg_value].nil? # some rows can be nil
|
||||
next if row[:other_key].include?("swarmLocation") || row[:other_key].include?("locationL") || ["weight"].include?(row[:other_key])
|
||||
|
||||
|
||||
# TODO: Limit that the entry can't be more than 24 hours away
|
||||
flat.each do |current_row|
|
||||
buckets[current_row[:bucket]] ||= []
|
||||
buckets[current_row[:bucket]] << current_row[:value].to_f
|
||||
structured[row[:other_key]] ||= {}
|
||||
structured[row[:other_key]][row[:bucket]] = {
|
||||
value: row[:avg_value].truncate(5).to_s('F').to_f, # convert from BigFloat to float,
|
||||
count: row[:count]
|
||||
}
|
||||
end
|
||||
grouped = buckets.collect { |k, v| [k, v.sum / v.count]}.to_h
|
||||
|
||||
return grouped
|
||||
# Remove the useless ones (e.g. only one value, not large enough buckets)
|
||||
structured.delete_if do |key, value|
|
||||
value.count < 2 ||
|
||||
value.find_all { |k, r| r[:count] > 30 }.count < 2
|
||||
end
|
||||
|
||||
return structured
|
||||
end
|
||||
|
||||
private
|
||||
@@ -167,7 +174,6 @@ end
|
||||
# GROUP BY raw_data.value
|
||||
|
||||
|
||||
|
||||
# SELECT
|
||||
# rd.value AS bucket,
|
||||
# nrd.key AS other_key,
|
||||
@@ -183,3 +189,18 @@ end
|
||||
# ORDER BY other_key, bucket
|
||||
|
||||
|
||||
# flat = database.fetch("
|
||||
# SELECT
|
||||
# raw_data.value AS bucket,
|
||||
# (
|
||||
# SELECT
|
||||
# rd.value
|
||||
# FROM raw_data rd
|
||||
# WHERE key = 'fishoilIntake'
|
||||
# AND timestamp > 1554076800000
|
||||
# ORDER BY abs(rd.timestamp - raw_data.timestamp) ASC
|
||||
# LIMIT 1
|
||||
# )
|
||||
# FROM raw_data raw_data
|
||||
# WHERE key = 'gym' AND timestamp > 1554076800000
|
||||
# ")
|
||||
@@ -63,14 +63,6 @@
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bucket By</td>
|
||||
<td>
|
||||
<select id="bucket-by" class="keys" onchange="updateBucketBy()">
|
||||
<option value="" selected="selected">None</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Start date</td>
|
||||
<td><input type="text" value="2019-04" id="start-date" name="start-date" onchange="reloadAllData()" /></td>
|
||||
@@ -79,6 +71,13 @@
|
||||
</span>
|
||||
</form>
|
||||
<div id='myGraph' />
|
||||
<br />
|
||||
|
||||
<h1>Buckets</h1>
|
||||
<select class="keys" id="bucket-key" onchange="updateBucket()"></select>
|
||||
<select class="bucket-by-option" id="bucket-by-option-1" onchange="updateBucketByOption()"></select>
|
||||
<select class="bucket-by-option" id="bucket-by-option-2" onchange="updateBucketByOption()"></select>
|
||||
<div id='bucketGraph' />
|
||||
</body>
|
||||
<script type="text/javascript" src="/frontend.js"></script>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const host = 'http://127.0.0.1:4567';
|
||||
const host = 'http://127.0.0.1:8080';
|
||||
let keys = [];
|
||||
const allData = [];
|
||||
let groupBy = 'month';
|
||||
@@ -14,7 +14,6 @@ for (const currentIndex of Array(5).keys()) {
|
||||
if (currentIndex > 0) { current.yaxis = `y${currentIndex + 1}`; }
|
||||
allData.push(current);
|
||||
}
|
||||
console.log(allData);
|
||||
|
||||
const layout = {
|
||||
title: 'Life Sheet Data',
|
||||
@@ -64,6 +63,26 @@ const layout = {
|
||||
|
||||
Plotly.newPlot('myGraph', allData, layout);
|
||||
|
||||
const bucketLayout = {
|
||||
title: 'Life Sheet Buckets',
|
||||
barmode: 'group',
|
||||
yaxis: {
|
||||
range: [-1, 1]
|
||||
}
|
||||
}
|
||||
const allBucketData = [{
|
||||
x: [],
|
||||
y: [],
|
||||
marker: {
|
||||
color: '#C8A2C8',
|
||||
line: {
|
||||
width: 2.5
|
||||
}
|
||||
},
|
||||
type: "bar"
|
||||
}]
|
||||
Plotly.newPlot('bucketGraph', allBucketData, bucketLayout);
|
||||
|
||||
function loadKeys(callback) {
|
||||
httpGetAsync(`${host}/keys`, (data) => {
|
||||
keys = data;
|
||||
@@ -121,7 +140,7 @@ function reloadIndex(index) {
|
||||
|
||||
Plotly.redraw('myGraph');
|
||||
console.log(allData);
|
||||
console.log(layout);
|
||||
// console.log(layout);
|
||||
});
|
||||
} else {
|
||||
allData[index].y = [];
|
||||
@@ -129,6 +148,63 @@ function reloadIndex(index) {
|
||||
}
|
||||
}
|
||||
|
||||
let currentBucketData = null;
|
||||
|
||||
function updateBucket() {
|
||||
bucket = document.getElementById("bucket-key").value
|
||||
httpGetAsync(`${host}/bucket_options_list?by=${bucket}`, (data) => {
|
||||
selects = document.getElementsByClassName('bucket-by-option');
|
||||
for (let i = 0; i < selects.length; i++) {
|
||||
selects[i].innerHTML = ""
|
||||
data.forEach((row) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = row.value;
|
||||
opt.innerHTML = `${row.value} (${row.count})`;
|
||||
selects[i].appendChild(opt);
|
||||
});
|
||||
selects[i].selectedIndex = i
|
||||
}
|
||||
updateBucketByOption();
|
||||
})
|
||||
httpGetAsync(`${host}/bucket?by=${bucket}`, (data) => {
|
||||
currentBucketData = data;
|
||||
console.log(data)
|
||||
updateBucketByOption();
|
||||
});
|
||||
}
|
||||
|
||||
function updateBucketByOption() {
|
||||
if (!currentBucketData) { return; }
|
||||
|
||||
bucket1 = document.getElementById("bucket-by-option-1").value
|
||||
bucket2 = document.getElementById("bucket-by-option-2").value
|
||||
|
||||
const dataToRender = []
|
||||
for (var key in currentBucketData) {
|
||||
val = currentBucketData[key]
|
||||
let diff = parseFloat(val[bucket1]["value"] - val[bucket2]["value"].toFixed(5))
|
||||
|
||||
if (Math.abs(diff) > 0.2) {
|
||||
dataToRender.push({
|
||||
"key": key,
|
||||
"bucket1": val[bucket1],
|
||||
"bucket2": val[bucket2],
|
||||
"diff": diff
|
||||
})
|
||||
}
|
||||
}
|
||||
dataToRender.sort(function(a, b) {
|
||||
return a.diff - b.diff;
|
||||
})
|
||||
console.log(dataToRender)
|
||||
|
||||
allBucketData[0]["x"] = dataToRender.map(({ key }) => key);
|
||||
allBucketData[0]["y"] = dataToRender.map(({ diff }) => diff);
|
||||
console.log(allBucketData)
|
||||
|
||||
Plotly.redraw('bucketGraph');
|
||||
}
|
||||
|
||||
function httpGetAsync(theUrl, callback) {
|
||||
const xmlHttp = new XMLHttpRequest();
|
||||
xmlHttp.onreadystatechange = function() {
|
||||
@@ -146,6 +222,10 @@ loadKeys(() => {
|
||||
updateKeyForIndex('sleepDurationWithings', 2);
|
||||
updateKeyForIndex('bedTime', 3);
|
||||
updateKeyForIndex('gym', 4);
|
||||
|
||||
document.getElementById("bucket-key").value = "headache"
|
||||
updateBucket();
|
||||
|
||||
// reloadAllData();
|
||||
|
||||
// updateKeyForIndex(keys[0].key, 0);
|
||||
|
||||
@@ -29,12 +29,20 @@ get "/keys" do
|
||||
JSON.pretty_generate(api.list_keys)
|
||||
end
|
||||
|
||||
get "/bucket_options_list" do
|
||||
json_response
|
||||
|
||||
JSON.pretty_generate(api.bucket_options_list(
|
||||
by: params.fetch("by"),
|
||||
start_date: params.fetch("start_date", ENV["DEFAULT_MIN_DATE"].strip)
|
||||
))
|
||||
end
|
||||
|
||||
get "/bucket" do
|
||||
json_response
|
||||
|
||||
JSON.pretty_generate(api.bucket(
|
||||
by: "gym",
|
||||
value: "fishoilIntake",
|
||||
by: params.fetch("by"),
|
||||
start_date: params.fetch("start_date", ENV["DEFAULT_MIN_DATE"].strip)
|
||||
))
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user