Example - Dynamic Resizable Quoteboard With Live/Test Data

Hi, I’m back! Here’s an example of a quoteboard heavily modified from the example posted by AnneMarieW here:

Quote Cards - Card Group

Here’s the code:

## TallTim's Quoteboard v0.04
## Styled quoteboard based on the application card example by AnneMarieW from here ("Card Group"):
## https://dash-building-blocks.com/app_cards

import dash
from dash import Dash, dcc, html, Input, Output
import dash_bootstrap_components as dbc
import requests
import dash_mantine_components as dmc
import pandas as pd
# If you want to work with the original example code you'll need to enable the 'SUPERHERO' stylesheet here...even though I'm not
# using the icons, I left it in as a convenience...
app = Dash(__name__, external_stylesheets=[dbc.themes.CYBORG, dbc.icons.BOOTSTRAP])

#################### This is a flag you can set for testing so you don't ping the site API ####################
#testFlag = "False" # Uses live data from API URL request
testFlag = "True" # Uses test data, no URL requests
###############################################################################################################

# Assorted Variables
# Define coins to make quoteblocks from, this is asset_id column in the df.Quotes dataframe
# This will change if you change data sources
coins = ["bitcoin", "ethereum", "binancecoin", "ripple", "usd-coin", "tether", "bnb", "usdc", "staked-ether", "dogecoin", "solana", "cardano", "tron", "matic-network"]
interval = 6000 # update frequency - 1000 = 1 second, so 6 seconds here
# Access API 
api_url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd"
# Colors For Card Backgrounds/Borders/Text
text_decline='#C70000'# Red
text_rally='#00C700' # Green
text_unchanged='#A8A8A8' # Off-white
gradient_decline = 'linear-gradient(0deg,#870900,#000000 95%)'
gradient_border_decline = '#940000'
gradient_rally = 'linear-gradient(0deg,#008709,#000000 95%)'
gradient_border_rally = '#009400'
gradient_unchanged = 'linear-gradient(0deg,#A8A8A8,#000000 95%)'
gradient_border_unchanged = '#949494'

change = 0
price = 0
icon = ""
color = ""
interval = dcc.Interval(interval=interval)
cards = html.Div()
df_Quotes = pd.DataFrame() # Init empty dataframe

# Used in assigning cards to quoteblock vars -- must match length of 'coins' list above...
quoteBlockVarNames = ['quoteBlock_1', 'quoteBlock_2', 'quoteBlock_3', 'quoteBlock_4', 'quoteBlock_5', 'quoteBlock_6', 'quoteBlock_7', 'quoteBlock_8', 'quoteBlock_9', 'quoteBlock_10', 'quoteBlock_11', 'quoteBlock_12']

# Debug -- snapshot of data served by CoinGecko so I can test style/layout without pinging the site...
# Testing volume sorting - edited volume so the sorted order should be:
# Bitcoin - 1000, Ethereum - 900, Ripple - 800, USD-Coin - 700, Doge - 650, Tether - 600, BinanceCoin - 500, Staked-Ether - 400, Solana - 300, Matic - 200, Tron - 10, Cardano - 5

scraped_data=[{'id': 'bitcoin', 'symbol': 'btc', 'name': 'Bitcoin', 'image': 'https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579', 'current_price': 29346, 'market_cap': 571125444188, 'market_cap_rank': 1, 'fully_diluted_valuation': 616394102148, 'total_volume': 1000, 'high_24h': 29653, 'low_24h': 29263, 'price_change_24h': -51.54874027145706, 'price_change_percentage_24h': 5.25, 'market_cap_change_24h': -896753676.9368896, 'market_cap_change_percentage_24h': -0.15677, 'circulating_supply': 19457737.0, 'total_supply': 21000000.0, 'max_supply': 21000000.0, 'ath': 69045, 'ath_change_percentage': -57.50061, 'ath_date': '2021-11-10T14:24:11.849Z', 'atl': 67.81, 'atl_change_percentage': 43173.91396, 'atl_date': '2013-07-06T00:00:00.000Z', 'roi': None, 'last_updated': '2023-08-15T05:38:38.522Z'}, {'id': 'ethereum', 'symbol': 'eth', 'name': 'Ethereum', 'image': 'https://assets.coingecko.com/coins/images/279/large/ethereum.png?1595348880', 'current_price': 1840.48, 'market_cap': 221180610887, 'market_cap_rank': 2, 'fully_diluted_valuation': 221180610887, 'total_volume': 900, 'high_24h': 1854.61, 'low_24h': 1836.08, 'price_change_24h': -5.970458272938913, 'price_change_percentage_24h': -0.32335, 'market_cap_change_24h': -599404386.0502014, 'market_cap_change_percentage_24h': -0.27027, 'circulating_supply': 120142222.589507, 'total_supply': 120142222.589507, 'max_supply': None, 'ath': 4878.26, 'ath_change_percentage': -62.27958, 'ath_date': '2021-11-10T14:24:19.604Z', 'atl': 0.432979, 'atl_change_percentage': 424886.14404, 'atl_date': '2015-10-20T00:00:00.000Z', 'roi': {'times': 82.83960831086297, 'currency': 'btc', 'percentage': 8283.960831086297}, 'last_updated': '2023-08-15T05:38:39.869Z'}, {'id': 'tether', 'symbol': 'usdt', 'name': 'Tether', 'image': 'https://assets.coingecko.com/coins/images/325/large/Tether.png?1668148663', 'current_price': 0.998601, 'market_cap': 83275370679, 'market_cap_rank': 3, 'fully_diluted_valuation': 83275370679, 'total_volume': 600, 'high_24h': 1.001, 'low_24h': 0.995176, 'price_change_24h': 0, 'price_change_percentage_24h': 0, 'market_cap_change_24h': -21912507.411972046, 'market_cap_change_percentage_24h': -0.02631, 'circulating_supply': 83410866925.5156, 'total_supply': 83410866925.5156, 'max_supply': None, 'ath': 1.32, 'ath_change_percentage': -24.52252, 'ath_date': '2018-07-24T00:00:00.000Z', 'atl': 0.572521, 'atl_change_percentage': 74.42837, 'atl_date': '2015-03-02T00:00:00.000Z', 'roi': None, 'last_updated': '2023-08-15T05:35:00.921Z'}, {'id': 'binancecoin', 'symbol': 'bnb', 'name': 'BNB', 'image': 'https://assets.coingecko.com/coins/images/825/large/bnb-icon2_2x.png?1644979850', 'current_price': 239.27, 'market_cap': 36813979487, 'market_cap_rank': 4, 'fully_diluted_valuation': 47855063951, 'total_volume': 500, 'high_24h': 241.0, 'low_24h': 239.13, 'price_change_24h': -1.3132428836969154, 'price_change_percentage_24h': -0.54587, 'market_cap_change_24h': -216913346.59331512, 'market_cap_change_percentage_24h': -0.58576, 'circulating_supply': 153856150.0, 'total_supply': 153856150.0, 'max_supply': 200000000.0, 'ath': 686.31, 'ath_change_percentage': -65.13848, 'ath_date': '2021-05-10T07:24:17.097Z', 'atl': 0.0398177, 'atl_change_percentage': 600780.88219, 'atl_date': '2017-10-19T00:00:00.000Z', 'roi': None, 'last_updated': '2023-08-15T05:38:39.120Z'}, {'id': 'ripple', 'symbol': 'xrp', 'name': 'XRP', 'image': 'https://assets.coingecko.com/coins/images/44/large/xrp-symbol-white-128.png?1605778731', 'current_price': 0.628703, 'market_cap': 33176786260, 'market_cap_rank': 5, 'fully_diluted_valuation': 62849958286, 'total_volume': 800, 'high_24h': 0.635508, 'low_24h': 0.625689, 'price_change_24h': -0.003263613505237073, 'price_change_percentage_24h': -0.51642, 'market_cap_change_24h': -213536726.86206436, 'market_cap_change_percentage_24h': -0.63952, 'circulating_supply': 52787284454.0, 'total_supply': 99988519823.0, 'max_supply': 100000000000.0, 'ath': 3.4, 'ath_change_percentage': -81.52247, 'ath_date': '2018-01-07T00:00:00.000Z', 'atl': 0.00268621, 'atl_change_percentage': 23276.81696, 'atl_date': '2014-05-22T00:00:00.000Z', 'roi': None, 'last_updated': '2023-08-15T05:38:40.312Z'}, {'id': 'usd-coin', 'symbol': 'usdc', 'name': 'USD Coin', 'image': 'https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png?1547042389', 'current_price': 1.0, 'market_cap': 26002210526, 'market_cap_rank': 6, 'fully_diluted_valuation': 25995337308, 'total_volume': 700, 'high_24h': 1.001, 'low_24h': 0.995914, 'price_change_24h': 0.00024895, 'price_change_percentage_24h': 0.0249, 'market_cap_change_24h': -164825818.58258438, 'market_cap_change_percentage_24h': -0.6299, 'circulating_supply': 26002476366.8477, 'total_supply': 25995603078.5546, 'max_supply': None, 'ath': 1.17, 'ath_change_percentage': -14.76763, 'ath_date': '2019-05-08T00:40:28.300Z', 'atl': 0.877647, 'atl_change_percentage': 13.88698, 'atl_date': '2023-03-11T08:02:13.981Z', 'roi': None, 'last_updated': '2023-08-15T05:38:41.408Z'}, {'id': 'staked-ether', 'symbol': 'steth', 'name': 'Lido Staked Ether', 'image': 'https://assets.coingecko.com/coins/images/13442/large/steth_logo.png?1608607546', 'current_price': 1840.15, 'market_cap': 14926181150, 'market_cap_rank': 7, 'fully_diluted_valuation': 14922486712, 'total_volume': 400, 'high_24h': 1853.33, 'low_24h': 1837.27, 'price_change_24h': -5.9586862118578665, 'price_change_percentage_24h': -0.32277, 'market_cap_change_24h': -23891417.27374077, 'market_cap_change_percentage_24h': -0.15981, 'circulating_supply': 8109614.0695121, 'total_supply': 8109614.0695121, 'max_supply': 8107606.82716179, 'ath': 4829.57, 'ath_change_percentage': -61.9037, 'ath_date': '2021-11-10T14:40:47.256Z', 'atl': 482.9, 'atl_change_percentage': 281.01098, 'atl_date': '2020-12-22T04:08:21.854Z', 'roi': None, 'last_updated': '2023-08-15T05:38:41.375Z'}, {'id': 'dogecoin', 'symbol': 'doge', 'name': 'Dogecoin', 'image': 'https://assets.coingecko.com/coins/images/5/large/dogecoin.png?1547792256', 'current_price': 0.07442, 'market_cap': 10463841439, 'market_cap_rank': 8, 'fully_diluted_valuation': 10463755851, 'total_volume': 650, 'high_24h': 0.075371, 'low_24h': 0.073692, 'price_change_24h': -0.000596043719873193, 'price_change_percentage_24h': -0.79455, 'market_cap_change_24h': -93366893.82887459, 'market_cap_change_percentage_24h': -0.88439, 'circulating_supply': 140595866383.705, 'total_supply': 140594716383.705, 'max_supply': None, 'ath': 0.731578, 'ath_change_percentage': -89.83321, 'ath_date': '2021-05-08T05:08:23.458Z', 'atl': 8.69e-05, 'atl_change_percentage': 85486.66332, 'atl_date': '2015-05-06T00:00:00.000Z', 'roi': None, 'last_updated': '2023-08-15T05:38:46.559Z'}, {'id': 'solana', 'symbol': 'sol', 'name': 'Solana', 'image': 'https://assets.coingecko.com/coins/images/4128/large/solana.png?1640133422', 'current_price': 24.96, 'market_cap': 10142116320, 'market_cap_rank': 9, 'fully_diluted_valuation': 13859120315, 'total_volume': 300, 'high_24h': 25.24, 'low_24h': 24.36, 'price_change_24h': 0.530718, 'price_change_percentage_24h': 2.17233, 'market_cap_change_24h': 211395396, 'market_cap_change_percentage_24h': 2.1287, 'circulating_supply': 406100728.206927, 'total_supply': 554933376.28717, 'max_supply': None, 'ath': 259.96, 'ath_change_percentage': -90.4005, 'ath_date': '2021-11-06T21:54:35.825Z', 'atl': 0.500801, 'atl_change_percentage': 4882.97123, 'atl_date': '2020-05-11T19:35:23.449Z', 'roi': None, 'last_updated': '2023-08-15T05:38:39.674Z'}, {'id': 'cardano', 'symbol': 'ada', 'name': 'Cardano', 'image': 'https://assets.coingecko.com/coins/images/975/large/cardano.png?1547034860', 'current_price': 0.289028, 'market_cap': 10129225134, 'market_cap_rank': 10, 'fully_diluted_valuation': 13006558999, 'total_volume': 5, 'high_24h': 0.292078, 'low_24h': 0.287716, 'price_change_24h': -0.001666535922617174, 'price_change_percentage_24h': -0.57329, 'market_cap_change_24h': -58743586.493177414, 'market_cap_change_percentage_24h': -0.5766, 'circulating_supply': 35045020830.3234, 'total_supply': 45000000000.0, 'max_supply': 45000000000.0, 'ath': 3.09, 'ath_change_percentage': -90.64588, 'ath_date': '2021-09-02T06:00:10.474Z', 'atl': 0.01925275, 'atl_change_percentage': 1399.80376, 'atl_date': '2020-03-13T02:22:55.044Z', 'roi': None, 'last_updated': '2023-08-15T05:38:46.726Z'}, {'id': 'tron', 'symbol': 'trx', 'name': 'TRON', 'image': 'https://assets.coingecko.com/coins/images/1094/large/tron-logo.png?1547035066', 'current_price': 0.077272, 'market_cap': 6915242709, 'market_cap_rank': 11, 'fully_diluted_valuation': 6915291935, 'total_volume': 10, 'high_24h': 0.077575, 'low_24h': 0.077072, 'price_change_24h': 0.00011325, 'price_change_percentage_24h': 0.14678, 'market_cap_change_24h': 4577588, 'market_cap_change_percentage_24h': 0.06624, 'circulating_supply': 89471632948.6968, 'total_supply': 89472269852.3265, 'max_supply': None, 'ath': 0.231673, 'ath_change_percentage': -66.63921, 'ath_date': '2018-01-05T00:00:00.000Z', 'atl': 0.00180434, 'atl_change_percentage': 4183.43794, 'atl_date': '2017-11-12T00:00:00.000Z', 'roi': {'times': 39.66971290740434, 'currency': 'usd', 'percentage': 3966.9712907404337}, 'last_updated': '2023-08-15T05:38:39.511Z'}, {'id': 'matic-network', 'symbol': 'matic', 'name': 'Polygon', 'image': 'https://assets.coingecko.com/coins/images/4713/large/matic-token-icon.png?1624446912', 'current_price': 0.674663, 'market_cap': 6290066896, 'market_cap_rank': 12, 'fully_diluted_valuation': 6749383306, 'total_volume': 200, 'high_24h': 0.684453, 'low_24h': 0.674531, 'price_change_24h': -0.006132114034599567, 'price_change_percentage_24h': -0.90073, 'market_cap_change_24h': -48608944.01885128, 'market_cap_change_percentage_24h': -0.76686, 'circulating_supply': 9319469069.28493, 'total_supply': 10000000000.0, 'max_supply': 10000000000.0, 'ath': 2.92, 'ath_change_percentage': -76.86017, 'ath_date': '2021-12-27T02:08:34.307Z', 'atl': 0.00314376, 'atl_change_percentage': 21365.32317, 'atl_date': '2019-05-10T00:00:00.000Z', 'roi': {'times': 255.5260048380359, 'currency': 'usd', 'percentage': 25552.600483803588}, 'last_updated': '2023-08-15T05:38:39.642Z'}]

def get_data(): # Get data via API, pass exceptions to console if any generated
	try:
		response = requests.get(api_url, timeout=1)
		return response.json()
	except requests.exceptions.RequestException as e:
		print(e)

	return

def styleCard(myCoin, myPrice, myChange, myColor, myGradient, myGradientBorder):
	card_html = dbc.Card(
			html.Div(
				[
					html.H4( # Our asset name font size/style
						[
							myCoin # full_name of asset
						],
						style={'font-size':'160%', 'color':'white', 'text-align':'left', 'padding-left':'20px', 'padding-top':'20px'}
					),
					html.H4( # Price and Net Change format/style/padding
						[	# Either change the number of decimals by changing the round(number,places) statement or the quoteblock width down at the end	
							html.Span(f"${round(myPrice,5):,}", style={'font-size':'90%', 'color' : 'white', 'text-align' : 'left', 'padding-left' : '20px'}), 
							html.Span(f"{round(myChange,2):+}%", style={'font-size':'90%','color': myColor, 'text-align' : 'left', 'padding-left' : '10px', 'padding-top' : '5px'})
						]
					)
				],
				# Background gradient/colorstops of quoteblock/padding - transparent background color
				style={
					'background':'#00000000','background-image': myGradient,'border':'1px','solid': '#00000000','border-radius':'5px', 'border-style':'solid', 'color':myGradientBorder, 'padding-bottom' : '10px'
				}
			),
			# Quoteblock set width/height/padding/background - Also can be used to fit more decimals...
			# Note that color here for the background is transparent so it doesn't clash with the overall theme background
			style={'width':'270px', 'height':'130px', 'padding-right':'10px','background':'#00000000'}
		)
		
	return card_html
	
app.layout = dbc.Container([interval, cards], className="my-5", style={'background':'#00000000'}) # Same here, transparent background

def make_card(myName, myChange, myPrice): # passes full_name, net_change_percent, price
	if myChange < 0: # Decreased price?
		color = text_decline # Color of net change % +/-
		gradient_fill = gradient_decline
		gradient_border = gradient_border_decline
	elif myChange > 0: # Increased price?
		color = text_rally
		gradient_fill = gradient_rally
		gradient_border = gradient_border_rally
	else: 			# Rare, but no change in price?
		color = text_unchanged
		gradient_fill = gradient_unchanged
		gradient_border = gradient_border_unchanged

	return styleCard(myName, myPrice, myChange, color, gradient_fill, gradient_border)

# Columns we don't need from data -- feel free to change if you want to use these
dropList = ['image','market_cap','market_cap_rank','fully_diluted_valuation','high_24h','low_24h','price_change_24h','market_cap_change_24h','market_cap_change_percentage_24h','circulating_supply','total_supply','max_supply','ath','ath_change_percentage','ath_date','atl','atl_change_percentage','atl_date','roi','last_updated']

# Renamed columns - original:new name -- You can change these, but you need to alter their references in the rest of the code...
renamedPairsList = {'id':'asset_id','symbol':'ticker','name':'full_name','current_price':'price','total_volume':'volume','price_change_percentage_24h':'net_change_percent'}

@app.callback(Output(cards, "children"), Input(interval, "n_intervals"))
def update_cards(_):
	if testFlag == "True":
		coin_data = scraped_data # For debug so I don't poll the site a bazillion times
		
	if testFlag == "False":
		coin_data = get_data() # Live data
		
	if coin_data is None or type(coin_data) is dict: # Catch none type exception
		return dash.no_update
		
	## Process scraped data into pandas dataframe
	df_Quotes = pd.DataFrame.from_dict(coin_data, orient="columns")
	# Drop columns we don't need - helps when debugging
	df_Quotes = df_Quotes.drop(columns=dropList, axis=1)
	# Rename columns for ease of reference/debugging
	df_Quotes = df_Quotes.rename(columns=renamedPairsList)
	# Sort based on volume, descending order
	df_Quotes = df_Quotes.sort_values(by="volume", ascending=False)
	# Reindex dataframe for ease of reference
	df_Quotes = df_Quotes.reset_index(drop=True)
	
	# Debug
	# Print data types if needed
	#print(df_Quotes.info())
	# Print dataframe contents
	#print("Dataframe Contents: \n" + df_Quotes.to_string() + "\n")
	
	# Our list of cards with data fully styled
	coin_cards = []
	
	# Assign dataframe columns to variables
	coin_name = df_Quotes["full_name"]
	net_change = df_Quotes["net_change_percent"]
	price = df_Quotes["price"]
	
	# Using dataframes, iterate via index
	for idx, item in enumerate(coin_name):
		coin_cards.append(make_card(coin_name[idx], net_change[idx], price[idx])) # pass asset_id, net change, price
	
	# Don't like the padding on this one, not sure how to adjust - tried a lot of variants...(not shown, but trust me, I did)
	# Just leaving it here for demonstration sake...
	
	# make the card layout -- this works -- but the grid is spaced out more than I'd like...
	#card_layout = [
	#	dmc.Grid([
	#		dbc.Row([
	#					dmc.Col(card, md=1) for card in coin_cards
	#				])
	#	])
	#]

	# I prefer this layout, even though its a bit simplistic...
	# Assign variables values in loop
	for idx, var in enumerate(quoteBlockVarNames): # If you want more cards, just add to the list in the variable section up top
		globals()[var] = coin_cards[idx] # Assign our quoteblock variable names the proper coin_card[] using proper index

	# Simple nested structure with main Div and a Grid inside... 
	card_layout = [
		html.Div([
			html.H1(["Top Crypto Assets - By Volume"], style={'padding-left':'20px', 'font-size':'30px', 'font-weight':'bold'}),
			html.Br(),
			# Probably could use a loop to assign this as well, but honestly I'm tired and this is all the cards I want...
			dmc.Grid([
				quoteBlock_1, quoteBlock_2, quoteBlock_3, quoteBlock_4, quoteBlock_5, quoteBlock_6, quoteBlock_7, quoteBlock_8, quoteBlock_9, quoteBlock_10, quoteBlock_11, quoteBlock_12
			], align="center", justify="center")
		])
	]

	return card_layout

# Run the App
if __name__ == '__main__':
	app.run_server(debug=True, host='0.0.0.0',port=8100) # Set host argument to allow LAN connections, Port for custom ports

Here’s a pic of the quoteboard using the test data:

Here’s a pic of the quoteboard using live data:

Enjoy!

1 Like

Hi @TallTim

Thanks for sharing - those are great looking cards!

You might have missed one of my comments in a previous post - It is not recommended to updated global variables in a Dash callback. It can lead to unpredictable results.

For more information, please see the Dash documentation:

Why Global Variable WIll Break Your APP

2 Likes

Its cool, I’m sure I’m messing up right and left :laughing: I see what you mean – I’m lucky that my quoteboard and other stuff is only intended for one user, but your point is well taken.

Thanks!

Really glad someone is using the example site, package it up and showcase it for everyone :smiley:

3 Likes

I’m afraid this is as far as my skills take me, don’t know what the process is to package it, and I’d have to fix that globals thing that AnneMarieW mentioned. (She’s right, but I’m working my way slowly up the ladder of knowledge.)

just a simple zip file, I can handle the rest of it. The zip file is mainly so my S3 bucket doesn’t get destroyed with a ton of various files lol

OR

can toss it here too

Hey @TallTim your doing a great job, we all start at the bottom and work our way up.

I have had a play around with your code as an exercise in working on someone else’s code. I have fixed the globals issue I think. @AnnMarieW correct me if I am wrong. I believe the 3 I have left behind can stay there, but I am more than happy to be corrected/informed.

This is a rewrite of the code to get rid of unneccasary global variables and a chance to play with the new Python 3.10 match/case statement.
Along the way I changed for loops to list comprehensions and added a few comments to help me remember what I did.
I have added doc strings and error checking
I have also stripped out the test scrape data for conciceness. This version only works with live data from coingeko

Here is the resultant code

# Updated code for TallTim's Quoteboard v0.04. So I suppose this makes it v0.05?
# This is a rehash of the code to get rid of unneccasary global variables and a chance to play with the new
# Python 3.10 match/case statement.
# Along the way I changed for loops to list comprehensions and added a few comments to help me remember what I did.
# Added doc strings and error checking
# I have also stripped out the test scrape data for conciseness. 
# This version only works with live data from coingeko

# TallTim's Quoteboard v0.04
# Styled quoteboard based on the application card example by AnneMarieW from here ("Card Group"):
# https://dash-building-blocks.com/app_cards

import dash
from dash import Dash, dcc, html, Input, Output
import dash_bootstrap_components as dbc
import requests
import dash_mantine_components as dmc
import pandas as pd

app = Dash(__name__, external_stylesheets=[dbc.themes.CYBORG, dbc.icons.BOOTSTRAP])

# Set App title
app.title = "TallTim's Quoteboard"

# Assorted Variables
interval = 6000  # update frequency - 1000 = 1 second, so 6 seconds here
interval = dcc.Interval(interval=interval)
cards = html.Div()


def get_data():
    """
    Get data from the API.

    Returns:
        dict: The data retrieved from the API.
    """
    api_url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd"
    try:
        response = requests.get(api_url, timeout=1)
        return response.json()
    except requests.exceptions.RequestException as e:
        print(e)

    return


def style_card(my_coin, my_price, my_change, my_color, my_gradient, my_gradient_border):
    """
    Create a styled card with the given parameters.

    Args:
        my_coin (str): The name of the coin.
        my_price (float): The price of the coin.
        my_change (float): The net change percentage of the coin.
        my_color (str): The color of the text.
        my_gradient (str): The background gradient of the card.
        my_gradient_border (str): The border gradient of the card.

    Returns:
        dash.development.base_component.Component: The styled card.
    """
    card_html = dbc.Card(
        html.Div(
            [
                html.H4(
                    [my_coin],
                    style={
                        "font-size": "160%",
                        "color": "white",
                        "text-align": "left",
                        "padding-left": "20px",
                        "padding-top": "20px",
                    },
                ),
                html.H4(
                    [
                        html.Span(
                            f"${round(my_price, 5):,}",
                            style={
                                "font-size": "90%",
                                "color": "white",
                                "text-align": "left",
                                "padding-left": "20px",
                            },
                        ),
                        html.Span(
                            f"{round(my_change, 2):+}%",
                            style={
                                "font-size": "90%",
                                "color": my_color,
                                "text-align": "left",
                                "padding-left": "10px",
                                "padding-top": "5px",
                            },
                        ),
                    ]
                ),
            ],
            style={
                "background": "#00000000",
                "background-image": my_gradient,
                "border": "1px",
                "solid": "#00000000",
                "border-radius": "5px",
                "border-style": "solid",
                "color": my_gradient_border,
                "padding-bottom": "10px",
            },
        ),
        style={
            "width": "270px",
            "height": "130px",
            "padding-right": "10px",
            "background": "#00000000",
        },
    )

    return card_html


def make_card(coin_name, net_change, price):
    """
    Create a card by applying styles to the given coin data.

    Args:
        coin_name (str): The name of the coin.
        net_change (float): The net change percentage of the coin.
        price (float): The price of the coin.

    Returns:
        dash.development.base_component.Component: The styled card.
    """
    # Using list comprehension to create the Grid
    colors = {
        "text_decline": "#C70000",  # Red
        "text_rally": "#00C700",  # Green
        "text_unchanged": "#A8A8A8",  # Off-white
        "gradient_decline": "linear-gradient(0deg,#870900,#000000 95%)",
        "gradient_border_decline": "#940000",
        "gradient_rally": "linear-gradient(0deg,#008709,#000000 95%)",
        "gradient_border_rally": "#009400",
        "gradient_unchanged": "linear-gradient(0deg,#A8A8A8,#000000 95%)",
        "gradient_border_unchanged": "#949494",
    }

    # Define the cases and their corresponding values
    # Using a dictionary called switch_cases to define the different cases and their corresponding values.
    # The keys in the dictionary are the conditions, and the values are tuples containing the color values.
    # Use the get() method to retrieve the values based on the condition. If none of the conditions match, it falls back
    # to the default case, which represents no change in price.

    # This approach provides a more concise and readable way to handle multiple conditions, simulating a switch
    # statement in Python. (Python 3.10 onwards)
    switch_cases = {
        net_change
        < 0: (  # Decreased price?
            colors["text_decline"],
            colors["gradient_decline"],
            colors["gradient_border_decline"],
        ),
        net_change
        > 0: (  # Increased price?
            colors["text_rally"],
            colors["gradient_rally"],
            colors["gradient_border_rally"],
        ),
    }

    # Get the values based on the condition
    color, gradient_fill, gradient_border = switch_cases.get(
        True,
        (  # Rare, but no change in price?
            colors["text_unchanged"],
            colors["gradient_unchanged"],
            colors["gradient_border_unchanged"],
        ),
    )

    return style_card(
        coin_name, price, net_change, color, gradient_fill, gradient_border
    )


@app.callback(Output(cards, "children"), Input(interval, "n_intervals"))
def update_cards(_):
    """
    Update the cards based on the API data.

    Args:
        _: The number of intervals (not used).

    Returns:
        list: The layout of the updated cards.
    """
    coin_data = get_data()

    if coin_data is None or type(coin_data) is dict:
        return dash.no_update

    drop_list = [
        "image",
        "market_cap",
        "market_cap_rank",
        "fully_diluted_valuation",
        "high_24h",
        "low_24h",
        "price_change_24h",
        "market_cap_change_24h",
        "market_cap_change_percentage_24h",
        "circulating_supply",
        "total_supply",
        "max_supply",
        "ath",
        "ath_change_percentage",
        "ath_date",
        "atl",
        "atl_change_percentage",
        "atl_date",
        "roi",
        "last_updated",
    ]

    renamed_pairs_list = {
        "id": "asset_id",
        "symbol": "ticker",
        "name": "full_name",
        "current_price": "price",
        "total_volume": "volume",
        "price_change_percentage_24h": "net_change_percent",
    }
    try:
        # Process scraped data into pandas dataframe
        df_quotes = pd.DataFrame.from_dict(coin_data, orient="columns")

        # Drop columns we don't need - helps when debugging
        df_quotes = df_quotes.drop(columns=drop_list, axis=1)

        # Rename columns for ease of reference/debugging
        df_quotes = df_quotes.rename(columns=renamed_pairs_list)

        # Sort based on volume, descending order
        df_quotes = df_quotes.sort_values(by="volume", ascending=False)

        # Reindex dataframe for ease of reference
        df_quotes = df_quotes.reset_index(drop=True)

        coin_cards = [
            make_card(*row)
            for row in df_quotes[
                ["full_name", "net_change_percent", "price"]
            ].to_numpy()
        ]

        quote_block_var_names = [f"quoteBlock_{i}" for i in range(1, 13)]
        quote_block_vars = dict(zip(quote_block_var_names, coin_cards))

        card_layout = [
            html.Div(
                [
                    html.H1(
                        ["Top Crypto Assets - By Volume"],
                        style={
                            "padding-left": "20px",
                            "font-size": "30px",
                            "font-weight": "bold",
                        },
                    ),
                    html.Br(),
                    dmc.Grid(
                        [quote_block_vars[var] for var in quote_block_var_names],
                        align="center",
                        justify="center",
                    ),
                ]
            )
        ]

        return card_layout

    except Exception as e:
        print(f"Error occurred: {e}")
        return dash.no_update


app.layout = dbc.Container(
    [interval, cards], className="my-5", style={"background": "#00000000"}
)

if __name__ == "__main__":
    app.run_server(debug=True)
1 Like

Thanks for checking it out and going through my code with improvements! I’ll look at it to see how to implement things properly (not relying on global variables) so this really helps me learn.

I appreciate the effort.