major refactor

This commit is contained in:
Sam 2025-01-11 15:10:53 +00:00
parent e6139bcbab
commit c64e22f007
36 changed files with 1129 additions and 1123 deletions

View File

@ -93,3 +93,42 @@ def miner_rewards(args):
order by date asc order by date asc
""" """
def feerate_percentiles(args):
days_ago = parse_int(args["days_ago"])
return f"""
with
filtered_data as (
select * from models_final.final__feerate_percentiles order by date desc limit {days_ago}
)
select *
from filtered_data
order by date asc
"""
def bitcoin_price_timeseries(args):
days_ago = parse_int(args["days_ago"])
return f"""
with
filtered_data as (
select * from models_final.final__bitcoin_price order by date desc limit {days_ago}
)
select *
from filtered_data
order by date asc
"""
def bitcoin_hashrate(args):
days_ago = parse_int(args["days_ago"])
return f"""
with
filtered_data as (
select * from models_final.final__hashrate order by date desc limit {days_ago}
)
select *
from filtered_data
order by date asc
"""

View File

@ -79,3 +79,45 @@ async def miner_rewards(query: str):
rawData = handler.execute_query(pipeline) rawData = handler.execute_query(pipeline)
serializedData = serializer.serialize_many(rawData) serializedData = serializer.serialize_many(rawData)
return serializedData return serializedData
@router.get("/feerate_percentiles")
async def feerate_percentiles(query: str):
args = parse_args_to_dict(query)
pipeline = pipelines.feerate_percentiles(args)
handler = PostgresHandler()
schema = schemas.feerate_percentiles_schema
serializer = DataSerializer(schema)
rawData = handler.execute_query(pipeline)
serializedData = serializer.serialize_many(rawData)
return serializedData
@router.get("/bitcoin_price_timeseries")
async def bitcoin_price_timeseries(query: str):
args = parse_args_to_dict(query)
pipeline = pipelines.bitcoin_price_timeseries(args)
handler = PostgresHandler()
schema = schemas.bitcoin_price_timeseries_schema
serializer = DataSerializer(schema)
rawData = handler.execute_query(pipeline)
serializedData = serializer.serialize_many(rawData)
return serializedData
@router.get("/bitcoin_hashrate")
async def bitcoin_hashrate(query: str):
args = parse_args_to_dict(query)
pipeline = pipelines.bitcoin_hashrate(args)
handler = PostgresHandler()
schema = schemas.bitcoin_hashrate_schema
serializer = DataSerializer(schema)
rawData = handler.execute_query(pipeline)
serializedData = serializer.serialize_many(rawData)
return serializedData

View File

@ -51,6 +51,32 @@ def miner_rewards_schema(data):
"subsidy_usd": data["subsidy_usd"], "subsidy_usd": data["subsidy_usd"],
} }
def feerate_percentiles_schema(data):
return {
"date": data["date"],
"feerate_10th": data["feerate_10th"],
"feerate_25th": data["feerate_25th"],
"feerate_50th": data["feerate_50th"],
"feerate_75th": data["feerate_75th"],
"feerate_90th": data["feerate_90th"],
"maxfeerate": data["maxfeerate"],
"minfeerate": data["minfeerate"],
}
def bitcoin_price_timeseries_schema(data):
return {
"date": data["date"],
"price": data["price"],
}
def bitcoin_hashrate_schema(data):
return {
"date": data["date"],
"hashrate": data["hashrate"],
"difficulty": data["difficulty"],
"hashrate28": data["hashrate28"],
"difficulty28": data["difficulty28"],
}
class DataSerializer: class DataSerializer:
def __init__(self, schema_func): def __init__(self, schema_func):

View File

@ -7,7 +7,7 @@ User=admin
Group=www-data Group=www-data
WorkingDirectory=/var/www/baseddata.io/backend WorkingDirectory=/var/www/baseddata.io/backend
Environment="PATH=/var/www/baseddata.io/.venv/bin" Environment="PATH=/var/www/baseddata.io/.venv/bin"
ExecStart=/var/www/baseddata.io/.venv/bin/gunicorn --workers 4 --bind unix:baseddata.sock -m 007 app:app ExecStart=/var/www/baseddata.io/.venv/bin/gunicorn --workers 4 --bind unix:baseddata.sock -m 007 main:app
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -1,5 +0,0 @@
flask
flask_cors
orjson
gunicorn

View File

@ -6,11 +6,11 @@ author:
header_image: "/pics/charts/price.webp" header_image: "/pics/charts/price.webp"
summary: "Daily bitcoin price. Data is obtained from CoinGecko using their public API." summary: "Daily bitcoin price. Data is obtained from CoinGecko using their public API."
tags: ["Bitcoin", "Stats"] tags: ["Bitcoin", "Stats"]
jsScripts: [ "/js/lib/chart-params.js", "/js/lib/helper-functions.js", "/js/lib/echarts.min.js", "/js/lib/sorttable.js", "/js/content/bitcoin-price.js"]
--- ---
Daily bitcoin price. Data is obtained from [CoinGecko](https://www.coingecko.com/) using their Daily bitcoin price. Data is obtained from [CoinGecko](https://www.coingecko.com/) using their
public API. public API.
{{< bitcoin-price >}} {{< dropdown_filter id="days_ago_dropdown_filter" >}}
{{< chart id = "bitcoin-price-chart" >}}
{{< chart src="/js/bitcoin-price.js" >}}

View File

@ -5,10 +5,12 @@ author:
name: "Sam Chance" name: "Sam Chance"
header_image: "/pics/charts/feerate-percentile.webp" header_image: "/pics/charts/feerate-percentile.webp"
summary: "Bar chart showing historical median daily feerate percentiles for the Bitcoin protocol." summary: "Bar chart showing historical median daily feerate percentiles for the Bitcoin protocol."
chart: "/js/feerate_percentile.js"
tags: ["Bitcoin", "Stats"] tags: ["Bitcoin", "Stats"]
jsScripts: [ "/js/lib/chart-params.js", "/js/lib/helper-functions.js", "/js/lib/echarts.min.js", "/js/lib/sorttable.js", "/js/content/feerate-percentile.js"]
--- ---
This chart shows historical median daily feerate percentiles for the Bitcoin This chart shows historical median daily feerate percentiles for the Bitcoin
protocol. protocol.
{{< chart src="/js/feerate-percentile.js" >}} {{< dropdown_filter id="days_ago_dropdown_filter" >}}
{{< chart id="feerate-percentiles-chart" >}}

View File

@ -0,0 +1,23 @@
---
title: "Global Bitcoin Business Growth"
date: 2024-04-04T09:16:33+01:00
author:
name: "Sam Chance"
header_image: "/pics/charts/growth.webp"
summary: "This analysis uses OpenStreetMaps data to chart the yearly growth of bitcoin-accepting businesses worldwide."
tags: ["Bitcoin", "Stats", "OpenStreetMaps"]
jsScripts: [ "/js/lib/chart-params.js", "/js/lib/helper-functions.js", "/js/lib/echarts.min.js", "/js/lib/sorttable.js", "/js/content/global-bitcoin-business-growth.js"]
---
The table below illustrates growth of businesses worldwide that accept bitcoin as payment for products or services. The accompanying chart displays the cumulative number of bitcoin-accepting businesses for the countries selected in the table. The data is sourced from OpenStreetMaps.
You can select the period of interest from the drop-down, which updates the values in the table. The table shows the ***Previous*** value, which was the number of businesses `n days ago` specified in the drop-down. The ***Current*** value is the number of businesses as of the latest update. The table also shows the ***Absolute Difference***, and the ***Percent Difference*** between these periods.
<br/>
{{< dropdown_filter id="days_ago_dropdown_filter" >}}
{{< table id="bitcoin-business-growth-table" >}}
{{< chart id="bitcoin-business-growth-chart" >}}
#### Attribution
Data obtained from © [OpenStreetMap](https://www.openstreetmap.org/copyright)

View File

@ -1,30 +0,0 @@
---
title: "Global Bitcoin Business Growth"
date: 2024-04-04T09:16:33+01:00
author:
name: "Sam Chance"
header_image: "/pics/charts/growth.webp"
summary: "This analysis uses OpenStreetMaps data to chart the yearly growth of bitcoin-accepting businesses worldwide."
tags: ["Bitcoin", "Stats", "OpenStreetMaps"]
---
The table below illustrates growth of businesses worldwide that accept bitcoin as payment for products or services. The accompanying chart displays the cumulative number of bitcoin-accepting businesses for the countries selected in the table. The data is sourced from OpenStreetMaps and is updated approximately every 2 hours.
You can select the growth period of interest from the drop-down, which updates the values in the table. The table shows the ***Previous*** value, which was the number of businesses *n* days ago specified in the drop-down. The ***Current*** value is the number of businesses as of the latest update. The table also shows the ***Absolute Difference***, and the ***Percent Difference*** between this period.
The chart always reflects the countries selected in the table.
<br/>
{{< dropdown_filter id="days_ago_dropdown_filter" id_filter="days_ago" options="1 day:1,7 day:7,28 day:28,1 year:365,5 year:1826,10 year:3652,all time:10000" default_selection="7 day" targets="bitcoin-business-growth-chart bitcoin-business-growth-table" >}}
{{< table id="bitcoin-business-growth-table" endpoint="bitcoin_business_growth_percent_diff" headers="{'country_name': 'Country', 'date_range': 'Date Range', 'first_value': 'Previous #', 'last_value': 'Current #', 'difference': 'Diff', 'percent_difference': '% Diff'}" maxHeight="400px" sortable="true" valueId="country_name" selectableRows="multi" targets="bitcoin-business-growth-chart" defaultFirstSelected="true" >}}
{{< chart id="bitcoin-business-growth-table" src="/js/bitcoin-business-growth-chart.js" >}}
{{< chart chartMethod="tableRowSelectChart" id="bitcoin-business-growth-chart" endpoint="bitcoin_business_growth_timeseries" chartType="line" xAxisField="date" yAxisField="cumulative_value" scaleChart=true xAxisType="category" >}}
#### Attribution and License
Data obtained from © [OpenStreetMap](https://www.openstreetmap.org/copyright)
Licensed under the [ODbl](https://opendatacommons.org/licenses/odbl/)

View File

@ -6,8 +6,10 @@ author:
header_image: "/pics/charts/hashrate.webp" header_image: "/pics/charts/hashrate.webp"
summary: "Timeseries chart showing the Bitcoin network hashrate and difficulty." summary: "Timeseries chart showing the Bitcoin network hashrate and difficulty."
tags: ["Bitcoin", "Stats", "Hashrate"] tags: ["Bitcoin", "Stats", "Hashrate"]
jsScripts: [ "/js/lib/chart-params.js", "/js/lib/helper-functions.js", "/js/lib/echarts.min.js", "/js/lib/sorttable.js", "/js/content/hashrate.js"]
--- ---
This chart shows the estimated hashrate and difficulty of the Bitcoin network, accompanied by the 28-day moving average. This information is extracted from a bitcoin node using the `bitcoin-cli getnetworkhashps` command. This chart shows the estimated hashrate and difficulty of the Bitcoin network, accompanied by the 28-day moving average. This information is extracted from a bitcoin node using the `bitcoin-cli getnetworkhashps` command.
{{< chart src="/js/hashrate.js" >}} {{< dropdown_filter id="days_ago_dropdown_filter" >}}
{{< chart id = "hashrate-chart" >}}

View File

@ -7,10 +7,11 @@ summary: "Miner rewards"
header_image: "/pics/charts/rewards.webp" header_image: "/pics/charts/rewards.webp"
draft: false draft: false
tags: ["Bitcoin", "Stats"] tags: ["Bitcoin", "Stats"]
jsScripts: [ "/js/lib/chart-params.js", "/js/lib/helper-functions.js", "/js/lib/echarts.min.js", "/js/lib/sorttable.js", "/js/content/miner-rewards.js"]
--- ---
The following chart shows daily miner revenue in USD for the period selected in the dropdown. This information is based on the sum of bitcoin mined each day (i.e. the block-subsidy) plus the transaction fees. Price data is obtained from [CoinGecko](https://www.coingecko.com/). The following chart shows daily miner revenue in USD for the period selected in the dropdown. This information is based on the sum of bitcoin mined each day (i.e. the block-subsidy) plus the transaction fees. Price data is obtained from [CoinGecko](https://www.coingecko.com/).
{{< dropdown_filter id="days_ago_dropdown_filter" id_filter="days_ago" options="1 day:1,7 day:7,28 day:28,1 year:365,5 year:1826,10 year:3652,all time:10000" default_selection="7 day" targets="miner-rewards-chart" >}} {{< dropdown_filter id="days_ago_dropdown_filter" >}}
{{< chart id="miner-rewards" src="/js/miner-rewards.js" >}} {{< chart id="miner-rewards-chart" >}}

View File

@ -7,6 +7,7 @@ header_image: "/pics/charts/price.webp"
summary: "Daily bitcoin price. Data is obtained from CoinGecko using their public API." summary: "Daily bitcoin price. Data is obtained from CoinGecko using their public API."
tags: ["Bitcoin", "Stats"] tags: ["Bitcoin", "Stats"]
script: "/js/mangrove-map.js" script: "/js/mangrove-map.js"
draft: true
--- ---
{{< table id="mangrove_countries" endpoint="mangrove_by_country_latest" headers="{'country_with_parent': 'Country', 'original_pixels': '1996 Cover', 'total_n_pixels': '2020 Cover', 'cumulative_pixels_diff': 'Diff', 'cumulative_pct_diff': '% Diff'}" maxHeight="400px" sortable="true" valueId="country_with_parent" selectableRows="single" defaultFirstSelected="true" targets="mangrove-country-timeseries-chart" >}} {{< table id="mangrove_countries" endpoint="mangrove_by_country_latest" headers="{'country_with_parent': 'Country', 'original_pixels': '1996 Cover', 'total_n_pixels': '2020 Cover', 'cumulative_pixels_diff': 'Diff', 'cumulative_pct_diff': '% Diff'}" maxHeight="400px" sortable="true" valueId="country_with_parent" selectableRows="single" defaultFirstSelected="true" targets="mangrove-country-timeseries-chart" >}}

View File

@ -1,6 +1,11 @@
<!doctype html> <!doctype html>
<script src="/js/chart-params.js"></script>
<head> <head>
<script>
const apiURL = "{{ .Site.Params.apiURL }}";
</script>
{{ partial "head.html" . }} {{ partial "head.html" . }}
</head> </head>
{{ template "partials/body.html" . }} {{ template "partials/body.html" . }}
{{ range .Params.jsScripts }}
<script src="{{ . }}"></script>
{{ end }}

View File

@ -1,162 +0,0 @@
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
<script>
chartData = [];
function tableRowSelectChart(
id,
endpoint,
chartType,
xAxisField,
yAxisField,
sortField = null,
scaleChart = false,
xAxisType = "time",
formatValueDecimalPlaces = null,
) {
async function fetchDataForChart(query, valueId) {
try {
const apiEndpoint = `${apiURL}/${endpoint}?${query}`;
const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const fetchedData = await response.json();
const newData = fetchedData.reduce((acc, item) => {
const objectId = item[valueId];
if (!acc[objectId]) {
acc[objectId] = [];
}
acc[objectId].push([item[xAxisField], item[yAxisField]]);
return acc;
}, {});
chartData = { ...chartData, ...newData };
updateChart();
} catch (error) {
console.error("Fetching data failed:", error);
}
}
function updateChart() {
let chartDataMap = new Map();
for (let objectId in chartData) {
chartDataMap.set(objectId, chartData[objectId]);
}
var chartDom = document.getElementById(`${id}`);
var myChart = echarts.init(chartDom);
var option = {
tooltip: {
...tooltip,
valueFormatter(value, index) {
return formatValueDecimalPlaces == null
? value
: nFormatter(value, formatValueDecimalPlaces);
},
},
xAxis: {
type: xAxisType,
},
yAxis: {
scale: scaleChart,
type: "value",
},
series: Array.from(chartDataMap.entries()).map(([name, data]) => ({
name,
type: chartType,
data,
showSymbol: false,
})),
};
myChart.setOption(option, true);
}
// listen for filter events for this target
document.addEventListener("filterChange", function (event) {
tableId = document.getElementById(id).id;
console.log(event.detail);
eventDetail = event.detail;
if (eventDetail.filterActions.includes("refresh")) {
chartData = [];
updateChart();
} else {
if (eventDetail.filterTargets.includes(tableId)) {
if (eventDetail.filterActions.includes("selected")) {
valueId = eventDetail.filterId;
let selectedRow = {
[valueId]: eventDetail.filterValue,
};
query = queryConstructor(selectedRow);
fetchDataForChart(query, valueId);
} else {
delete chartData[eventDetail.filterValue];
updateChart();
}
}
}
});
}
function simpleChart(
id,
endpoint,
chartType,
xAxisField,
yAxisField,
series,
sortField = null,
scaleChart = false,
xAxisType = "time",
formatValueDecimalPlaces = null,
) {
console.log(series);
async function fetchDataForChart(query) {
try {
const apiEndpoint = `${apiURL}/${endpoint}?${query}`;
const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
chartData = await response.json();
updateChart();
console.log(chartData);
} catch (error) {
console.error("Fetching data failed:", error);
}
}
function updateChart() {
var chartDom = document.getElementById(`${id}`);
var myChart = echarts.init(chartDom);
var option = {
tooltip: {
...tooltip,
valueFormatter(value, index) {
return formatValueDecimalPlaces == null
? value
: nFormatter(value, formatValueDecimalPlaces);
},
},
xAxis: {
type: "category",
data: chartData.map((item) => item.date),
},
yAxis: {
scale: scaleChart,
type: "value",
},
series: JSON.parse(`{${series}}`),
};
myChart.setOption(option, true);
}
query = queryConstructor();
fetchDataForChart(query);
document.addEventListener("filterChange", function (event) {
query = queryConstructor();
fetchDataForChart(query);
});
}
</script>

View File

@ -8,8 +8,6 @@
<link rel="stylesheet" href="/css/toc.css" type="text/css" media="all" /> <link rel="stylesheet" href="/css/toc.css" type="text/css" media="all" />
<link rel="stylesheet" href="/css/articles.css" type="text/css" media="all" /> <link rel="stylesheet" href="/css/articles.css" type="text/css" media="all" />
<link rel="stylesheet" href="/css/charts.css" type="text/css" media="all" /> <link rel="stylesheet" href="/css/charts.css" type="text/css" media="all" />
<script src="/js/lib/sorttable.js"></script>
<script src="/js/lib/helper-functions.js"></script>
<link <link
rel="stylesheet" rel="stylesheet"
href="/css/codeblock.css" href="/css/codeblock.css"
@ -22,7 +20,4 @@
href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<script>
const apiURL = "{{ .Site.Params.apiURL }}";
</script>
</html> </html>

View File

@ -1,152 +0,0 @@
<script>
function createTable(
endpoint,
id,
headers,
maxHeight,
sortable,
valueId,
selectableRows,
filterTargets,
defaultFirstSelected,
) {
async function fetchDataForTable(query) {
try {
const apiEndpoint = `${apiURL}/${endpoint}?${query}`;
const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const fetchedData = await response.json();
data = fetchedData;
generateTable(data);
} catch (error) {
console.error("Fetching data failed:", error);
}
}
function generateTable(data) {
const jsonTableContainer = document.getElementById(`${id}--container`);
jsonTableContainer.className = "jsonTableContainer";
jsonTableContainer.innerHTML = "";
jsonTableContainer.style.maxHeight = maxHeight;
tableHeaderNames = Object.values(headers);
tableHeaderKeys = Object.keys(headers);
const table = document.createElement("table");
table.id = `${id}`;
const thead = document.createElement("thead");
const tbody = document.createElement("tbody");
const headerRow = document.createElement("tr");
tableHeaderNames.forEach((header) => {
const th = document.createElement("th");
th.textContent = header;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
for (const key in data) {
const row = document.createElement("tr");
row.value = data[key][valueId];
tableHeaderKeys.forEach((columnName) => {
const td = document.createElement("td");
const div = document.createElement("div");
div.id = "scrollable";
div.textContent = data[key][columnName];
td.appendChild(div);
row.appendChild(td);
tbody.appendChild(row);
});
}
table.appendChild(thead);
table.appendChild(tbody);
jsonTableContainer.appendChild(table);
// sortable
if (sortable == "true") {
table.className = "sortable";
sorttable.makeSortable(document.getElementById(`${id}`));
}
if (selectableRows === "multi" || selectableRows === "single") {
const rows = table.getElementsByTagName("tr");
for (let i = 1; i < rows.length; i++) {
rows[i].addEventListener("click", function () {
if (selectableRows === "multi") {
this.classList.toggle("selected");
if (this.classList.contains("selected")) {
const event = new CustomEvent("filterChange", {
detail: {
filterId: valueId,
filterValue: this.value,
filterActions: ["selected"],
filterTargets: filterTargets,
},
});
document.dispatchEvent(event);
} else {
const event = new CustomEvent("filterChange", {
detail: {
filterId: valueId,
filterValue: this.value,
filterActions: ["deselected"],
filterTargets: filterTargets,
},
});
document.dispatchEvent(event);
}
} else if (selectableRows === "single") {
const event = new CustomEvent("filterChange", {
detail: {
filterId: valueId,
filterValue: this.value,
filterActions: ["selected"],
filterTargets: filterTargets,
},
});
document.dispatchEvent(event);
if (this.classList.contains("selected")) {
this.classList.remove("selected");
} else {
for (let j = 1; j < rows.length; j++) {
rows[j].classList.remove("selected");
}
this.classList.add("selected");
}
}
});
if (defaultFirstSelected == true) {
if (i == 1) {
rows[i].classList.add("selected");
const event = new CustomEvent("filterChange", {
detail: {
filterId: valueId,
filterValue: rows[i].value,
filterActions: ["selected"],
filterTargets: filterTargets,
},
});
document.dispatchEvent(event);
}
}
}
}
}
// listen for filter events for this target
document.addEventListener("filterChange", function (event) {
tableId = document.getElementById(id).id;
if (event.detail.filterTargets.includes(tableId)) {
query = queryConstructor();
fetchDataForTable(query);
}
});
query = queryConstructor();
fetchDataForTable(query);
}
</script>

View File

@ -1,19 +0,0 @@
<!doctype html>
<html>
<body>
<p id="price"></p>
<script>
fetch(`${"{{ .Site.Params.apiURL }}"}/get_json/final__price.json`)
.then((response) => response.json())
.then((data) => {
var lastElement = data[data.length - 1];
document.getElementById("price").innerHTML =
`The current price is: <em>$ ${lastElement.price.toLocaleString()}</em> (${lastElement.date}) `;
})
.catch((error) => {
console.error("Error:", error);
});
</script>
</body>
</html>

View File

@ -1,7 +1,3 @@
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.js"></script> <section class = 'chart-container' id="{{ .Get "id" }}-container">
<section class = 'chart-container'> <div id="{{ .Get "id" }}" class="chart"></div>
<script>let id = '{{ .Get "id" }}'</script>
<div class = "chart" id='{{ .Get "id" }}'>
<script src={{ .Get "src" }}></script>
</div>
</section> </section>

View File

@ -1,29 +1,4 @@
{{ $id := .Get "id" }} <div id="{{ .Get "id" }}-container" class="dropdown-filter-container">
{{ $default_selection := .Get "default_selection" }} <select id="{{ .Get "id" }}" class="filter dropdown-filter">
{{ $options := .Get "options" }}
{{ $options_split := split $options "," }}
<div class="dropdown-filter-container">
<select class="filter dropdown-filter" id="{{ $id }}" idFilter='{{ .Get "id_filter" }}' onchange="dispatchDropdownEvent(this)">
{{ range $options_split }}
{{ $parts := split . ":" }}
{{ $key := index $parts 0 }}
{{ $value := index $parts 1 }}
<option value="{{ $value }}" {{ if eq $key $default_selection }}selected{{ end }}>{{ $key }}</option>
{{ end }}
</select> </select>
<script>
function dispatchDropdownEvent(selectElement) {
const event = new CustomEvent('filterChange', {
detail: {
filterId: '{{ .Get "id_filter" }}',
filterValue: selectElement.value,
filterActions: ["refresh"],
filterTargets: '{{ .Get "targets" }}'.split(" ")
}
});
document.dispatchEvent(event);
}
</script>
</div> </div>

View File

@ -0,0 +1,9 @@
{{ if .Get "src" }}
<script src="{{ .Get "src" }}"></script>
{{ else }}
<script id="{{ .Get " id" }}">
window.addEventListener('DOMContentLoaded', (event) => {
{{ .Inner | safeJS }}
});
</script>
{{ end }}

View File

@ -1,8 +1,3 @@
{{ partial "table.html" }} <section class = 'table-container' id = '{{ .Get "id" }}-container'>
<div id = '{{ .Get "id" }}--container'> <div id="{{ .Get "id" }}" class="table"></div>
<script> </section>
document.addEventListener("DOMContentLoaded", function () {
createTable({{ .Get "endpoint" }}, {{ .Get "id" }}, {{ .Get "headers" | safeJS }}, {{ .Get "maxHeight" }}, {{ .Get "sortable" }}, {{ .Get "valueId" }}, {{ .Get "selectableRows" }}, '{{ .Get "targets" }}'.split(" "), {{ .Get "defaultFirstSelected" | safeJS }})
});
</script>
</div>

View File

@ -1,102 +0,0 @@
var chartDom = document.getElementById(id);
var myChart = echarts.init(chartDom);
chartData = [];
function tableRowSelectChart(
endpoint,
xAxisField,
yAxisField,
chartType,
xAxisType,
scaleChart = false,
formatValueDecimalPlaces = null,
) {
async function fetchDataForChart(query, valueId) {
try {
const apiEndpoint = `${apiURL}/${endpoint}?${query}`;
const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const fetchedData = await response.json();
console.log(fetchedData);
const newData = fetchedData.reduce((acc, item) => {
const objectId = item[valueId];
if (!acc[objectId]) {
acc[objectId] = [];
}
acc[objectId].push([item[xAxisField], item[yAxisField]]);
return acc;
}, {});
chartData = { ...chartData, ...newData };
updateChart();
} catch (error) {
console.error("Fetching data failed:", error);
}
}
function updateChart() {
let chartDataMap = new Map();
for (let objectId in chartData) {
chartDataMap.set(objectId, chartData[objectId]);
}
var option = {
tooltip: {
...tooltip,
valueFormatter(value, index) {
return formatValueDecimalPlaces == null
? value
: nFormatter(value, formatValueDecimalPlaces);
},
},
xAxis: {
type: xAxisType,
},
yAxis: {
scale: scaleChart,
type: "value",
},
series: Array.from(chartDataMap.entries()).map(([name, data]) => ({
name,
type: chartType,
data,
showSymbol: false,
})),
};
myChart.setOption(option, true);
}
// listen for filter events for this target
document.addEventListener("filterChange", function (event) {
tableId = document.getElementById(id).id;
console.log(event.detail);
eventDetail = event.detail;
if (eventDetail.filterActions.includes("refresh")) {
chartData = [];
updateChart();
} else {
if (eventDetail.filterTargets.includes(tableId)) {
if (eventDetail.filterActions.includes("selected")) {
valueId = eventDetail.filterId;
let selectedRow = {
[valueId]: eventDetail.filterValue,
};
query = queryConstructor(selectedRow);
fetchDataForChart(query, valueId);
console.log(valueId)
} else {
delete chartData[eventDetail.filterValue];
updateChart();
}
}
}
});
}
tableRowSelectChart(
endpoint = "bitcoin_business_growth_timeseries",
xAxisField = "date",
yAxisField = "cumulative_value",
chartType = "line",
xAxisType = "category",
);

View File

@ -1,95 +0,0 @@
async function fetchDataForTable() {
try {
selectedPeriod = getPeriodFromDropdown();
const apiEndpoint = `${apiURL}/bitcoin_business_growth_by_country?latest_date=true`;
const response = await fetch(
apiEndpoint + `&cumulative_period_type=${selectedPeriod}`,
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const fetchedData = await response.json();
data = fetchedData;
createTable(data);
} catch (error) {
console.error("Fetching data failed:", error);
}
}
function createTable(data) {
const jsonTableContainer = document.getElementById("jsonTableContainer");
jsonTableContainer.innerHTML = "";
const table = document.createElement("table");
let checkedBoxes = [];
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
tableHeaders = [
"Select",
"Country",
"Previous #",
"Current #",
"Diff",
"% Diff",
];
tableHeaders.forEach((header) => {
const th = document.createElement("th");
th.textContent = header;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement("tbody");
filteredData = data.filter((item) => 1 === 1);
filteredData.sort((a, b) => {
const sortField = "diff";
return Math.abs(b[sortField]) - Math.abs(a[sortField]);
});
filteredData.forEach((row, index) => {
const tr = document.createElement("tr");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = index < 1;
checkbox.addEventListener("change", function () {
if (this.checked) {
checkedBoxes.push(row["country_name"]);
} else {
checkedBoxes = checkedBoxes.filter((id) => id !== row["country_name"]);
}
});
checkbox.id = row["country_name"];
tr.appendChild(document.createElement("td")).append(checkbox);
columnNames = [
"country_name",
"cumulative_previous_value",
"cumulative_current_value",
"diff",
"pct_diff",
];
columnNames.forEach((columnName) => {
const td = document.createElement("td");
const div = document.createElement("div");
div.id = "scrollable";
div.textContent = row[columnName];
td.appendChild(div);
tr.appendChild(td);
});
tbody.appendChild(tr);
if (checkbox.checked) {
checkedBoxes.push(row["country_name"]);
}
});
table.appendChild(tbody);
const tableContainer = document.getElementById("jsonTableContainer");
tableContainer.appendChild(table);
tableContainer.dispatchEvent(
new CustomEvent("reloadTable", { bubbles: true }),
);
}
document.addEventListener("DOMContentLoaded", function () {
const dropdown = document.getElementById("select-period-dropdown");
fetchDataForTable();
dropdown.addEventListener("change", () => {
fetchDataForTable();
});
});

View File

@ -1,84 +0,0 @@
let dataArr = [];
const myChart = echarts.init(document.getElementById("chart"));
const filename = "final__price.json";
const periods = [
"all time",
"last 7 days",
"last 28 days",
"last 365 days",
"last 2 years",
];
async function fetchDataForChart(selectedValue) {
try {
const apiEndpoint = `${apiURL}/get_json/${filename}?period=${selectedValue}`;
const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const dataArr = await response.json();
initEchart(dataArr);
} catch (error) {
console.error("Fetching data failed:", error);
}
}
function initEchart(dataArr) {
const option = {
backgroundColor: backgroundColor,
tooltip: tooltip,
toolbox: toolboxParams,
xAxis: {
data: dataArr.map((row) => row.date),
axisTick: axisTick,
axisLabel: axisLabel,
axisLine: axisLine,
},
grid: grid,
dataZoom: dataZoom(0),
yAxis: [
{
type: "value",
name: "Price (USD)",
nameGap: 30,
nameLocation: "middle",
nameTextStyle: textStyleMain,
position: "left",
alignTicks: true,
axisTick: axisTick,
axisLine: axisLine,
splitLine: {
show: false,
},
axisLabel: {
...axisLabel,
formatter(value, index) {
return nFormatter(value, 0);
},
},
},
],
series: [
{
type: "line",
name: "Price (USD)",
data: dataArr.map((row) => row.price),
},
],
};
myChart.setOption(option);
}
document.addEventListener("DOMContentLoaded", function () {
let periodIndex = 2;
periodDropdown(periods, periodIndex);
fetchDataForChart(periods[periodIndex]);
const selectElement = document.getElementById("select-period-dropdown");
selectElement.addEventListener("change", function (event) {
const selectedValue = event.target.value;
fetchDataForChart(selectedValue);
});
});

View File

@ -1,139 +0,0 @@
const fontScale = 0.8;
const backgroundColor = "#f9f9f9";
const tooltipBgColor = "#e7e7f5";
const textColor = "#373841";
const textStyleMain = {
fontFamily: "sans-serif",
fontSize: 12 * fontScale,
color: textColor,
};
const tooltip = {
borderColor: textColor,
backgroundColor: tooltipBgColor,
order: "seriesDesc",
textStyle: textStyleMain,
trigger: "axis",
};
const toolboxParams = {
itemSize: 16 * fontScale,
showTitle: true,
top: "-1%",
right: "20%",
iconStyle: {
borderColor: textColor,
borderWidth: 2,
},
feature: {
dataZoom: {
yAxisIndex: "none",
},
dataView: {},
saveAsImage: {
pixelRatio: 2,
},
},
};
const axisLabel = {
fontSize: 12 * fontScale,
color: textColor,
margin: 10,
};
const axisLine = {
show: true,
lineStyle: {
color: textColor,
width: 2,
},
};
const axisTick = { show: true };
const grid = {
bottom: 30,
top: 10,
left: 15,
containLabel: true,
};
const yaxisTextStyle = {
fontSize: 12 * fontScale,
padding: [0, 0, 10, 0],
color: textColor,
};
const yaxisTextStyle2 = {
fontSize: 12 * fontScale,
padding: [50, 0, 0, 0],
color: textColor,
};
function dataZoom() {
const dataZoom = [
{
type: "inside",
},
];
return dataZoom;
}
function nFormatter(value, digits) {
const lookup = [
{ value: 1, symbol: "" },
{ value: 1e3, symbol: "k" },
{ value: 1e6, symbol: "M" },
{ value: 1e9, symbol: "G" },
{ value: 1e12, symbol: "T" },
{ value: 1e15, symbol: "P" },
{ value: 1e18, symbol: "E" },
];
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
const item = lookup
.slice()
.reverse()
.find((item) => value >= item.value);
return item
? (value / item.value).toFixed(digits).replace(rx, "$1") + item.symbol
: "0";
}
function periodDropdown(periods, defaultIndex = 0) {
const modifiers = document.getElementById("chart-modifiers");
const dropdownContainer = document.createElement("div");
const dropdown = document.createElement("select");
const textNode = document.createTextNode("Select period: ");
dropdownContainer.id = "dropdown-container";
dropdown.id = "select-period-dropdown";
dropdownContainer.appendChild(textNode);
dropdownContainer.appendChild(dropdown);
modifiers.appendChild(dropdownContainer);
periods.forEach((period, index) => {
const option = document.createElement("option");
if (index === defaultIndex) {
option.selected = true;
}
option.value = period;
option.textContent = period;
dropdown.appendChild(option);
});
}
function getPeriodFromDropdown() {
let dropdown = document.getElementById("select-period-dropdown");
let selectedIndex = dropdown.selectedIndex;
return dropdown.options[selectedIndex].value;
}
window.addEventListener("resize", function () {
if (myChart != null && myChart != undefined) {
myChart.resize();
}
});

View File

@ -0,0 +1,93 @@
function createChart(
chart,
endpoint,
xAxisType,
scaleChart = false,
) {
async function fetchDataForChart(query) {
try {
const apiEndpoint = `${apiURL}/${endpoint}?${query}`;
const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const chartData = await response.json();
console.log(chartData)
updateChart(chartData);
} catch (error) {
console.error("Fetching data failed:", error);
}
}
function updateChart(chartData) {
var option = {
tooltip: {
...tooltip,
},
xAxis: {
type: xAxisType,
data: chartData.map((item) => item.date),
},
yAxis: [
{
type: "value",
name: "Price (USD)",
nameGap: 30,
nameLocation: "middle",
nameTextStyle: textStyleMain,
position: "left",
alignTicks: true,
axisTick: axisTick,
axisLine: axisLine,
splitLine: {
show: false,
},
axisLabel: {
...axisLabel,
formatter(value, index) {
return nFormatter(value, 0);
},
},
},
],
series: [
{
type: "line",
name: "Price (USD)",
data: chartData.map((row) => row.price),
},
],
};
chart.setOption(option, true);
}
// listen for filter events for this target
document.addEventListener("filterChange", function(event) {
eventDetail = event.detail;
if (eventDetail.filterActions.includes("refresh")) {
chartData = [];
query = queryConstructor();
fetchDataForChart(query);
updateChart(chartData);
}
});
let query = queryConstructor();
fetchDataForChart(query);
}
document.addEventListener("DOMContentLoaded", function() {
chartData = [];
let chartDom = document.getElementById("bitcoin-price-chart");
let myChart = echarts.init(chartDom);
const filterPeriods = { "7 day": 7, "28 day": 28, "365 day": 365, "2 years": 730, "5 years": 1826, "10 years": 3652, "all time": 10000 };
dropDownFilter("days_ago_dropdown_filter", "days_ago", ["bitcoin-price-chart"], filterPeriods, "365 day")
createChart(
chart = myChart,
endpoint = "bitcoin_price_timeseries",
xAxisType = "category",
);
});

View File

@ -0,0 +1,106 @@
function createChart(
chart,
endpoint,
xAxisType,
scaleChart = false,
) {
async function fetchDataForChart(query) {
try {
const apiEndpoint = `${apiURL}/${endpoint}?${query}`;
const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const chartData = await response.json();
console.log(chartData)
updateChart(chartData);
} catch (error) {
console.error("Fetching data failed:", error);
}
}
function updateChart(chartData) {
var option = {
tooltip: {
...tooltip,
},
xAxis: {
type: xAxisType,
data: chartData.map((item) => item.date),
},
yAxis: {
scale: scaleChart,
type: "value",
},
series: [
{
type: "bar",
stack: "value",
name: "10th",
barCategoryGap: "0%",
data: chartData.map((row) => row.feerate_10th),
},
{
type: "bar",
stack: "value",
name: "25th",
barCategoryGap: "0%",
data: chartData.map((row) => row.feerate_25th),
},
{
type: "bar",
stack: "value",
name: "50th",
barCategoryGap: "0%",
data: chartData.map((row) => row.feerate_50th),
},
{
type: "bar",
stack: "value",
name: "75th",
barCategoryGap: "0%",
data: chartData.map((row) => row.feerate_75th),
},
{
type: "bar",
stack: "value",
name: "90th",
barCategoryGap: "0%",
data: chartData.map((row) => row.feerate_90th),
},
],
};
chart.setOption(option, true);
}
// listen for filter events for this target
document.addEventListener("filterChange", function(event) {
eventDetail = event.detail;
if (eventDetail.filterActions.includes("refresh")) {
chartData = [];
query = queryConstructor();
fetchDataForChart(query);
updateChart(chartData);
}
});
let query = queryConstructor();
fetchDataForChart(query);
}
document.addEventListener("DOMContentLoaded", function() {
chartData = [];
let chartDom = document.getElementById("feerate-percentiles-chart");
let myChart = echarts.init(chartDom);
const filterPeriods = {"7 day": 7, "28 day": 28, "365 day": 365, "2 years": 730, "5 years": 1826, "10 years": 3652, "all time": 10000};
dropDownFilter("days_ago_dropdown_filter", "days_ago", ["feerate-percentile-chart"], filterPeriods, "365 day")
createChart(
chart = myChart,
endpoint = "feerate_percentiles",
xAxisType = "category",
);
});

View File

@ -0,0 +1,279 @@
function createTable(
endpoint,
tableId,
headers,
maxHeight,
sortable,
valueId,
selectableRows,
filterTargets,
defaultFirstSelected,
) {
async function fetchDataForTable(query) {
try {
const apiEndpoint = `${apiURL}/${endpoint}?${query}`;
const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const fetchedData = await response.json();
data = fetchedData;
generateTable(data);
} catch (error) {
console.error("Fetching data failed:", error);
}
}
function generateTable(data) {
const jsonTableContainer = document.getElementById(`${tableId}-container`);
jsonTableContainer.className = "jsonTableContainer";
jsonTableContainer.innerHTML = "";
jsonTableContainer.style.maxHeight = maxHeight;
tableHeaderNames = Object.values(headers);
tableHeaderKeys = Object.keys(headers);
const table = document.createElement("table");
table.id = tableId;
const thead = document.createElement("thead");
const tbody = document.createElement("tbody");
const headerRow = document.createElement("tr");
tableHeaderNames.forEach((header) => {
const th = document.createElement("th");
th.textContent = header;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
for (const key in data) {
const row = document.createElement("tr");
row.value = data[key][valueId];
tableHeaderKeys.forEach((columnName) => {
const td = document.createElement("td");
const div = document.createElement("div");
div.id = "scrollable";
div.textContent = data[key][columnName];
td.appendChild(div);
row.appendChild(td);
tbody.appendChild(row);
});
}
table.appendChild(thead);
table.appendChild(tbody);
jsonTableContainer.appendChild(table);
if (sortable == true) {
table.className = "sortable";
sorttable.makeSortable(document.getElementById(tableId));
}
if (selectableRows === "multi" || selectableRows === "single") {
const rows = table.getElementsByTagName("tr");
for (let i = 1; i < rows.length; i++) {
rows[i].addEventListener("click", function() {
if (selectableRows === "multi") {
this.classList.toggle("selected");
if (this.classList.contains("selected")) {
const event = new CustomEvent("filterChange", {
detail: {
filterId: valueId,
filterValue: this.value,
filterActions: ["selected"],
filterTargets: filterTargets,
},
});
document.dispatchEvent(event);
} else {
const event = new CustomEvent("filterChange", {
detail: {
filterId: valueId,
filterValue: this.value,
filterActions: ["deselected"],
filterTargets: filterTargets,
},
});
document.dispatchEvent(event);
}
} else if (selectableRows === "single") {
const event = new CustomEvent("filterChange", {
detail: {
filterId: valueId,
filterValue: this.value,
filterActions: ["selected"],
filterTargets: filterTargets,
},
});
document.dispatchEvent(event);
if (this.classList.contains("selected")) {
this.classList.remove("selected");
} else {
for (let j = 1; j < rows.length; j++) {
rows[j].classList.remove("selected");
}
this.classList.add("selected");
}
}
});
if (defaultFirstSelected == true) {
if (i == 1) {
rows[i].classList.add("selected");
const event = new CustomEvent("filterChange", {
detail: {
filterId: valueId,
filterValue: rows[i].value,
filterActions: ["selected"],
filterTargets: filterTargets,
},
});
document.dispatchEvent(event);
}
}
}
}
}
document.addEventListener("filterChange", function(event) {
tableId = document.getElementById(tableId).id;
if (event.detail.filterTargets.includes(tableId)) {
query = queryConstructor();
fetchDataForTable(query);
}
});
let query = queryConstructor();
fetchDataForTable(query);
}
function createChart(
chart,
chatId,
endpoint,
xAxisField,
yAxisField,
chartType,
xAxisType,
scaleChart = false,
formatValueDecimalPlaces = null,
) {
async function fetchDataForChart(query, valueId) {
try {
const apiEndpoint = `${apiURL}/${endpoint}?${query}`;
const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const fetchedData = await response.json();
const newData = fetchedData.reduce((acc, item) => {
const objectId = item[valueId];
if (!acc[objectId]) {
acc[objectId] = [];
}
acc[objectId].push([item[xAxisField], item[yAxisField]]);
return acc;
}, {});
chartData = { ...chartData, ...newData };
updateChart();
} catch (error) {
console.error("Fetching data failed:", error);
}
}
function updateChart() {
let chartDataMap = new Map();
for (let objectId in chartData) {
chartDataMap.set(objectId, chartData[objectId]);
}
var option = {
tooltip: {
...tooltip,
valueFormatter(value, index) {
return formatValueDecimalPlaces == null
? value
: nFormatter(value, formatValueDecimalPlaces);
},
},
xAxis: {
type: xAxisType,
},
yAxis: {
scale: scaleChart,
type: "value",
},
series: Array.from(chartDataMap.entries()).map(([name, data]) => ({
name,
type: chartType,
data,
showSymbol: false,
})),
};
chart.setOption(option, true);
}
// listen for filter events for this target
document.addEventListener("filterChange", function(event) {
eventDetail = event.detail;
if (eventDetail.filterActions.includes("refresh")) {
chartData = [];
updateChart();
} else {
if (eventDetail.filterTargets.includes(chatId)) {
if (eventDetail.filterActions.includes("selected")) {
valueId = eventDetail.filterId;
let selectedRow = {
[valueId]: eventDetail.filterValue,
};
query = queryConstructor(selectedRow);
fetchDataForChart(query, valueId);
} else {
delete chartData[eventDetail.filterValue];
updateChart();
}
}
}
});
}
document.addEventListener("DOMContentLoaded", function() {
chartData = [];
let chartDom = document.getElementById("bitcoin-business-growth-chart");
let myChart = echarts.init(chartDom);
const filterPeriods = {"7 day": 7, "28 day": 28, "365 day": 365, "2 years": 730, "5 years": 1826, "10 years": 3652, "all time": 10000};
dropDownFilter("days_ago_dropdown_filter", "days_ago", ["bitcoin-business-growth-chart", "bitcoin-business-growth-table"], filterPeriods, "365 day")
createTable(
endpoint = "bitcoin_business_growth_percent_diff",
tableId = "bitcoin-business-growth-table",
headers = {
'country_name': 'Country',
'date_range': 'Date Range',
'first_value': 'Previous #',
'last_value': 'Current #',
'difference': 'Diff',
'percent_difference': '% Diff'
},
maxHeight = "400px",
sortable = true,
valueId = "country_name",
selectableRows = "multi",
filterTargets = "bitcoin-business-growth-chart",
defaultFirstSelected = true
)
createChart(
chart = myChart,
chartId = "bitcoin-business-growth-chart",
endpoint = "bitcoin_business_growth_timeseries",
xAxisField = "date",
yAxisField = "cumulative_value",
chartType = "line",
xAxisType = "category",
);
});

View File

@ -0,0 +1,261 @@
// let chartData = [];
// const myChart = echarts.init(document.getElementById("chart"));
// const filename = "final__hashrate.json";
// const periods = [
// "all time",
// "last 7 days",
// "last 28 days",
// "last 365 days",
// "last 2 years",
// ];
//
// async function fetchDataForChart(selectedValue) {
// try {
// const apiEndpoint = `${apiURL}/get_json/${filename}?period=${selectedValue}`;
// const response = await fetch(apiEndpoint);
// if (!response.ok) {
// throw new Error(`HTTP error! status: ${response.status}`);
// }
// const chartData = await response.json();
// initEchart(chartData);
// } catch (error) {
// console.error("Fetching data failed:", error);
// }
// }
//
// function initEchart(chartData) {
// const option = {
// backgroundColor: backgroundColor,
// tooltip: {
// ...tooltip,
// valueFormatter(value, index) {
// return nFormatter(value, 0);
// },
// },
// toolbox: toolboxParams,
// xAxis: {
// data: chartData.map((row) => row.date),
// axisTick: axisTick,
// axisLabel: axisLabel,
// axisLine: axisLine,
// },
// grid: grid,
// dataZoom: dataZoom(0),
// yAxis: [
// {
// type: "value",
// name: "Hashrate (H/s)",
// nameLocation: "middle",
// nameGap: 30,
// nameTextStyle: textStyleMain,
// position: "left",
// alignTicks: true,
// axisTick: axisTick,
// splitLine: {
// show: false,
// },
// axisLine: axisLine,
// axisLabel: {
// fontSize: 12 * fontScale,
// color: textColor,
// formatter(value, index) {
// return nFormatter(value, 0);
// },
// },
// },
// {
// type: "value",
// name: "Difficulty",
// nameLocation: "middle",
// nameGap: 30,
// nameTextStyle: textStyleMain,
// axisTick: axisTick,
// position: "right",
// alignTicks: true,
// axisLine: axisLine,
// splitLine: {
// show: false,
// },
// axisLabel: {
// fontSize: 12 * fontScale,
// color: textColor,
// formatter(value, index) {
// return nFormatter(value, 0);
// },
// },
// },
// ],
// series: [
// {
// type: "line",
// name: "Hashrate",
// barCategoryGap: "0%",
// data: chartData.map((row) => row.hashrate),
// },
// {
// type: "line",
// color: "red",
// id: "rolling-average",
// name: "MA28",
// showSymbol: false,
// barCategoryGap: "0%",
// data: chartData.map((row) => row.hashrate28),
// },
// {
// type: "line",
// yAxisIndex: 1,
// name: "Difficulty",
// barCategoryGap: "0%",
// data: chartData.map((row) => row.difficulty),
// },
// ],
// };
//
// myChart.setOption(option);
// }
//
// document.addEventListener("DOMContentLoaded", function () {
// let periodIndex = 2;
// periodDropdown(periods, periodIndex);
// fetchDataForChart(periods[periodIndex]);
// const selectElement = document.getElementById("select-period-dropdown");
//
// selectElement.addEventListener("change", function (event) {
// const selectedValue = event.target.value;
// fetchDataForChart(selectedValue);
// });
// });
function createChart(
chart,
endpoint,
xAxisType,
scaleChart = false,
) {
async function fetchDataForChart(query) {
try {
const apiEndpoint = `${apiURL}/${endpoint}?${query}`;
const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const chartData = await response.json();
console.log(chartData)
updateChart(chartData);
} catch (error) {
console.error("Fetching data failed:", error);
}
}
function updateChart(chartData) {
var option = {
tooltip: {
...tooltip,
},
xAxis: {
type: xAxisType,
data: chartData.map((item) => item.date),
},
yAxis: [
{
type: "value",
name: "Hashrate (H/s)",
nameLocation: "middle",
nameGap: 30,
nameTextStyle: textStyleMain,
position: "left",
alignTicks: true,
axisTick: axisTick,
splitLine: {
show: false,
},
axisLine: axisLine,
axisLabel: {
fontSize: 12 * fontScale,
color: textColor,
formatter(value, index) {
return nFormatter(value, 0);
},
},
},
{
type: "value",
name: "Difficulty",
nameLocation: "middle",
nameGap: 30,
nameTextStyle: textStyleMain,
axisTick: axisTick,
position: "right",
alignTicks: true,
axisLine: axisLine,
splitLine: {
show: false,
},
axisLabel: {
fontSize: 12 * fontScale,
color: textColor,
formatter(value, index) {
return nFormatter(value, 0);
},
},
},
],
series: [
{
type: "line",
name: "Hashrate",
barCategoryGap: "0%",
data: chartData.map((row) => row.hashrate),
},
{
type: "line",
color: "red",
id: "rolling-average",
name: "MA28",
showSymbol: false,
barCategoryGap: "0%",
data: chartData.map((row) => row.hashrate28),
},
{
type: "line",
yAxisIndex: 1,
name: "Difficulty",
barCategoryGap: "0%",
data: chartData.map((row) => row.difficulty),
},
],
};
chart.setOption(option, true);
}
// listen for filter events for this target
document.addEventListener("filterChange", function(event) {
eventDetail = event.detail;
if (eventDetail.filterActions.includes("refresh")) {
chartData = [];
query = queryConstructor();
fetchDataForChart(query);
updateChart(chartData);
}
});
let query = queryConstructor();
fetchDataForChart(query);
}
document.addEventListener("DOMContentLoaded", function() {
chartData = [];
let chartDom = document.getElementById("hashrate-chart");
let myChart = echarts.init(chartDom);
const filterPeriods = { "7 day": 7, "28 day": 28, "365 day": 365, "2 years": 730, "5 years": 1826, "10 years": 3652, "all time": 10000 };
dropDownFilter("days_ago_dropdown_filter", "days_ago", ["hashrate-chart"], filterPeriods, "365 day")
createChart(
chart = myChart,
endpoint = "bitcoin_hashrate",
xAxisType = "category",
);
});

View File

@ -1,9 +1,8 @@
var chartDom = document.getElementById(id); function createChart(
var myChart = echarts.init(chartDom); chart,
chartData = [];
function simpleChart(
endpoint, endpoint,
formatValueDecimalPlaces = null, xAxisType,
scaleChart = false,
) { ) {
async function fetchDataForChart(query) { async function fetchDataForChart(query) {
try { try {
@ -12,26 +11,21 @@ function simpleChart(
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
chartData = await response.json(); const chartData = await response.json();
updateChart(); console.log(chartData)
console.log(chartData); updateChart(chartData);
} catch (error) { } catch (error) {
console.error("Fetching data failed:", error); console.error("Fetching data failed:", error);
} }
} }
function updateChart() { function updateChart(chartData) {
var option = { var option = {
tooltip: { tooltip: {
...tooltip, ...tooltip,
valueFormatter(value, index) {
return formatValueDecimalPlaces == null
? value
: nFormatter(value, formatValueDecimalPlaces);
},
}, },
xAxis: { xAxis: {
type: "category", type: xAxisType,
data: chartData.map((item) => item.date), data: chartData.map((item) => item.date),
}, },
yAxis: [ yAxis: [
@ -122,15 +116,36 @@ function simpleChart(
}, },
], ],
}; };
chart.setOption(option, true);
myChart.setOption(option, true);
} }
query = queryConstructor(); // listen for filter events for this target
fetchDataForChart(query);
document.addEventListener("filterChange", function(event) { document.addEventListener("filterChange", function(event) {
eventDetail = event.detail;
if (eventDetail.filterActions.includes("refresh")) {
chartData = [];
query = queryConstructor(); query = queryConstructor();
fetchDataForChart(query); fetchDataForChart(query);
}); updateChart(chartData);
} }
simpleChart((endpoint = "miner_rewards")); });
let query = queryConstructor();
fetchDataForChart(query);
}
document.addEventListener("DOMContentLoaded", function() {
chartData = [];
let chartDom = document.getElementById("miner-rewards-chart");
let myChart = echarts.init(chartDom);
const filterPeriods = { "7 day": 7, "28 day": 28, "365 day": 365, "2 years": 730, "5 years": 1826, "10 years": 3652, "all time": 10000 };
dropDownFilter("days_ago_dropdown_filter", "days_ago", ["miner-rewards-chart"], filterPeriods, "365 day")
createChart(
chart = myChart,
endpoint = "miner_rewards",
xAxisType = "category",
);
});

View File

@ -1,127 +0,0 @@
let dataArr = [];
const myChart = echarts.init(document.getElementById("chart"));
const filename = "final__feerate_percentiles.json";
const periods = [
"all time",
"last 7 days",
"last 28 days",
"last 365 days",
"last 2 years",
];
async function fetchDataForChart(selectedValue) {
try {
const apiEndpoint = `${apiURL}/get_json/${filename}?period=${selectedValue}`;
const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const dataArr = await response.json();
initEchart(dataArr);
} catch (error) {
console.error("Fetching data failed:", error);
}
}
function initEchart(dataArr) {
const option = {
backgroundColor: backgroundColor,
tooltip: {
...tooltip,
valueFormatter: (value) => `${value.toFixed(0)} sats/vByte`,
},
toolbox: toolboxParams,
xAxis: {
data: dataArr.map((row) => row.date),
axisTick: axisTick,
axisLabel: axisLabel,
axisLine: axisLine,
},
yAxis: {
axisTick: axisTick,
splitLine: {
show: false,
},
axisLabel: axisLabel,
axisLine: axisLine,
},
grid: grid,
dataZoom: dataZoom(0),
series: [
{
type: "line",
id: "min-feerate",
name: "min",
showSymbol: false,
barCategoryGap: "0%",
data: [0],
},
{
type: "bar",
stack: "value",
name: "10th",
barCategoryGap: "0%",
data: dataArr.map((row) => row.feerate_10th),
},
{
type: "bar",
stack: "value",
name: "25th",
barCategoryGap: "0%",
data: dataArr.map((row) => row.feerate_25th),
},
{
type: "bar",
stack: "value",
name: "50th",
barCategoryGap: "0%",
data: dataArr.map((row) => row.feerate_50th),
},
{
type: "line",
id: "mean-feerate",
name: "mean",
showSymbol: false,
barCategoryGap: "0%",
data: [0],
},
{
type: "bar",
stack: "value",
name: "75th",
barCategoryGap: "0%",
data: dataArr.map((row) => row.feerate_75th),
},
{
type: "bar",
stack: "value",
name: "90th",
barCategoryGap: "0%",
data: dataArr.map((row) => row.feerate_90th),
},
{
type: "line",
id: "max-feerate",
name: "max",
showSymbol: false,
barCategoryGap: "0%",
data: [0],
},
],
};
myChart.setOption(option);
}
document.addEventListener("DOMContentLoaded", function () {
let periodIndex = 2;
periodDropdown(periods, periodIndex);
fetchDataForChart(periods[periodIndex]);
const selectElement = document.getElementById("select-period-dropdown");
selectElement.addEventListener("change", function (event) {
const selectedValue = event.target.value;
fetchDataForChart(selectedValue);
});
});

View File

@ -1,127 +0,0 @@
let dataArr = [];
const myChart = echarts.init(document.getElementById("chart"));
const filename = "final__hashrate.json";
const periods = [
"all time",
"last 7 days",
"last 28 days",
"last 365 days",
"last 2 years",
];
async function fetchDataForChart(selectedValue) {
try {
const apiEndpoint = `${apiURL}/get_json/${filename}?period=${selectedValue}`;
const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const dataArr = await response.json();
initEchart(dataArr);
} catch (error) {
console.error("Fetching data failed:", error);
}
}
function initEchart(dataArr) {
const option = {
backgroundColor: backgroundColor,
tooltip: {
...tooltip,
valueFormatter(value, index) {
return nFormatter(value, 0);
},
},
toolbox: toolboxParams,
xAxis: {
data: dataArr.map((row) => row.date),
axisTick: axisTick,
axisLabel: axisLabel,
axisLine: axisLine,
},
grid: grid,
dataZoom: dataZoom(0),
yAxis: [
{
type: "value",
name: "Hashrate (H/s)",
nameLocation: "middle",
nameGap: 30,
nameTextStyle: textStyleMain,
position: "left",
alignTicks: true,
axisTick: axisTick,
splitLine: {
show: false,
},
axisLine: axisLine,
axisLabel: {
fontSize: 12 * fontScale,
color: textColor,
formatter(value, index) {
return nFormatter(value, 0);
},
},
},
{
type: "value",
name: "Difficulty",
nameLocation: "middle",
nameGap: 30,
nameTextStyle: textStyleMain,
axisTick: axisTick,
position: "right",
alignTicks: true,
axisLine: axisLine,
splitLine: {
show: false,
},
axisLabel: {
fontSize: 12 * fontScale,
color: textColor,
formatter(value, index) {
return nFormatter(value, 0);
},
},
},
],
series: [
{
type: "line",
name: "Hashrate",
barCategoryGap: "0%",
data: dataArr.map((row) => row.hashrate),
},
{
type: "line",
color: "red",
id: "rolling-average",
name: "MA28",
showSymbol: false,
barCategoryGap: "0%",
data: dataArr.map((row) => row.hashrate28),
},
{
type: "line",
yAxisIndex: 1,
name: "Difficulty",
barCategoryGap: "0%",
data: dataArr.map((row) => row.difficulty),
},
],
};
myChart.setOption(option);
}
document.addEventListener("DOMContentLoaded", function () {
let periodIndex = 2;
periodDropdown(periods, periodIndex);
fetchDataForChart(periods[periodIndex]);
const selectElement = document.getElementById("select-period-dropdown");
selectElement.addEventListener("change", function (event) {
const selectedValue = event.target.value;
fetchDataForChart(selectedValue);
});
});

View File

@ -0,0 +1,90 @@
const fontScale = 0.8;
const backgroundColor = "#f9f9f9";
const tooltipBgColor = "#e7e7f5";
const textColor = "#373841";
const textStyleMain = {
fontFamily: "sans-serif",
fontSize: 12 * fontScale,
color: textColor,
};
const tooltip = {
borderColor: textColor,
backgroundColor: tooltipBgColor,
order: "seriesDesc",
textStyle: textStyleMain,
trigger: "axis",
};
const toolboxParams = {
itemSize: 16 * fontScale,
showTitle: true,
top: "-1%",
right: "20%",
iconStyle: {
borderColor: textColor,
borderWidth: 2,
},
feature: {
dataZoom: {
yAxisIndex: "none",
},
dataView: {},
saveAsImage: {
pixelRatio: 2,
},
},
};
const axisLabel = {
fontSize: 12 * fontScale,
color: textColor,
margin: 10,
};
const axisLine = {
show: true,
lineStyle: {
color: textColor,
width: 2,
},
};
const axisTick = { show: true };
const grid = {
bottom: 30,
top: 10,
left: 15,
containLabel: true,
};
const yaxisTextStyle = {
fontSize: 12 * fontScale,
padding: [0, 0, 10, 0],
color: textColor,
};
const yaxisTextStyle2 = {
fontSize: 12 * fontScale,
padding: [50, 0, 0, 0],
color: textColor,
};
function dataZoom() {
const dataZoom = [
{
type: "inside",
},
];
return dataZoom;
}
window.addEventListener("resize", function () {
if (myChart != null && myChart != undefined) {
myChart.resize();
}
});

45
static/js/lib/echarts.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -5,7 +5,7 @@ function queryConstructor(customFilters = {}) {
Object.assign(queryObject, customFilters); Object.assign(queryObject, customFilters);
filters.forEach((filter) => { filters.forEach((filter) => {
const filterId = filter.getAttribute("idFilter"); const filterId = filter.filterId;
const filterValue = filter.value; const filterValue = filter.value;
queryObject[filterId] = filterValue; queryObject[filterId] = filterValue;
}); });
@ -14,3 +14,51 @@ function queryConstructor(customFilters = {}) {
return queryString; return queryString;
} }
function dispatchDropdownEvent(selectElement, filterId, filterTargets) {
const event = new CustomEvent('filterChange', {
detail: {
filterId: filterId,
filterValue: selectElement.value,
filterActions: ["refresh"],
filterTargets: filterTargets
}
});
document.dispatchEvent(event);
}
function dropDownFilter(domId, filterId, filterTargets, options, defaultKey) {
domFilter = document.getElementById(domId);
domFilter.filterId = filterId;
for (const [key, value] of Object.entries(options)) {
const option = document.createElement('option');
option.value = value;
option.textContent = key;
domFilter.appendChild(option);
if (key == defaultKey) {
option.selected = true;
}
}
dispatchDropdownEvent(domFilter, filterId, filterTargets)
domFilter.addEventListener('change', () => dispatchDropdownEvent(domFilter, filterId, filterTargets));
}
function nFormatter(value, digits) {
const lookup = [
{ value: 1, symbol: "" },
{ value: 1e3, symbol: "k" },
{ value: 1e6, symbol: "M" },
{ value: 1e9, symbol: "G" },
{ value: 1e12, symbol: "T" },
{ value: 1e15, symbol: "P" },
{ value: 1e18, symbol: "E" },
];
const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
const item = lookup
.slice()
.reverse()
.find((item) => value >= item.value);
return item
? (value / item.value).toFixed(digits).replace(rx, "$1") + item.symbol
: "0";
}