Compare commits

..

No commits in common. "master" and "new-backend" have entirely different histories.

45 changed files with 1410 additions and 1410 deletions

View file

@ -1,3 +1,3 @@
# My personal website ([https://baseddata.io](https://baseddata.io))
# My personal website ([https://bitlab21.com](https://bitlab21.com))
This site is made using [Hugo](https://gohugo.io/), a static website generator. The backend (which powers the api for getting data for the charts) is build in python and served using gunicorn.

View file

@ -12,14 +12,6 @@ def mangrove_by_country_latest():
order by cumulative_pixels_diff desc
"""
def mangrove_country_timeseries(args):
country_name = args["country_with_parent"]
return f"""
select year, total_n_pixels from models_final.final__protected_mangroves_summary_stats_by_country_agg
where country_with_parent = '{country_name}'
order by year
"""
def bitcoin_business_growth_timeseries(args):
days_ago = parse_int(args["days_ago"])
country_name = args["country_name"]
@ -71,7 +63,7 @@ def bitcoin_business_growth_percent_diff_days_ago(args):
last_value,
last_value - first_value as difference,
round(
100 * models_final.safe_divide((last_value - first_value), first_value), 2
100 * safe_divide((last_value - first_value), first_value), 2
) as percent_difference
from first_and_last_values
)
@ -80,55 +72,144 @@ def bitcoin_business_growth_percent_diff_days_ago(args):
where days_ago = 1
order by difference desc
"""
def miner_rewards(args):
days_ago = parse_int(args["days_ago"])
return f"""
with
filtered_data as (
select * from models_final.final__miner_rewards order by date desc limit {days_ago}
)
select *
from filtered_data
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
"""
# def bitcoin_business_growth_timeseries(query):
# pipeline = [
# {
# "$match": {
# "days_ago": {"$lte": int(query["days_ago"])},
# "country_name": query["country_name"],
# }
# },
# {
# "$project": {
# "country_name": "$country_name",
# "date": "$date",
# "cumulative_value": "$cumulative_value",
# }
# },
# {"$sort": {"country_name": 1, "days_ago": 1}},
# ]
# return pipeline
# def mangrove_by_country_latest():
# pipeline = [
# {
# "$match": {"year": "2020"},
# },
# ]
# return pipeline
#
#
# def mangrove_by_country_agg(query):
# pipeline = [
# {"$match": {"country_with_parent": query["country_with_parent"]}},
# {
# "$group": {
# "_id": {"country_with_parent": "$country_with_parent", "year": "$year"},
# "total_pixels": {"$sum": "$total_n_pixels"},
# }
# },
# {
# "$project": {
# "_id": 0,
# "country_with_parent": "$_id.country_with_parent",
# "year": "$_id.year",
# "total_pixels": 1,
# }
# },
# {"$sort": {"year": 1}},
# ]
# return pipeline
#
#
# def bitcoin_business_growth_timeseries(query):
# pipeline = [
# {
# "$match": {
# "days_ago": {"$lte": int(query["days_ago"])},
# "country_name": query["country_name"],
# }
# },
# {
# "$project": {
# "country_name": "$country_name",
# "date": "$date",
# "cumulative_value": "$cumulative_value",
# }
# },
# {"$sort": {"country_name": 1, "days_ago": 1}},
# ]
# return pipeline
#
#
# def bitcoin_business_growth_percent_diff_days_ago(query):
pipeline = [
{"$match": {"days_ago": {"$lte": int(query["days_ago"])}}},
{"$sort": {"country_name": 1, "days_ago": 1}},
{
"$group": {
"_id": "$country_name",
"firstvalue": {"$first": "$cumulative_value"},
"lastvalue": {"$last": "$cumulative_value"},
"firstdate": {"$min": "$date"},
"lastdate": {"$max": "$date"},
}
},
{
"$project": {
"country_name": "$_id",
"first_value": "$firstvalue",
"last_value": "$lastvalue",
"difference": {
"$subtract": [
{"$todouble": "$firstvalue"},
{"$todouble": "$lastvalue"},
]
},
"first_date": "$firstdate",
"last_date": "$lastdate",
"percent_difference": {
"$cond": {
"if": {"$eq": [{"$todouble": "$lastvalue"}, 0]},
"then": {
"$cond": {
"if": {"$gt": [{"$todouble": "$firstvalue"}, 0]},
"then": "new",
"else": "none",
}
},
"else": {
"$round": [
{
"$multiply": [
{
"$divide": [
{
"$subtract": [
{"$todouble": "$firstvalue"},
{"$todouble": "$lastvalue"},
]
},
{"$todouble": "$lastvalue"},
]
},
100,
]
}
]
},
}
},
}
},
]
return pipeline
#
#
# def bitcoin_business_growth_latest(query):
# pipeline = [
# {
# "$match": query["filter"],
# },
# {"$sort": {"date": 1}},
# ]
# return pipeline

View file

@ -1,9 +1,6 @@
from psycopg2.extras import RealDictCursor
from dotenv import load_dotenv
import psycopg2, os
load_dotenv()
class PostgresHandler:
def __init__(self):
self.connection = self.connect_to_pg()

View file

@ -24,19 +24,6 @@ async def mangrove_by_country_latest():
serializedData = serializer.serialize_many(rawData)
return serializedData
@router.get("/mangrove_country_timeseries")
async def mangrove_country_timeseries(query: str):
args = parse_args_to_dict(query)
pipeline = pipelines.mangrove_country_timeseries(args)
handler = PostgresHandler()
schema = schemas.mangrove_country_timeseries_schema
serializer = DataSerializer(schema)
rawData = handler.execute_query(pipeline)
serializedData = serializer.serialize_many(rawData)
return serializedData
@router.get("/bitcoin_business_growth_timeseries")
async def bitcoin_business_growth_timeseries(query: str):
args = parse_args_to_dict(query)
@ -66,58 +53,44 @@ async def bitcoin_business_growth_percent_diff(query: str):
serializedData = serializer.serialize_many(rawData)
return serializedData
@router.get("/miner_rewards")
async def miner_rewards(query: str):
args = parse_args_to_dict(query)
# @router.get("/bitcoin_business_growth_percent_diff")
# async def bitcoin_business_growth_percent_diff(query: str):
# query = ast.literal_eval(query)
#
# query = queries.bitcoin_business_growth_percent_diff_days_ago(query)
# handler = PostgresHandler(connection)
#
# schema = schemas.bitcoin_business_growth_percent_diff_schema
# pipeline = pipelines.bitcoin_business_growth_percent_diff_days_ago(query)
# serializer = DataSerializer(schema)
# handler = MongoDBHandler(collection_name)
# rawData = handler.aggregate(pipeline)
# serializedData = serializer.serialize_many(rawData)
# return serializedData
# @router.get("/mangrove_by_country_agg")
# async def mangrove_by_country_agg(query: str):
# query = ast.literal_eval(query)
# db = client.baseddata
# collection_name = db["final__protected_mangroves_summary_stats_by_country_agg"]
# schema = schemas.mangrove_by_country_agg_schema
# pipeline = pipelines.mangrove_by_country_agg(query)
# serializer = DataSerializer(schema)
# handler = MongoDBHandler(collection_name)
# rawData = handler.aggregate(pipeline)
# serializedData = serializer.serialize_many(rawData)
# return serializedData
#
pipeline = pipelines.miner_rewards(args)
handler = PostgresHandler()
# @router.get("/bitcoin_business_growth_timeseries")
# async def bitcoin_business_growth_timeseries(query: str):
# query = ast.literal_eval(query)
# db = client.baseddata
# collection_name = db["final__bitcoin_business_growth_by_country"]
# schema = schemas.bitcoin_business_growth_timeseries_schema
# pipeline = pipelines.bitcoin_business_growth_timeseries(query)
# serializer = DataSerializer(schema)
# handler = MongoDBHandler(collection_name)
# rawData = handler.aggregate(pipeline)
# serializedData = serializer.serialize_many(rawData)
# return serializedData
schema = schemas.miner_rewards_schema
serializer = DataSerializer(schema)
rawData = handler.execute_query(pipeline)
serializedData = serializer.serialize_many(rawData)
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

@ -7,22 +7,13 @@ def mangrove_by_country_latest_schema(data):
"cumulative_pct_diff": float(data["cumulative_pct_diff"]),
}
def mangrove_country_timeseries_schema(data):
return {
"year": str(data["year"]),
"total_n_pixels": int(data["total_n_pixels"]),
}
def mangrove_by_country_agg_schema(data):
return {
"country_with_parent": str(data["country_with_parent"]),
"year": int(data["year"]),
"total_pixels": int(data["total_pixels"]),
"total_pixels": int(data["total_pixels"])
}
def bitcoin_business_growth_percent_diff_schema(data):
return {
"country_name": str(data["country_name"]),
@ -30,52 +21,14 @@ def bitcoin_business_growth_percent_diff_schema(data):
"first_value": int(data["first_value"]),
"last_value": int(data["last_value"]),
"difference": int(data["difference"]),
"percent_difference": str(data["percent_difference"]),
"percent_difference": str(data["percent_difference"])
}
def bitcoin_business_growth_timeseries_schema(data):
return {
"country_name": str(data["country_name"]),
"date": data["date"],
"cumulative_value": int(data["cumulative_value"]),
}
def miner_rewards_schema(data):
return {
"date": data["date"],
"block_subsidy": data["block_subsidy"],
"total_reward_usd": data["total_reward_usd"],
"totalfee_usd": data["totalfee_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"],
"cumulative_value": int(data["cumulative_value"])
}
class DataSerializer:
@ -83,7 +36,7 @@ class DataSerializer:
self.schema_func = schema_func
def serialize_one(self, data) -> dict:
return self.schema_func(dict(data))
return self.schema_func(dict( data ))
def serialize_many(self, data_list) -> list:
return [self.serialize_one(data) for data in data_list]

View file

@ -1,5 +1,5 @@
[Unit]
Description=Uvicorn instance to serve baseddata.io
Description=Gunicorn instance to serve baseddata.io
After=network.target
[Service]
@ -7,7 +7,7 @@ User=admin
Group=www-data
WorkingDirectory=/var/www/baseddata.io/backend
Environment="PATH=/var/www/baseddata.io/.venv/bin"
ExecStart=/var/www/baseddata.io/.venv/bin/uvicorn --workers 4 --uds /var/sockets/baseddata.sock main:app
ExecStart=/var/www/baseddata.io/.venv/bin/gunicorn --workers 4 --bind unix:baseddata.sock -m 007 app:app
[Install]
WantedBy=multi-user.target

View file

@ -1,21 +1,6 @@
from fastapi import FastAPI
from api.route import router
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.include_router(router)
origins = [
"https://baseddata.io",
"https://api.baseddata.io",
]
# Add the CORS middleware to the app
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

5
backend/requirements.txt Normal file
View file

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

View file

@ -1,43 +0,0 @@
## Sam Chance | Analytics Engineer | MSc Marine Biology | United Kingdom
***contact@sjplab.com***
4 years experience working in the tech industry as a Data Analyst & Engineer for a geocoding firm in London. Strong educational background with two STEM degrees. Excellent communication skills with competence at articulating complex ideas to non-technical people.
## Experience
### Growth Analyst / Analytics Engineer | What3Words | May 2020 to Apr 2024 (4 years)
*Engineered data pipelines and managed geospatial data for geocoding company that serves 200k daily users*
- Engineered & implemented alerting system from scratch to detect anomalous behaviour in partner API usage, resulting in 30+ actionable alerts per month & facilitating service improvements to paying customers
- Developed ETL orchestration platform using Airflow & DBT to automate 40+ DAGS & 140+ data models
- Lead GIS project to map and classify company spatial data using OpenStreetMaps
- Migrated data analytics from Firebase to Mixpanel, enhancing self-serviceability of analytics & reducing work-load of Data Team by 60% from ad-hoc requests
- Architected 80% of company KPI dashboard used by external stakeholders & potential investors
- Designed & implemented QA tool for Product Team to monitor potential issues in new app releases
- Promoted to Analytics Engineer after starting as a Growth Analyst
## Skills
- **Data Warehousing**: BigQuery | Postgres
- **Data Analysis & Modelling**: Python (pandas, numpy) & R (dplyr) | DBT | SQL | Data Pipeline Design & Development
- **Data Visualization**: matplotlib | ggplot | Looker Studio | Apache ECharts
- **Programming Languages**: *proficient*: SQL, Python, Bash, R | *learning*: JavaScript
- **Workflow Orchestration**: Apache Airflow | Prefect
- **Sysadmin**: Linux | Networking | SSH | Reverse Proxy (Nginx) | API Management | Containerization (Docker, LXC) | Nixos
- **Geographic Information Systems**: ArcGIS | QGIS | PostGIS | GDAL | OpenStreetMaps
- **Code Collaboration & Version Control**: Git, Github, Self Hosted Gitea
- **CLI wizard**: Scripting (Bash, Python) | GNU Coreutils | Neovim
- **Web Development**: Hugo | JavaScript/HTML/CSS | Apache ECharts
- **Personal**: Full UK Driving License | Flexible Working | Remote Working | Autonomous Worker
## Education
### MSc Marine Biology | Bangor University | 1 year
*Advanced theoretical & practical training with focus on GIS skills*
- **Thesis**: "Quantifying the Effectiveness of Indonesia's Protected Areas at Preventing Mangrove Deforestation"
* A GIS project that involved statistical modelling of satellite data using ArcGIS and R
### BSc Marine Biology & Zoology | Bangor University | 3 years
## Personal Websites
- **https://semitamaps.com**: A free high-quality map printing service using self-hosted vector tiles generated from OpenStreetMaps and visualised with Maplibre.
- **https://baseddata.io**: A site where I publish analytical content based on open data. Still a work in progress.
- **https://git.bitlab21.com**: Self-hosted Gitea instance where I host my code.

View file

@ -5,12 +5,10 @@ author:
name: "Sam Chance"
header_image: "/pics/charts/feerate-percentile.webp"
summary: "Bar chart showing historical median daily feerate percentiles for the Bitcoin protocol."
chart: "/js/feerate_percentile.js"
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
protocol.
{{< dropdown_filter id="days_ago_dropdown_filter" >}}
{{< chart id="feerate-percentiles-chart" >}}
{{< chart src="/js/feerate-percentile.js" >}}

View file

@ -1,23 +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"]
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

@ -0,0 +1,28 @@
---
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-chart" endpoint="bitcoin_business_growth_timeseries" chartType="line" xAxisField="date" yAxisField="cumulative_value" scaleChart=true >}}
#### Attribution and License
Data obtained from © [OpenStreetMap](https://www.openstreetmap.org/copyright)
Licensed under the [ODbl](https://opendatacommons.org/licenses/odbl/)

View file

@ -6,10 +6,8 @@ author:
header_image: "/pics/charts/hashrate.webp"
summary: "Timeseries chart showing the Bitcoin network hashrate and difficulty."
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.
{{< dropdown_filter id="days_ago_dropdown_filter" >}}
{{< chart id = "hashrate-chart" >}}
{{< chart src="/js/hashrate.js" >}}

View file

@ -7,11 +7,8 @@ summary: "Miner rewards"
header_image: "/pics/charts/rewards.webp"
draft: false
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/).
{{< dropdown_filter id="days_ago_dropdown_filter" >}}
{{< chart id="miner-rewards-chart" >}}
{{< chart src="/js/miner-rewards.js" >}}

View file

@ -6,11 +6,11 @@ author:
header_image: "/pics/charts/price.webp"
summary: "Daily bitcoin price. Data is obtained from CoinGecko using their public API."
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
public API.
{{< dropdown_filter id="days_ago_dropdown_filter" >}}
{{< chart id = "bitcoin-price-chart" >}}
{{< bitcoin-price >}}
{{< chart src="/js/bitcoin-price.js" >}}

View file

@ -7,10 +7,10 @@ header_image: "/pics/charts/price.webp"
summary: "Daily bitcoin price. Data is obtained from CoinGecko using their public API."
tags: ["Bitcoin", "Stats"]
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" >}}
{{< chart id="mangrove-country-timeseries-chart" endpoint="mangrove_country_timeseries" chartType="bar" xAxisField="year" yAxisField="total_n_pixels" scaleChart=true xAxisType="category" >}}
{{< 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" >}}
{{< chart id="mangrove_countries" endpoint="mangrove_by_country_agg" chartType="bar" xAxisField="year" yAxisField="total_pixels" scaleChart=true >}}
{{< map id="map" style="https://tiles.semitamaps.com/styles/maptiler-basic/style.json">}}
{{< chart id="mangrove-country-timeseries-chart" endpoint="mangrove_country_timeseries" chartType="line" xAxisField="date" yAxisField="n_pixels" scaleChart=true >}}

View file

@ -3,7 +3,7 @@ languageCode = 'en-gb'
title = 'Based Data'
[params]
apiURL = 'https://api.baseddata.io'
apiURL = 'http://localhost:8000'
[markup.highlight]
pygmentsUseClasses = false

View file

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

View file

@ -0,0 +1,96 @@
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
<script>
chartData = [];
function createChart(
id,
endpoint,
chartType,
xAxisField,
yAxisField,
sortField = null,
scaleChart = false,
) {
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() {
console.log(chartData);
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 nFormatter(value, 0);
},
},
xAxis: {
type: "time",
},
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();
}
}
}
});
}
</script>
<script src="/js/chart-params.js"></script>

View file

@ -8,6 +8,8 @@
<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/charts.css" type="text/css" media="all" />
<script src="/js/lib/sorttable.js"></script>
<script src="/js/lib/helper-functions.js"></script>
<link
rel="stylesheet"
href="/css/codeblock.css"
@ -20,4 +22,7 @@
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"
/>
<script>
const apiURL = "{{ .Site.Params.apiURL }}";
</script>
</html>

142
layouts/partials/table.html Normal file
View file

@ -0,0 +1,142 @@
<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") {
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

@ -0,0 +1,19 @@
<!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,3 +1,10 @@
<section class = 'chart-container' id="{{ .Get "id" }}-container">
<div id="{{ .Get "id" }}" class="chart"></div>
{{ partial "chart.html" }}
<section class = 'chart-container'>
<div class = "chart" id='{{ .Get "id" }}'>
<script>
document.addEventListener("DOMContentLoaded", function () {
createChart(id={{ .Get "id" }}, endpoint={{ .Get "endpoint" }}, chartType={{ .Get "chartType" }}, xAxisField={{ .Get "xAxisField" }}, yAxisField={{ .Get "yAxisField" }}, sortField={{ .Get "sortField" }}, scaleChart={{ .Get "scaleChart" }})
});
</script>
</div>
</section>

View file

@ -1,4 +1,29 @@
<div id="{{ .Get "id" }}-container" class="dropdown-filter-container">
<select id="{{ .Get "id" }}" class="filter dropdown-filter">
</select>
{{ $id := .Get "id" }}
{{ $default_selection := .Get "default_selection" }}
{{ $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>
<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>

View file

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

View file

@ -1,3 +1,8 @@
<section class = 'table-container' id = '{{ .Get "id" }}-container'>
<div id="{{ .Get "id" }}" class="table"></div>
</section>
{{ partial "table.html" }}
<div id = '{{ .Get "id" }}--container'>
<script>
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

@ -14,8 +14,7 @@ in-project = true
python = "^3.11"
fastapi = "^0.115.4"
uvicorn = "^0.32.0"
psycopg2-binary = "^2.9.10"
load-dotenv = "^0.1.0"
psycopg2 = "^2.9.10"
[build-system]

View file

@ -2,7 +2,7 @@
.chart-container {
display: flex;
/* height: 600px; */
aspect-ratio: 2 / 1;
aspect-ratio: 1 / 1;
}
.chart {
@ -10,12 +10,3 @@
height: 100%;
width: 100%;
}
@media (max-width: 600px) {
.chart-container {
display: flex;
/* height: 600px; */
aspect-ratio: 1 / 1;
}
}

View file

@ -0,0 +1,109 @@
let chartData = [];
let myChart;
let periodIndex = 3;
const periods = ["1 day", "7 day", "28 day", "365 day"];
async function fetchDataForChart(str, period) {
try {
const apiEndpoint = `${apiURL}/bitcoin_business_growth_by_country?cumulative_period_type=${period}&countries=${str}`;
console.log("Fetching from " + apiEndpoint);
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.country_name;
if (!acc[objectId]) {
acc[objectId] = [];
}
acc[objectId].push([item.date, item.cumulative_current_value]);
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]);
}
option = {
backgroundColor: backgroundColor,
grid: grid,
tooltip: tooltip,
toolbox: toolboxParams,
xAxis: {
axisTick: axisTick,
axisLabel: axisLabel,
axisLine: axisLine,
type: "category",
},
yAxis: {
axisTick: axisTick,
scale: true,
splitLine: {
show: false,
},
axisLabel: {
fontSize: 12 * fontScale,
color: textColor,
formatter(value, index) {
return nFormatter(value, 2);
},
},
axisLine: axisLine,
},
series: Array.from(chartDataMap.entries()).map(([name, data]) => ({
name,
type: "line",
data,
showSymbol: false,
})),
};
myChart.setOption(option, true);
}
function handleCheckboxChange(event) {
if (event.target.type === "checkbox") {
const boxChecked = event.target.checked;
const boxId = event.target.id;
let selectedPeriod = getPeriodFromDropdown();
// Remove unchecked boxes
if (boxChecked === false) {
delete chartData[boxId];
updateChart();
// Add checked boxes
} else {
fetchDataForChart(boxId, selectedPeriod);
}
}
}
document.addEventListener("DOMContentLoaded", function () {
periodDropdown(periods, periodIndex);
document.addEventListener("reloadTable", () => {
myChart = echarts.init(document.getElementById("chart"));
jsonTableCheckedBoxes = document.querySelectorAll(
'#jsonTableContainer input[type="checkbox"]:checked',
);
const checkedBoxes = Array.from(jsonTableCheckedBoxes).map(
(checkbox) => checkbox.id,
);
const str = checkedBoxes.join(",");
let selectedPeriod = getPeriodFromDropdown();
chartData = [];
fetchDataForChart(str, selectedPeriod);
jsonTable = document.getElementById("jsonTableContainer");
jsonTable.removeEventListener("change", handleCheckboxChange);
jsonTable.addEventListener("change", handleCheckboxChange);
});
});

View file

@ -0,0 +1,95 @@
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

@ -0,0 +1,84 @@
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);
});
});

139
static/js/chart-params.js Normal file
View file

@ -0,0 +1,139 @@
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

@ -1,93 +0,0 @@
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

@ -1,106 +0,0 @@
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

@ -1,279 +0,0 @@
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 = true,
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

@ -1,17 +0,0 @@
// maplibregl.addProtocol("cog", MaplibreCOGProtocol.cogProtocol);
//
// map.on("load", () => {
// map.addSource("imageSource", {
// type: "raster",
// url: `cog://http://localhost:5000/cog?year=${year}&pid=${pid}`,
// tileSize: 256,
// minzoom: 0,
// });
//
// map.addLayer({
// id: "imageLayer",
// source: "imageSource",
// type: "raster",
// });
// });
//

View file

@ -1,261 +0,0 @@
// 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,151 +0,0 @@
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: "Rewards (USD)",
nameLocation: "middle",
nameGap: 30,
nameTextStyle: textStyleMain,
position: "left",
alignTicks: true,
axisTick: axisTick,
axisLine: axisLine,
splitLine: {
show: false,
},
axisLabel: {
...axisLabel,
formatter(value, index) {
return nFormatter(value, 0);
},
},
},
{
type: "value",
name: "Subsidy (BTC)",
nameLocation: "middle",
nameGap: 20,
nameTextStyle: {
fontSize: 12 * fontScale,
color: textColor,
},
axisTick: axisTick,
position: "right",
alignTicks: true,
axisLine: axisLine,
splitLine: {
show: false,
},
axisLabel: {
fontSize: 12 * fontScale,
color: textColor,
},
},
],
series: [
{
type: "line",
name: "Subsidy (USD)",
stack: "Total",
areaStyle: {},
symbol: "none",
lineStyle: {
width: 0,
},
data: chartData.map((row) => row.subsidy_usd),
},
{
type: "line",
name: "Fees (USD)",
stack: "Total",
areaStyle: {},
symbol: "none",
lineStyle: {
width: 0,
},
data: chartData.map((row) => row.totalfee_usd),
},
{
type: "line",
name: "Total (USD)",
symbol: "none",
lineStyle: {
width: 1,
color: textColor,
},
data: chartData.map((row) => row.total_reward_usd),
},
{
type: "line",
yAxisIndex: 1,
name: "Block Subsidy (BTC)",
symbol: "none",
lineStyle: {
width: 3,
},
data: chartData.map((row) => row.block_subsidy),
},
],
};
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("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

@ -0,0 +1,127 @@
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);
});
});

127
static/js/hashrate.js Normal file
View file

@ -0,0 +1,127 @@
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

@ -1,90 +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;
}
window.addEventListener("resize", function () {
if (myChart != null && myChart != undefined) {
myChart.resize();
}
});

File diff suppressed because one or more lines are too long

View file

@ -5,7 +5,7 @@ function queryConstructor(customFilters = {}) {
Object.assign(queryObject, customFilters);
filters.forEach((filter) => {
const filterId = filter.filterId;
const filterId = filter.getAttribute("idFilter");
const filterValue = filter.value;
queryObject[filterId] = filterValue;
});
@ -14,51 +14,3 @@ function queryConstructor(customFilters = {}) {
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";
}

17
static/js/mangrove-map.js Normal file
View file

@ -0,0 +1,17 @@
maplibregl.addProtocol("cog", MaplibreCOGProtocol.cogProtocol);
map.on("load", () => {
map.addSource("imageSource", {
type: "raster",
url: `cog://http://localhost:5000/cog?year=${year}&pid=${pid}`,
tileSize: 256,
minzoom: 0,
});
map.addLayer({
id: "imageLayer",
source: "imageSource",
type: "raster",
});
});

169
static/js/miner-rewards.js Normal file
View file

@ -0,0 +1,169 @@
let dataArr = [];
const myChart = echarts.init(document.getElementById("chart"));
const filename = "final__miner_rewards.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) {
console.log(dataArr);
const option = {
backgroundColor: backgroundColor,
tooltip: {
...tooltip,
formatter: function (params) {
let colors = ["#ee6666", "#000000", "#b0da9d", "#8699d5"];
let colorSpan = (color) =>
'<span style="display:inline-block;margin-right:1px;border-radius:5px;width:9px;height:9px;background-color:' +
color +
'"></span>';
let tooltip = "<p>" + params[0].axisValue + "</p>";
params.reverse().forEach((item, index) => {
let color = colors[index % colors.length];
let labels =
"<p>" +
colorSpan(color) +
" " +
item.seriesName +
": " +
nFormatter(item.data, 3);
("</p>");
tooltip += labels;
});
return tooltip;
},
valueFormatter(value, index) {
return nFormatter(value, 3);
},
},
toolbox: toolboxParams,
xAxis: {
data: dataArr.map((row) => row.date),
axisTick: axisTick,
axisLabel: axisLabel,
axisLine: axisLine,
},
grid: grid,
dataZoom: dataZoom(0),
yAxis: [
{
type: "value",
name: "Rewards (USD)",
nameLocation: "middle",
nameGap: 30,
nameTextStyle: textStyleMain,
position: "left",
alignTicks: true,
axisTick: axisTick,
axisLine: axisLine,
splitLine: {
show: false,
},
axisLabel: {
...axisLabel,
formatter(value, index) {
return nFormatter(value, 0);
},
},
},
{
type: "value",
name: "Subsidy (BTC)",
nameLocation: "middle",
nameGap: 20,
nameTextStyle: {
fontSize: 12 * fontScale,
color: textColor,
},
axisTick: axisTick,
position: "right",
alignTicks: true,
axisLine: axisLine,
splitLine: {
show: false,
},
axisLabel: {
fontSize: 12 * fontScale,
color: textColor,
},
},
],
series: [
{
type: "line",
name: "Subsidy (USD)",
stack: "Total",
areaStyle: {},
symbol: "none",
lineStyle: {
width: 0,
},
data: dataArr.map((row) => row.subsidy_usd),
},
{
type: "line",
name: "Fees (USD)",
stack: "Total",
areaStyle: {},
symbol: "none",
lineStyle: {
width: 0,
},
data: dataArr.map((row) => row.totalfee_usd),
},
{
type: "line",
name: "Total (USD)",
symbol: "none",
lineStyle: {
width: 1,
color: textColor,
},
data: dataArr.map((row) => row.total_reward_usd),
},
{
type: "line",
yAxisIndex: 1,
name: "Block Subsidy (BTC)",
symbol: "none",
lineStyle: {
width: 3,
},
data: dataArr.map((row) => row.block_subsidy),
},
],
};
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);
});
});