Compare commits
No commits in common. "master" and "new-backend" have entirely different histories.
master
...
new-backen
45 changed files with 1410 additions and 1410 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
5
backend/requirements.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
flask
|
||||
flask_cors
|
||||
orjson
|
||||
gunicorn
|
||||
|
|
@ -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.
|
|
@ -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" >}}
|
|
@ -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)
|
28
content/data-lab/global-business-growth.md
Normal file
28
content/data-lab/global-business-growth.md
Normal 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/)
|
||||
|
||||
|
|
@ -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" >}}
|
||||
|
|
|
@ -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" >}}
|
||||
|
|
|
@ -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" >}}
|
|
@ -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 >}}
|
||||
|
|
|
@ -3,7 +3,7 @@ languageCode = 'en-gb'
|
|||
title = 'Based Data'
|
||||
|
||||
[params]
|
||||
apiURL = 'https://api.baseddata.io'
|
||||
apiURL = 'http://localhost:8000'
|
||||
|
||||
[markup.highlight]
|
||||
pygmentsUseClasses = false
|
||||
|
|
|
@ -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 }}
|
||||
|
|
96
layouts/partials/chart.html
Normal file
96
layouts/partials/chart.html
Normal 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>
|
|
@ -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
142
layouts/partials/table.html
Normal 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>
|
19
layouts/shortcodes/bitcoin-price.html
Normal file
19
layouts/shortcodes/bitcoin-price.html
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }}
|
|
@ -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>
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
109
static/js/bitcoin-business-growth-chart.js
Normal file
109
static/js/bitcoin-business-growth-chart.js
Normal 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);
|
||||
});
|
||||
});
|
95
static/js/bitcoin-business-growth-table.js
Normal file
95
static/js/bitcoin-business-growth-table.js
Normal 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();
|
||||
});
|
||||
});
|
84
static/js/bitcoin-price.js
Normal file
84
static/js/bitcoin-price.js
Normal 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
139
static/js/chart-params.js
Normal 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();
|
||||
}
|
||||
});
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
|
|
@ -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",
|
||||
// });
|
||||
// });
|
||||
//
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
|
127
static/js/feerate-percentile.js
Normal file
127
static/js/feerate-percentile.js
Normal 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
127
static/js/hashrate.js
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
});
|
45
static/js/lib/echarts.min.js
vendored
45
static/js/lib/echarts.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -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
17
static/js/mangrove-map.js
Normal 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
169
static/js/miner-rewards.js
Normal 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);
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue