mirror of
https://github.com/Labelbase/Labelbase.git
synced 2026-08-13 12:33:23 +02:00
.
This commit is contained in:
parent
f18bce10b0
commit
e9cd749151
7 changed files with 145 additions and 57 deletions
|
|
@ -564,7 +564,7 @@ class TreeMapsView(ListView):
|
|||
context['action'] = self.kwargs.get('action', 'unspent-outputs')
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
def get_queryset_OLD(self):
|
||||
#action = self.kwargs.get('action', 'unspent-outputs')
|
||||
label_ids = []
|
||||
|
||||
|
|
@ -596,6 +596,38 @@ class TreeMapsView(ListView):
|
|||
qs = Label.objects.none()
|
||||
return qs.order_by("id")
|
||||
|
||||
def get_queryset(self):
|
||||
label_ids = []
|
||||
|
||||
qs = Label.objects.filter(
|
||||
labelbase__user_id=self.request.user.id,
|
||||
labelbase_id=self.kwargs["pk"],
|
||||
)
|
||||
|
||||
action = self.kwargs.get('action', 'unspent-outputs')
|
||||
|
||||
for l in qs:
|
||||
if l.type == "output":
|
||||
output = OutputStat.objects.filter(
|
||||
user=l.labelbase.user,
|
||||
type_ref_hash=l.type_ref_hash,
|
||||
network=l.labelbase.network).last()
|
||||
if output and output.spent is False:
|
||||
# For fee-efficiency, only show spendable outputs
|
||||
if action == 'fee-efficiency':
|
||||
if l.spendable is True:
|
||||
label_ids.append(l.id)
|
||||
else:
|
||||
label_ids.append(l.id)
|
||||
|
||||
if label_ids:
|
||||
qs = qs.filter(id__in=label_ids,
|
||||
labelbase__user_id=self.request.user.id,
|
||||
labelbase_id=self.kwargs["pk"])
|
||||
else:
|
||||
qs = Label.objects.none()
|
||||
return qs.order_by("id")
|
||||
|
||||
|
||||
class LabelbasePortfolioView(LabelbaseView):
|
||||
template_name = "labelbase_portfolio.html"
|
||||
|
|
|
|||
|
|
@ -185,12 +185,12 @@
|
|||
{% endwith %}
|
||||
<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_fix_and_manage" labelbase_id=labelbase.id %}"><span data-feather="git-merge" class="align-text-bottom"></span> Fix & Manage</a></li>
|
||||
<li><a class="nav-link" style="font-size: .875rem;" href="{% url "fill_missing_data" labelbase_id=labelbase.id %}"><span data-feather="refresh-cw" class="align-text-bottom"></span> Sync Fields</a></li>
|
||||
{% comment %}<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_health" labelbase_id=labelbase.id %}"><span data-feather="activity" class="align-text-bottom"></span>UTXOs Health</a></li>{% endcomment %}
|
||||
<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_tree_maps" pk=labelbase.id %}unspent-outputs/"><span data-feather="grid" class="align-text-bottom"></span> Tree Map</a></li>
|
||||
{% comment %}<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_stats_and_kpi" labelbase_id=labelbase.id %}"><span data-feather="pie-chart" class="align-text-bottom"></span> Stats & KPIs</a></li>{% endcomment %}
|
||||
{% if request.user.profile.use_fiatfinances %}
|
||||
<li><a class="nav-link" style="font-size: .875rem;" href="{% url "currency_sync" labelbase_id=labelbase.id %}"><span data-feather="dollar-sign" class="align-text-bottom"></span> Fiat Finances</a></li>
|
||||
{% endif %}
|
||||
{% comment %}<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_health" labelbase_id=labelbase.id %}"><span data-feather="activity" class="align-text-bottom"></span>UTXOs Health</a></li>{% endcomment %}
|
||||
<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_tree_maps" pk=labelbase.id %}unspent-outputs/"><span data-feather="grid" class="align-text-bottom"></span> Tree Map</a></li>
|
||||
{% comment %}<li><a class="nav-link" style="font-size: .875rem;" href="{% url "labelbase_stats_and_kpi" labelbase_id=labelbase.id %}"><span data-feather="pie-chart" class="align-text-bottom"></span> Stats & KPIs</a></li>{% endcomment %}
|
||||
<li><a class="nav-link" style="font-size: .875rem;" data-bs-toggle="modal" data-bs-target="#importLabelbaseModal"><span data-feather="upload-cloud" class="align-text-bottom"></span> Import</a></li>
|
||||
<li><a class="nav-link" style="font-size: .875rem;" data-bs-toggle="modal" data-bs-target="#exportLabelbaseModal"><span data-feather="download-cloud" class="align-text-bottom"></span> Export</a></li>
|
||||
{% if labelbase.user.profile.use_hashtags %}
|
||||
|
|
|
|||
|
|
@ -339,7 +339,12 @@
|
|||
<p>Instantly visualize the cost-effectiveness of spending your UTXOs. Labelbase calculates and displays fee health status for each spendable output, color-coded to show if transaction fees would consume a healthy percentage of the output value.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col d-flex align-items-start">
|
||||
<div>
|
||||
<h3 class="fw-bold mb-0 fs-4 text-body-emphasis">UTXO Fee Efficiency Visualization</h3>
|
||||
<p>Visualize the cost-effectiveness of spending your UTXOs with color-coded treemaps. See at a glance which outputs have healthy fee-to-value ratios and which would be expensive to spend, helping you make informed decisions about consolidation and transaction planning.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col d-flex align-items-start">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@
|
|||
<li class="nav-item">
|
||||
<a class="nav-link {% if action == "unspent-spendable-outputs" %}active" aria-current="page" {% else %}"{% endif %} href="{% url "labelbase_tree_maps" pk=labelbase.id %}unspent-spendable-outputs/">Unspent Spendable Outputs</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link {% if action == 'fee-efficiency' %}active{% endif %}"
|
||||
href="{% url 'labelbase_tree_maps' labelbase.id 'fee-efficiency' %}">
|
||||
Fee Efficiency (VTER)
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,14 +11,26 @@
|
|||
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.3"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-chart-treemap@0.2.3"></script>
|
||||
|
||||
|
||||
{% if labelbase.is_testnet %}
|
||||
{% if action == "fee-efficiency" %}
|
||||
<div class="alert alert-dismissible bd-callout bd-callout-info">
|
||||
<button type="button" class="btn-close" style="position: absolute; top: -8px; right: -4px; padding: 1.25rem 1rem;" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<div style="padding-right: 1.8rem;">
|
||||
<strong>Fee Efficiency (VTER):</strong> Color shows how efficient it is to spend each output based on the fee-to-value ratio.
|
||||
<br><br>
|
||||
🟢 <strong>Healthy:</strong> Fee < {{ request.user.profile.my_fee_threshold_healthy }}% of value<br>
|
||||
🟡 <strong>Warning:</strong> Fee {{ request.user.profile.my_fee_threshold_healthy }}-{{ request.user.profile.my_fee_threshold_warning }}% of value<br>
|
||||
🔴 <strong>High:</strong> Fee > {{ request.user.profile.my_fee_threshold_warning }}% of value
|
||||
<br><br>
|
||||
<small>Box size represents output value in sats. Thresholds based on your fee settings. <a href="{% url 'userprofile_fees' %}">Adjust in Profile → Fees</a></small>
|
||||
</div>
|
||||
</div>
|
||||
{% elif labelbase.is_testnet %}
|
||||
<div class="alert alert-dismissible bd-callout bd-callout-info">
|
||||
<button type="button" class="btn-close" style="position: absolute; top: -8px; right: -4px; padding: 1.25rem 1rem;" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<div style="padding-right: 1.8rem;">
|
||||
<strong>Important Notice for Testnet Transactions:</strong> Testnet coins hold no real-world value and are solely intended for testing purposes.
|
||||
</div>
|
||||
</div
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-dismissible bd-callout bd-callout-info ">
|
||||
<button type="button" class="btn-close" style="position: absolute; top: -8px; right: -4px; padding: 1.25rem 1rem;" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
|
|
@ -28,6 +40,7 @@ If the UTXO value is not provided, Labelbase will estimate it based on historica
|
|||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row ">
|
||||
|
||||
<style>
|
||||
|
|
@ -40,8 +53,6 @@ If the UTXO value is not provided, Labelbase will estimate it based on historica
|
|||
<canvas id="chart-area" style="height: 56vh;"></canvas>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
function colorFromValue(value, index, border) {
|
||||
|
|
@ -53,7 +64,6 @@ function colorFromValue(value, index, border) {
|
|||
} else {
|
||||
var color = "#F7931A";
|
||||
}
|
||||
//var color = "orange";
|
||||
if (border) {
|
||||
alpha += 0.01;
|
||||
}
|
||||
|
|
@ -62,15 +72,11 @@ function colorFromValue(value, index, border) {
|
|||
.rgbString();
|
||||
}
|
||||
|
||||
|
||||
function red_or_greem_fromValue(value, index, border) {
|
||||
var alpha = (1 + Math.log(value)) / 5;
|
||||
var color;
|
||||
|
||||
value = window.records[index].performance;
|
||||
/* if (window.records[index].spendable == "false") {
|
||||
color = "#dee2e6";
|
||||
} else */
|
||||
if (value > 0) {
|
||||
color = "rgba(46, 204, 113, 1)"; // muted green color for positive values
|
||||
} else if (value < 0) {
|
||||
|
|
@ -88,12 +94,39 @@ function red_or_greem_fromValue(value, index, border) {
|
|||
.rgbString();
|
||||
}
|
||||
|
||||
window.records = [
|
||||
function healthColorFromValue(value, index, border) {
|
||||
var health_status = window.records[index].health_status;
|
||||
var alpha = (1 + Math.log(value)) / 5;
|
||||
var color;
|
||||
|
||||
switch(health_status) {
|
||||
case 'green':
|
||||
color = "rgba(46, 204, 113, 1)"; // Green
|
||||
break;
|
||||
case 'yellow':
|
||||
color = "rgba(241, 196, 15, 1)"; // Yellow
|
||||
break;
|
||||
case 'red':
|
||||
color = "rgba(231, 76, 60, 1)"; // Red
|
||||
break;
|
||||
default:
|
||||
color = "#dee2e6"; // Gray - no health data
|
||||
}
|
||||
|
||||
if (border) {
|
||||
alpha += 0.01;
|
||||
}
|
||||
|
||||
return Chart.helpers.color(color)
|
||||
.alpha(alpha)
|
||||
.rgbString();
|
||||
}
|
||||
|
||||
window.records = [
|
||||
{% for label in label_list %}
|
||||
{% if action == "unspent-spendable-outputs" and label.spendable or action == "unspent-outputs" %}
|
||||
{% if action == "unspent-spendable-outputs" and label.spendable or action == "unspent-outputs" or action == "fee-efficiency" %}
|
||||
{
|
||||
'spendable': {% if label.spendable is None %}'Undefined'{% else %}'{{ label.spendable }}'{% endif %},
|
||||
'spendable': {% if label.spendable is None %}'Undefined'{% else %}'{{ label.spendable }}'{% endif %},
|
||||
'spent': '{{ label.get_finance_output_metrics_dict.spent }}',
|
||||
'ref': '{{ label.ref }}',
|
||||
'label': '{{ label.label }}',
|
||||
|
|
@ -105,23 +138,24 @@ window.records = [
|
|||
'type_ref_hash': '{{ label.get_finance_output_metrics_dict.type_ref_hash }}',
|
||||
'performance': {{label.get_finance_output_metrics_dict.performance|floatformat:"2" }},
|
||||
'current_price': {{label.get_finance_output_metrics_dict.current_price|floatformat:"2" }},
|
||||
'fiat_value_old': {{label.get_finance_output_metrics_dict.fiat_value_old|floatformat:"2" }}, // historical or tracked value
|
||||
'is_tracked': {{label.get_finance_output_metrics_dict.is_tracked|lower }} // tracked value
|
||||
|
||||
'fiat_value_old': {{label.get_finance_output_metrics_dict.fiat_value_old|floatformat:"2" }},
|
||||
'is_tracked': {{label.get_finance_output_metrics_dict.is_tracked|lower }},
|
||||
{% if action == "fee-efficiency" %}
|
||||
'health_status': '{{ label.get_fee_health_status.status }}',
|
||||
'health_display': '{{ label.get_fee_health_status_display }}',
|
||||
'health_fee_sats': {{ label.get_fee_health_status.fee_sats|default:"null" }},
|
||||
'health_fee_percentage': {{ label.get_fee_health_status.fee_percentage|default:"null" }},
|
||||
{% endif %}
|
||||
},
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
|
||||
// Sorting the array by the 'sats' property in descending order
|
||||
window.records.sort(function(a, b) {
|
||||
return b.sats - a.sats;
|
||||
});
|
||||
|
||||
|
||||
|
||||
var ctx = document.getElementById("chart-area").getContext("2d");
|
||||
|
||||
window.chart1 = new Chart(ctx, {
|
||||
|
|
@ -133,10 +167,18 @@ data: {
|
|||
data: window.records,
|
||||
key: 'sats',
|
||||
backgroundColor: function(ctx) {
|
||||
return red_or_greem_fromValue(ctx.dataset.data[ctx.dataIndex].v, ctx.dataIndex);
|
||||
{% if action == "fee-efficiency" %}
|
||||
return healthColorFromValue(ctx.dataset.data[ctx.dataIndex].v, ctx.dataIndex);
|
||||
{% else %}
|
||||
return red_or_greem_fromValue(ctx.dataset.data[ctx.dataIndex].v, ctx.dataIndex);
|
||||
{% endif %}
|
||||
},
|
||||
borderColor: function(ctx) {
|
||||
return red_or_greem_fromValue(ctx.dataset.data[ctx.dataIndex].v, ctx.dataIndex, true);
|
||||
{% if action == "fee-efficiency" %}
|
||||
return healthColorFromValue(ctx.dataset.data[ctx.dataIndex].v, ctx.dataIndex, true);
|
||||
{% else %}
|
||||
return red_or_greem_fromValue(ctx.dataset.data[ctx.dataIndex].v, ctx.dataIndex, true);
|
||||
{% endif %}
|
||||
},
|
||||
spacing: 1,
|
||||
borderWidth: 0,
|
||||
|
|
@ -148,7 +190,7 @@ options: {
|
|||
maintainAspectRatio: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Unspent {% if action == "unspent-spendable-outputs" %}Spendable {% endif %}Outputs"
|
||||
text: "{% if action == 'fee-efficiency' %}Fee Efficiency (VTER){% elif action == 'unspent-spendable-outputs' %}Unspent Spendable Outputs{% else %}Unspent Outputs{% endif %}"
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
|
|
@ -160,8 +202,6 @@ options: {
|
|||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
document.getElementById("chart-area").onclick = function(evt) {
|
||||
var activePoints = window.chart1.getElementsAtEventForMode(evt, 'nearest', { intersect: true }, true);
|
||||
if (activePoints[0]) {
|
||||
|
|
@ -169,28 +209,26 @@ document.getElementById("chart-area").onclick = function(evt) {
|
|||
}
|
||||
};
|
||||
|
||||
// Track the current popover index
|
||||
window.curr_popover_index = -1;
|
||||
// Track the current popover index
|
||||
window.curr_popover_index = -1;
|
||||
|
||||
// Define popover content
|
||||
function getPopoverContent(index) {
|
||||
if (index >= 0 && index < window.records.length) {
|
||||
|
||||
/*
|
||||
var is_spent = "no";
|
||||
if (window.records[index].spent) {
|
||||
is_spent = "yes";
|
||||
}
|
||||
|
||||
var is_spendable = "no";
|
||||
if (window.records[index].spendable) {
|
||||
is_spendable = "yes";
|
||||
}
|
||||
|
||||
*/
|
||||
var content = '<div class="popover-content">';
|
||||
content += '<div><strong>Label:</strong> ' + window.records[index].label + '</div>';
|
||||
content += '<div><strong>Output:</strong> ' + window.records[index].ref + '</div>';
|
||||
|
||||
{% if action == "fee-efficiency" %}
|
||||
// Add health information for fee-efficiency view
|
||||
if (window.records[index].health_status) {
|
||||
content += '<div><strong>Fee Health:</strong> ' + window.records[index].health_display + '</div>';
|
||||
if (window.records[index].health_fee_sats !== null) {
|
||||
content += '<div><strong>Estimated Fee:</strong> ' + window.records[index].health_fee_sats + ' sats</div>';
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
content += '<div><strong>Confirmed in block:</strong> ' + window.records[index].confirmed_at_block_height + '</div>';
|
||||
if (window.records[index].is_tracked) {
|
||||
content += '<div><strong>Tracked value:</strong> ' + window.records[index].fiat_cur + ' ' + window.records[index].fiat_value_old + '</div>';
|
||||
|
|
@ -238,16 +276,13 @@ document.body.onmousemove = function (evt) {
|
|||
var topPosition = evt.pageY - popoverHeight / 2;
|
||||
var leftPosition;
|
||||
if (evt.pageX <= element.getBoundingClientRect().left + element.offsetWidth / 2) {
|
||||
// Inside the left half of chart-area, keep popover on the right side of the evt.pageX
|
||||
leftPosition = evt.pageX + 10;
|
||||
} else {
|
||||
// Inside the right half of chart-area, keep popover on the left side of the evt.pageX (posx - popoverWidth = new pos x)
|
||||
leftPosition = evt.pageX - popoverWidth - 10;
|
||||
}
|
||||
leftPosition = Math.min(leftPosition, window.innerWidth - popoverWidth - 10); // Ensure the popover doesn't exceed the right screen border
|
||||
leftPosition = Math.max(leftPosition, 10); // Ensure the popover doesn't exceed the left screen border
|
||||
leftPosition = Math.min(leftPosition, window.innerWidth - popoverWidth - 10);
|
||||
leftPosition = Math.max(leftPosition, 10);
|
||||
|
||||
// Ensure that the top position is within the viewport
|
||||
if (topPosition < 0) {
|
||||
topPosition = 0;
|
||||
} else if (topPosition + popoverHeight > window.innerHeight) {
|
||||
|
|
@ -279,13 +314,10 @@ document.getElementById("chart-area").onmouseout = function (evt) {
|
|||
|
||||
</div>
|
||||
|
||||
|
||||
{% else %}
|
||||
NO LABELBASE
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
{% addtoblock "js" %}
|
||||
$(document).ready(function () {
|
||||
|
|
|
|||
|
|
@ -15,9 +15,11 @@
|
|||
Define the fee for new transactions. This will be used to evaluate the health of your unspent transaction outputs.
|
||||
|
||||
<br><br>
|
||||
🟢 Healthy fee: < 1% (fee/amount < 0.01)<br>
|
||||
🟡 Warning: 1-3%<br>
|
||||
🔴 High: > 3%<br>
|
||||
<!--(fee/amount < 0.01)<br>-->
|
||||
|
||||
🟢 Healthy fee: < {{ request.user.profile.my_fee_threshold_healthy }}%
|
||||
🟡 Warning: {{ request.user.profile.my_fee_threshold_healthy }}-{{ user.profile.my_fee_threshold_warning }}%
|
||||
🔴 High: > {{ request.user.profile.my_fee_threshold_warning }}%
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -50,9 +50,20 @@ class Profile(models.Model):
|
|||
default='USD')
|
||||
|
||||
my_fee = models.IntegerField(default=1, help_text="Fee used when broadcasting transactions")
|
||||
my_fee_rate_threshold = models.IntegerField(default=1, help_text="Overwrite healthy fee")
|
||||
my_fee_rate_threshold = models.IntegerField(default=1, help_text="Adjust fee health thresholds (percentage points). Default: 0 = no adjustment, +1 = more lenient, -1 = more strict")
|
||||
|
||||
has_seen_welcome_popup = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def my_fee_threshold_healthy(self):
|
||||
"""Calculate healthy threshold with user adjustment"""
|
||||
return 1.0 + self.my_fee_rate_threshold
|
||||
|
||||
@property
|
||||
def my_fee_threshold_warning(self):
|
||||
"""Calculate warning threshold with user adjustment"""
|
||||
return 3.0 + self.my_fee_rate_threshold
|
||||
|
||||
def labelbases(self):
|
||||
return Labelbase.objects.filter(user_id=self.user_id)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue