- Scheduled Maintenance:{" "}
-
- {name}
- {" "}
- on{" "}
+
+ Scheduled maintenance:{" "}
+
{name}
+
{moment(scheduled_for).utc().format("dddd, MMMM Do YYYY, [at] h:mma")}{" "}
UTC (
{moment(scheduled_for).format(
@@ -32,12 +27,8 @@ export const ScheduledMaintenance = ({ scheduledMaintenances }) => {
: "MMMM Do YYYY, h:mma",
)}{" "}
local time)
-
- {updates.length > 0 ? (
- {sanitizeHtml(updates[0].body)}
- ) : null}
-
-
+ {updates.length > 0 ?
{sanitizeHtml(updates[0].body)}
: null}
+
);
};
diff --git a/frontend/components/TotalCoins.jsx b/frontend/components/TotalCoins.jsx
index 5ded3331..ffc7b3d7 100644
--- a/frontend/components/TotalCoins.jsx
+++ b/frontend/components/TotalCoins.jsx
@@ -24,14 +24,11 @@ export default class TotalCoins extends AmountWidget {
});
}
- renderName() {
- return (
-
- );
+ name() {
+ return "Total supply";
+ }
+
+ apiUrl() {
+ return "/api/v2/lumens/";
}
}
diff --git a/frontend/scss/_components.scss b/frontend/scss/_components.scss
index f341b689..4f7de34e 100644
--- a/frontend/scss/_components.scss
+++ b/frontend/scss/_components.scss
@@ -174,6 +174,25 @@
}
}
+.nodes-cta {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ color: var(--color-text-secondary);
+
+ a {
+ white-space: nowrap;
+ font-weight: 500;
+ }
+
+ @media (max-width: 600px) {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ }
+}
+
// ---------------------------------------------------------------------------
// Chips & legends
@@ -215,12 +234,12 @@
background: var(--chart-primary);
}
- &.secondary {
- background: var(--chart-secondary);
+ &.series-a {
+ background: var(--chart-series-a);
}
- &.tertiary {
- background: var(--chart-tertiary);
+ &.series-b {
+ background: var(--chart-series-b);
}
}
@@ -268,6 +287,112 @@
font-size: 12px;
}
+// ---------------------------------------------------------------------------
+// Operations stream
+
+.ops-card .card-body {
+ max-height: 460px;
+ overflow-y: auto;
+}
+
+.ops-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.ops-row {
+ padding: 10px 0;
+ border-top: 1px solid var(--color-border);
+ font-size: 13px;
+
+ &:first-child {
+ border-top: none;
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ .ops-row.fresh {
+ animation: ops-flash 1.2s ease-out both;
+ }
+}
+
+@keyframes ops-flash {
+ from {
+ background-color: rgba(253, 218, 36, 0.1);
+ }
+
+ to {
+ background-color: transparent;
+ }
+}
+
+.ops-row-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 4px;
+
+ a.chip {
+ color: var(--color-text-secondary);
+
+ &:hover {
+ color: var(--color-text);
+ border-color: var(--color-text-faint);
+ text-decoration: none;
+ }
+ }
+}
+
+.ops-time {
+ color: var(--color-text-faint);
+ font-size: 12px;
+ white-space: nowrap;
+ font-variant-numeric: tabular-nums;
+}
+
+.ops-row-body {
+ color: var(--color-text-secondary);
+ line-height: 1.6;
+ overflow-wrap: anywhere;
+
+ code {
+ font-family: var(--font-mono);
+ font-size: 12px;
+ color: var(--color-text);
+ }
+
+ // Account/asset links read as data, not as calls to action — keep them
+ // quiet so gold stays scarce.
+ a {
+ color: var(--color-text);
+
+ &:hover {
+ color: var(--color-link);
+ }
+ }
+}
+
+.account-tag {
+ display: inline-block;
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ padding: 1px 5px;
+ margin-left: 4px;
+ border-radius: var(--radius-chip);
+ background: var(--color-border);
+ color: var(--color-text-secondary);
+}
+
+.account img {
+ height: 12px;
+ margin-left: 4px;
+ vertical-align: -1px;
+}
+
// ---------------------------------------------------------------------------
// Banners (active incidents / maintenance)
@@ -396,6 +521,86 @@
}
}
+// ---------------------------------------------------------------------------
+// Incident history
+
+.incident-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ font-size: 13px;
+
+ li {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 7px 0;
+ border-top: 1px solid var(--color-border);
+
+ &:first-child {
+ border-top: none;
+ }
+
+ a {
+ color: var(--color-text);
+ min-width: 0;
+
+ &:hover {
+ color: var(--color-link);
+ }
+ }
+ }
+
+ .incident-time {
+ flex-shrink: 0;
+ color: var(--color-text-faint);
+ font-size: 12px;
+ white-space: nowrap;
+ }
+}
+
+.incidents-card .card-body {
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+// ---------------------------------------------------------------------------
+// Capacity meter (fee stats)
+
+.capacity-meter {
+ margin-bottom: 12px;
+}
+
+.capacity-meter-head {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-bottom: 6px;
+}
+
+.capacity-meter-track {
+ height: 4px;
+ border-radius: 2px;
+ background: var(--color-border);
+ overflow: hidden;
+}
+
+.capacity-meter-fill {
+ height: 100%;
+ border-radius: 2px;
+ background: var(--color-accent);
+ transition: width 400ms ease-out;
+}
+
+.text-warning {
+ color: var(--color-warning);
+}
+
+.text-danger {
+ color: var(--color-danger);
+}
+
// ---------------------------------------------------------------------------
// Hero numbers (lumen supply)
@@ -421,6 +626,13 @@
}
}
+.amount-precise {
+ margin-top: 4px;
+ color: var(--color-text-faint);
+ font-size: 12px;
+ font-variant-numeric: tabular-nums;
+}
+
// ---------------------------------------------------------------------------
// Skeletons
From 62d5700841fcc6f6caf31bdbd6fa9cd2a988a22a Mon Sep 17 00:00:00 2001
From: Miguel Nieto <39246879+miguelnietoa@users.noreply.github.com>
Date: Wed, 8 Jul 2026 20:03:23 -0500
Subject: [PATCH 05/34] Restyle D3 charts with validated brand palette
---
frontend/components/D3BarChart.jsx | 208 +++++++-------
frontend/components/D3BarChartNoXLabels.jsx | 272 +++++++++---------
.../components/FailedTransactionsChart.jsx | 59 ++--
frontend/components/LedgerCloseChart.jsx | 45 ++-
.../PublicNetworkLedgersHistoryChart.jsx | 48 ++--
frontend/components/TransactionsChart.jsx | 53 ++--
frontend/components/ui/chartUtils.js | 77 +++++
frontend/scss/_tokens.scss | 7 +-
8 files changed, 403 insertions(+), 366 deletions(-)
create mode 100644 frontend/components/ui/chartUtils.js
diff --git a/frontend/components/D3BarChart.jsx b/frontend/components/D3BarChart.jsx
index 56fdbe22..870284b7 100644
--- a/frontend/components/D3BarChart.jsx
+++ b/frontend/components/D3BarChart.jsx
@@ -1,150 +1,144 @@
-import React, { useEffect, useRef } from "react";
+import React, { useEffect, useRef, useState } from "react";
import * as d3 from "d3";
-
+import {
+ getChartTheme,
+ roundedTopRect,
+ createTooltip,
+ tooltipRow,
+} from "./ui/chartUtils.js";
+
+// Single-series bar chart (used for ledger close times).
export default function D3BarChart({
data,
width = 400,
height = 120,
- margin = { top: 10, right: 10, bottom: 30, left: 50 },
- colorScale,
+ margin = { top: 10, right: 10, bottom: 8, left: 50 },
tickFormat,
+ tooltipTitle,
+ valueFormat,
}) {
const svgRef = useRef();
+ // Charts re-render when the theme changes so D3 picks up the new palette.
+ const [themeTick, setThemeTick] = useState(0);
+
+ useEffect(() => {
+ const onThemeChange = () => setThemeTick((t) => t + 1);
+ window.addEventListener("themechange", onThemeChange);
+ return () => window.removeEventListener("themechange", onThemeChange);
+ }, []);
useEffect(() => {
if (!data || data.length === 0) return;
+ const theme = getChartTheme(svgRef.current);
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove(); // Clear previous render
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
- // Flatten all values from all series for domain calculation
- const allValues = data.flatMap((series) => series.values);
- const xValues = allValues.map((d) => d.x);
- const yValues = allValues.map((d) => d.y);
+ const values = data[0].values;
+ const xValues = values.map((d) => d.x);
+ const yValues = values.map((d) => d.y);
- // Create scales - use scalePoint for fixed spacing instead of scaleBand
const xScale = d3
.scalePoint()
.domain(xValues)
.range([0, innerWidth])
- .padding(1.0); // Double the gap - more space from Y-axis
+ .padding(1.0);
const yScale = d3
.scaleLinear()
.domain([0, d3.max(yValues)])
.range([innerHeight, 0]);
- // Fixed bar dimensions
- const fixedBarWidth = 5; // Fixed 5px bar width
- const fixedGapWidth = 3; // Fixed 3px gap between bars
-
- // Create color scale - match original react-d3-components colors
- const colors =
- colorScale ||
- d3
- .scaleOrdinal()
- .range([
- "#1f77b4",
- "#ff7f0e",
- "#2ca02c",
- "#d62728",
- "#9467bd",
- "#8c564b",
- "#e377c2",
- "#7f7f7f",
- "#bcbd22",
- "#17becf",
- ]);
-
- // Create main group
+ const barWidth = 5;
+
const g = svg
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
- // Calculate bar width - always use fixed width for all bars
- const barWidth = fixedBarWidth; // All bars are exactly 5px wide
-
- // Add bars for each series
- data.forEach((series, seriesIndex) => {
- g.selectAll(`.bar-${seriesIndex}`)
- .data(series.values)
- .enter()
- .append("rect")
- .attr("class", `bar-${seriesIndex}`)
- .attr("x", (d) => xScale(d.x) - fixedBarWidth / 2) // Center the bar on the scale point
- .attr("y", (d) => yScale(d.y))
- .attr("width", barWidth) // Always 5px wide
- .attr("height", (d) => innerHeight - yScale(d.y))
- .attr("fill", colors(seriesIndex))
- .style("shape-rendering", "crispEdges"); // Crisp edges like original
- });
-
- // Add x-axis with styling to match original
- const xAxis = d3.axisBottom(xScale).tickSize(6).tickPadding(3);
-
- const xAxisGroup = g
- .append("g")
- .attr("class", "axis")
- .attr("transform", `translate(0,${innerHeight})`)
- .call(xAxis);
-
- // Style x-axis to match original
- xAxisGroup
- .selectAll("text")
- .style("font-size", "10px")
- .style("font-family", "sans-serif")
- .style("fill", "#000");
-
- xAxisGroup
+ // Recessive horizontal gridlines carry the scale; no axis spines.
+ const yTickValues = yScale.ticks(4);
+ g.append("g")
+ .attr("class", "chart-grid")
.selectAll("line")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
-
- xAxisGroup
- .select(".domain")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
-
- // Add y-axis with styling to match original
+ .data(yTickValues)
+ .enter()
+ .append("line")
+ .attr("x1", 0)
+ .attr("x2", innerWidth)
+ .attr("y1", (d) => yScale(d))
+ .attr("y2", (d) => yScale(d))
+ .style("stroke", theme.grid);
+
+ // Bars: thin marks with rounded data-ends anchored to the baseline.
+ g.selectAll(".bar")
+ .data(values)
+ .enter()
+ .append("path")
+ .attr("class", "bar")
+ .attr("d", (d) =>
+ roundedTopRect(
+ xScale(d.x) - barWidth / 2,
+ yScale(d.y),
+ barWidth,
+ innerHeight - yScale(d.y),
+ 2,
+ ),
+ )
+ .attr("fill", theme.primary);
+
+ // Y axis: text only.
const yAxis = d3
.axisLeft(yScale)
- .tickSize(6)
- .tickPadding(3)
- .ticks(Math.min(7, Math.floor(d3.max(yValues)))) // Limit to max 7 ticks or the max value, whichever is smaller
- .tickValues(
- d3.range(0, Math.ceil(d3.max(yValues)) + 1).filter((d) => d % 1 === 0),
- ); // Only integer values
-
- if (tickFormat) {
- yAxis.tickFormat(tickFormat);
- } else {
- yAxis.tickFormat(d3.format("d")); // Format as integers (1, 2, 3) instead of decimals (1.0, 2.0)
- }
+ .tickSize(0)
+ .tickPadding(8)
+ .tickValues(yTickValues)
+ .tickFormat(tickFormat || d3.format("d"));
const yAxisGroup = g.append("g").attr("class", "axis").call(yAxis);
-
- // Style y-axis to match original
- yAxisGroup
- .selectAll("text")
- .style("font-size", "10px")
- .style("font-family", "sans-serif")
- .style("fill", "#000");
-
- yAxisGroup
- .selectAll("line")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
-
- yAxisGroup
- .select(".domain")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
- }, [data, width, height, margin, colorScale, tickFormat]);
+ yAxisGroup.select(".domain").remove();
+ yAxisGroup.selectAll("text").style("fill", theme.axisText);
+
+ // Hover layer: full-height hit targets, one per bar.
+ const tooltip = createTooltip();
+ const step = Math.max(innerWidth / Math.max(xValues.length, 1), barWidth);
+ g.selectAll(".hit")
+ .data(values)
+ .enter()
+ .append("rect")
+ .attr("class", "hit")
+ .attr("x", (d) => xScale(d.x) - step / 2)
+ .attr("y", 0)
+ .attr("width", step)
+ .attr("height", innerHeight)
+ .attr("fill", "transparent")
+ .on("mousemove", (event, d) => {
+ const title = tooltipTitle ? tooltipTitle(d.x) : d.x;
+ const value = valueFormat ? valueFormat(d.y) : d.y;
+ tooltip.show(
+ `
${title}
` +
+ tooltipRow(theme.primary, data[0].label, value),
+ event.clientX,
+ event.clientY,
+ );
+ })
+ .on("mouseleave", () => tooltip.hide());
+
+ return () => tooltip.destroy();
+ }, [
+ data,
+ width,
+ height,
+ margin,
+ tickFormat,
+ tooltipTitle,
+ valueFormat,
+ themeTick,
+ ]);
return
;
}
diff --git a/frontend/components/D3BarChartNoXLabels.jsx b/frontend/components/D3BarChartNoXLabels.jsx
index 48a65f13..42ee5ec9 100644
--- a/frontend/components/D3BarChartNoXLabels.jsx
+++ b/frontend/components/D3BarChartNoXLabels.jsx
@@ -1,206 +1,206 @@
-import React, { useEffect, useRef } from "react";
+import React, { useEffect, useRef, useState } from "react";
import * as d3 from "d3";
-
+import {
+ getChartTheme,
+ roundedTopRect,
+ createTooltip,
+ tooltipRow,
+} from "./ui/chartUtils.js";
+
+// Stacked two-series bar chart without x labels (txs/ops, successful/failed).
export default function D3BarChartNoXLabels({
data,
width = 400,
height = 120,
- margin = { top: 10, right: 10, bottom: 8, left: 50 }, // Reduced bottom margin since no X labels
- colorScale,
+ margin = { top: 10, right: 10, bottom: 8, left: 50 },
tickFormat,
- yAxisMax = 450, // Maximum Y value
- yAxisStep = 50, // Y axis increment step
+ yAxisMax = 450,
+ yAxisStep = 50,
+ tooltipTitle,
+ valueFormat,
}) {
const svgRef = useRef();
+ // Charts re-render when the theme changes so D3 picks up the new palette.
+ const [themeTick, setThemeTick] = useState(0);
+
+ useEffect(() => {
+ const onThemeChange = () => setThemeTick((t) => t + 1);
+ window.addEventListener("themechange", onThemeChange);
+ return () => window.removeEventListener("themechange", onThemeChange);
+ }, []);
useEffect(() => {
if (!data || data.length === 0) return;
+ const theme = getChartTheme(svgRef.current);
+ const seriesColors = [theme.seriesA, theme.seriesB];
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove(); // Clear previous render
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
- // Flatten all values from all series for domain calculation
- const allValues = data.flatMap((series) => series.values);
- const xValues = allValues.map((d) => d.x);
- const yValues = allValues.map((d) => d.y);
+ const xValues = data[0].values.map((d) => d.x);
- // Create scales - use scalePoint for fixed spacing instead of scaleBand
const xScale = d3
.scalePoint()
.domain(xValues)
.range([0, innerWidth])
- .padding(1.0); // Double the gap - more space from Y-axis
+ .padding(1.0);
const yScale = d3
.scaleLinear()
- .domain([0, yAxisMax]) // Use fixed max instead of data max
+ .domain([0, yAxisMax])
.range([innerHeight, 0]);
- // Fixed bar dimensions
- const fixedBarWidth = 5; // Fixed 5px bar width
- const fixedGapWidth = 3; // Fixed 3px gap between bars
-
- // Create color scale - match original react-d3-components colors
- const colors =
- colorScale ||
- d3
- .scaleOrdinal()
- .range([
- "#1f77b4",
- "#ff7f0e",
- "#2ca02c",
- "#d62728",
- "#9467bd",
- "#8c564b",
- "#e377c2",
- "#7f7f7f",
- "#bcbd22",
- "#17becf",
- ]);
-
- // Create main group
+ const barWidth = 5;
+ const segmentGap = 2; // surface gap between stacked segments
+
const g = svg
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
- // Calculate bar width - always use fixed width for all bars
- const barWidth = fixedBarWidth; // All bars are exactly 5px wide
-
- // Add bars for each series - create stacked bars
- // First, we need to calculate the cumulative values for stacking
+ // Recessive horizontal gridlines carry the scale; no axis spines.
+ // Let D3 pick round tick values within the fixed domain.
+ const yTickValues = yScale.ticks(4);
+ g.append("g")
+ .attr("class", "chart-grid")
+ .selectAll("line")
+ .data(yTickValues)
+ .enter()
+ .append("line")
+ .attr("x1", 0)
+ .attr("x2", innerWidth)
+ .attr("y1", (d) => yScale(d))
+ .attr("y2", (d) => yScale(d))
+ .style("stroke", theme.grid);
+
+ // Stack the two series (bottom = series 0, top = series 1).
const stackedData = [];
-
- // Assume we have exactly 2 series for stacking
if (data.length === 2) {
- const xValues = data[0].values.map((d) => d.x);
-
xValues.forEach((x, index) => {
- const bottomValue = data[0].values[index].y; // First series (bottom)
- const topValue = data[1].values[index].y; // Second series (top)
-
stackedData.push({
x: x,
- bottom: bottomValue,
- top: topValue,
- bottomHeight: bottomValue,
- topHeight: topValue,
- totalHeight: bottomValue + topValue,
+ bottom: data[0].values[index].y,
+ top: data[1].values[index].y,
});
});
- // Draw bottom bars (first series)
- g.selectAll(".bar-bottom")
- .data(stackedData)
- .enter()
- .append("rect")
- .attr("class", "bar-bottom")
- .attr("x", (d) => xScale(d.x) - fixedBarWidth / 2)
- .attr("y", (d) => yScale(d.bottomHeight))
- .attr("width", barWidth)
- .attr("height", (d) => innerHeight - yScale(d.bottomHeight))
- .attr("fill", colors(0))
- .style("shape-rendering", "crispEdges");
-
- // Draw top bars (second series) - stacked on top of bottom bars
- g.selectAll(".bar-top")
+ const bars = g
+ .selectAll(".bar-group")
.data(stackedData)
.enter()
- .append("rect")
- .attr("class", "bar-top")
- .attr("x", (d) => xScale(d.x) - fixedBarWidth / 2)
- .attr("y", (d) => yScale(d.totalHeight))
- .attr("width", barWidth)
- .attr("height", (d) => yScale(d.bottomHeight) - yScale(d.totalHeight))
- .attr("fill", colors(1))
- .style("shape-rendering", "crispEdges");
+ .append("g")
+ .attr("class", "bar-group");
+
+ // Bottom segment: flat unless it is the data end, rounded when alone.
+ bars
+ .append("path")
+ .attr("d", (d) => {
+ const x = xScale(d.x) - barWidth / 2;
+ const yTop = yScale(d.bottom);
+ const h = innerHeight - yTop;
+ if (d.top > 0) {
+ return h > 0
+ ? `M${x},${yTop} H${x + barWidth} V${innerHeight} H${x} Z`
+ : "";
+ }
+ return roundedTopRect(x, yTop, barWidth, h, 2);
+ })
+ .attr("fill", seriesColors[0]);
+
+ // Top segment: rounded data end, separated by a surface gap.
+ bars
+ .append("path")
+ .attr("d", (d) => {
+ if (d.top <= 0) {
+ return "";
+ }
+ const x = xScale(d.x) - barWidth / 2;
+ const yTotal = yScale(d.bottom + d.top);
+ const yBottom = yScale(d.bottom);
+ const h = Math.max(yBottom - yTotal - segmentGap, 0.5);
+ return roundedTopRect(x, yTotal, barWidth, h, 2);
+ })
+ .attr("fill", seriesColors[1]);
} else {
- // Fallback to original behavior for non-stacked charts
+ // Fallback for non-stacked charts.
data.forEach((series, seriesIndex) => {
g.selectAll(`.bar-${seriesIndex}`)
.data(series.values)
.enter()
- .append("rect")
+ .append("path")
.attr("class", `bar-${seriesIndex}`)
- .attr("x", (d) => xScale(d.x) - fixedBarWidth / 2)
- .attr("y", (d) => yScale(d.y))
- .attr("width", barWidth)
- .attr("height", (d) => innerHeight - yScale(d.y))
- .attr("fill", colors(seriesIndex))
- .style("shape-rendering", "crispEdges");
+ .attr("d", (d) =>
+ roundedTopRect(
+ xScale(d.x) - barWidth / 2,
+ yScale(d.y),
+ barWidth,
+ innerHeight - yScale(d.y),
+ 2,
+ ),
+ )
+ .attr("fill", seriesColors[seriesIndex % seriesColors.length]);
});
}
- // Add x-axis with NO labels
- const xAxis = d3
- .axisBottom(xScale)
- .tickSize(6)
- .tickPadding(3)
- .tickFormat(""); // No labels
-
- const xAxisGroup = g
- .append("g")
- .attr("class", "axis")
- .attr("transform", `translate(0,${innerHeight})`)
- .call(xAxis);
-
- // Style x-axis lines only (no text)
- xAxisGroup
- .selectAll("line")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
-
- xAxisGroup
- .select(".domain")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
-
- // Add y-axis with custom ticks
- const yAxisTicks = d3.range(0, yAxisMax + yAxisStep, yAxisStep); // [0, 50, 100, 150, ..., yAxisMax]
-
+ // Y axis: text only.
const yAxis = d3
.axisLeft(yScale)
- .tickSize(6)
- .tickPadding(3)
- .tickValues(yAxisTicks);
-
- if (tickFormat) {
- yAxis.tickFormat(tickFormat);
- } else {
- yAxis.tickFormat(d3.format("d")); // Format as integers
- }
+ .tickSize(0)
+ .tickPadding(8)
+ .tickValues(yTickValues)
+ .tickFormat(tickFormat || d3.format("d"));
const yAxisGroup = g.append("g").attr("class", "axis").call(yAxis);
+ yAxisGroup.select(".domain").remove();
+ yAxisGroup.selectAll("text").style("fill", theme.axisText);
- // Style y-axis to match original
- yAxisGroup
- .selectAll("text")
- .style("font-size", "10px")
- .style("font-family", "sans-serif")
- .style("fill", "#000");
-
- yAxisGroup
- .selectAll("line")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
+ // Hover layer: full-height hit targets, one per bar.
+ const tooltip = createTooltip();
+ if (data.length === 2) {
+ const step = Math.max(
+ innerWidth / Math.max(xValues.length, 1),
+ barWidth,
+ );
+ g.selectAll(".hit")
+ .data(stackedData)
+ .enter()
+ .append("rect")
+ .attr("class", "hit")
+ .attr("x", (d) => xScale(d.x) - step / 2)
+ .attr("y", 0)
+ .attr("width", step)
+ .attr("height", innerHeight)
+ .attr("fill", "transparent")
+ .on("mousemove", (event, d) => {
+ const title = tooltipTitle ? tooltipTitle(d.x) : d.x;
+ const fmt = valueFormat || ((v) => v.toLocaleString("en-US"));
+ tooltip.show(
+ `
${title}
` +
+ tooltipRow(seriesColors[0], data[0].label, fmt(d.bottom)) +
+ tooltipRow(seriesColors[1], data[1].label, fmt(d.top)),
+ event.clientX,
+ event.clientY,
+ );
+ })
+ .on("mouseleave", () => tooltip.hide());
+ }
- yAxisGroup
- .select(".domain")
- .style("stroke", "#000")
- .style("shape-rendering", "crispEdges");
+ return () => tooltip.destroy();
}, [
data,
width,
height,
margin,
- colorScale,
tickFormat,
yAxisMax,
yAxisStep,
+ tooltipTitle,
+ valueFormat,
+ themeTick,
]);
return
;
diff --git a/frontend/components/FailedTransactionsChart.jsx b/frontend/components/FailedTransactionsChart.jsx
index 34fc5dbc..366335d7 100644
--- a/frontend/components/FailedTransactionsChart.jsx
+++ b/frontend/components/FailedTransactionsChart.jsx
@@ -1,30 +1,15 @@
import React from "react";
-import Panel from "muicss/lib/react/panel";
import axios from "axios";
import * as d3 from "d3";
import D3BarChartNoXLabels from "./D3BarChartNoXLabels.jsx";
import clone from "lodash/clone";
import each from "lodash/each";
+import Card from "./ui/Card.jsx";
export default class FailedTransactionsChart extends React.Component {
constructor(props) {
super(props);
this.panel = null;
- // Use the same colors as the original react-d3-components
- this.colorScale = d3
- .scaleOrdinal()
- .range([
- "#1f77b4",
- "#ff7f0e",
- "#2ca02c",
- "#d62728",
- "#9467bd",
- "#8c564b",
- "#e377c2",
- "#7f7f7f",
- "#bcbd22",
- "#17becf",
- ]);
this.state = {
loading: true,
chartWidth: 400,
@@ -33,6 +18,7 @@ export default class FailedTransactionsChart extends React.Component {
yAxisStep: 100, // Default value, will be updated dynamically
};
this.url = `${this.props.horizonURL}/ledgers?order=desc&limit=${this.props.limit}`;
+ this.tooltipTitle = (x) => `Ledger #${x}`;
}
componentDidMount() {
@@ -43,7 +29,7 @@ export default class FailedTransactionsChart extends React.Component {
}
updateSize() {
- let value = this.panel.offsetWidth - 20;
+ let value = this.panel.offsetWidth - 42;
if (this.state.chartWidth != value) {
this.setState({ chartWidth: value });
}
@@ -126,11 +112,11 @@ export default class FailedTransactionsChart extends React.Component {
axios.get(this.url).then((response) => {
let data = [
{
- label: "Success",
+ label: "Successful",
values: [],
},
{
- label: "Fail",
+ label: "Failed",
values: [],
},
];
@@ -164,33 +150,38 @@ export default class FailedTransactionsChart extends React.Component {
this.panel = el;
}}
>
-
-
-
- Successful
- {" "}
- &{" "}
-
Failed {" "}
- Txs in the last {this.props.limit} ledgers: {this.props.network}
-
- API
-
-
+
+
+ Successful
+
+
+ Failed
+
+
+ }
+ >
{this.state.loading ? (
- "Loading..."
+