From 5d450cf815829f12d05934e3dff22f1abc41b853 Mon Sep 17 00:00:00 2001 From: EspenTinius Date: Tue, 19 May 2026 17:20:27 +0200 Subject: [PATCH] stats side --- .../ntnu/idi/idatt2003/g40/mappe/Main.java | 74 +- .../mappe/view/widgets/stats/HoldingData.java | 94 +++ .../view/widgets/stats/StatsActions.java | 13 + .../view/widgets/stats/StatsController.java | 169 +++++ .../mappe/view/widgets/stats/StatsView.java | 634 ++++++++++++++++++ .../view/widgets/topbar/TopBarController.java | 84 ++- src/main/resources/styles.css | 349 ++++------ 7 files changed, 1152 insertions(+), 265 deletions(-) create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/HoldingData.java create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsActions.java create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsController.java create mode 100644 src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsView.java diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java index d86e8d9..4d11e40 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/Main.java @@ -25,11 +25,12 @@ import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.dashboard.DashBoardView; import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.market.MarketController; import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.market.MarketView; +import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.stats.StatsController; +import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.stats.StatsView; import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.topbar.TopBarController; import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.topbar.TopBarView; -import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.transactions.TransactionsController; -import edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.transactions.TransactionsView; import javafx.application.Application; +import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.layout.Pane; import javafx.scene.text.Font; @@ -38,10 +39,14 @@ /** * Main class. * - *

Extends {@link Application}

+ *

+ * Extends {@link Application} + *

* - *

Initializes the application through the javafx framework.

- * */ + *

+ * Initializes the application through the javafx framework. + *

+ */ public class Main extends Application { static void main(String[] args) { @@ -50,12 +55,12 @@ static void main(String[] args) { /** * {@inheritDoc} - * */ + */ @Override public void start(final Stage stage) throws Exception { Scene scene = new Scene(new Pane()); scene.getStylesheets() - .add(Objects.requireNonNull(getClass().getResource("/styles.css")).toExternalForm()); + .add(Objects.requireNonNull(getClass().getResource("/styles.css")).toExternalForm()); Font.loadFont(getClass().getResourceAsStream("/Fonts/Aptos.ttf"), 16); stage.setScene(scene); stage.setWidth(ConfigValues.VIEWPORT_WIDTH.getValue()); @@ -85,7 +90,6 @@ public void start(final Stage stage) throws Exception { SaveGameService saveGameService = new SaveGameService(); playGameView.setSaves(saveGameService.loadSaves()); - // Settings SettingsView settingsView = new SettingsView(); new SettingsController(settingsView, eventManager); @@ -102,40 +106,46 @@ public void start(final Stage stage) throws Exception { TopBarView topBarView2 = new TopBarView(); new TopBarController(topBarView2, eventManager); - // Stats page (dashboard, default center-view in-game) + // Dashboard (default center-view in-game - første siden du ser) DashBoardView dashBoardView = new DashBoardView(); new DashBoardController(dashBoardView, - eventManager, - player, - exchange, - stocksInFile); + eventManager, + player, + exchange, + stocksInFile); + + // Stats page (Stats-knappen i topbaren tar deg hit) + StatsView statsView = new StatsView(); + new StatsController(statsView, eventManager, player, exchange); - // Market page (vises i samme InGameView, byttes til via Market-knappen) + // Market page (Market-knappen tar deg hit) MarketView marketView = new MarketView(); new MarketController(marketView, - eventManager, - player, - exchange, - stocksInFile); - - TransactionsView transactionsView = new TransactionsView(); - TransactionsController transactionsController = new TransactionsController( - transactionsView, - eventManager, - player.getTransactionArchive()); + eventManager, + player, + exchange, + stocksInFile); // In-game (Change "topBarView" to "topBarView2" if no summary section). // Dashboard er default center-view. InGameView inGameView = new InGameView(topBarView, dashBoardView.getRootPane()); - // Wire top bar buttons til å bytte mellom dashboard og market. + // Transactions-widgeten finnes ikke enda - bruker dashboardet som + // placeholder slik at TRANSACTIONS-knappen ikke krasjer. Bytt ut når + // den faktiske transactions-widgeten er på plass. + Node transactionsCenter = dashBoardView.getRootPane(); + Runnable onTransactionUpdate = () -> { + }; + + // Wire top bar buttons til å bytte mellom dashboard / stats / market / + // transactions. Stats-knappen tar deg til stats-siden. topBarController.setMarketIntegration( - inGameView::changeCenterView, - dashBoardView.getRootPane(), - marketView.getRootPane(), - transactionsView.getRootPane(), - transactionsController::refresh - ); + inGameView::changeCenterView, + dashBoardView.getRootPane(), + marketView.getRootPane(), + statsView.getRootPane(), + transactionsCenter, + onTransactionUpdate); // Register all views viewManager.addView(mainMenuView); @@ -146,4 +156,4 @@ public void start(final Stage stage) throws Exception { stage.show(); } -} +} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/HoldingData.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/HoldingData.java new file mode 100644 index 0000000..26b1f9e --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/HoldingData.java @@ -0,0 +1,94 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.stats; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * Immutable data carrier for a single holding shown in {@link StatsView}. + * + *

Each instance represents an aggregated position in a single stock: + * the total number of shares owned, the weighted average purchase price, + * and the current market price. Derived values (value, cost, P&L) are + * computed on demand.

+ * */ +public final class HoldingData { + + /** The stock ticker symbol. */ + private final String ticker; + + /** Total number of shares owned of this ticker. */ + private final BigDecimal shares; + + /** Weighted average purchase price per share. */ + private final BigDecimal avgPrice; + + /** Current market price per share. */ + private final BigDecimal currentPrice; + + /** + * Creates a new holding data row. + * + * @param ticker the stock ticker symbol. + * @param shares total number of shares owned. + * @param avgPrice weighted average purchase price per share. + * @param currentPrice current market price per share. + * + * @throws IllegalArgumentException if any argument is null. + * */ + public HoldingData(final String ticker, + final BigDecimal shares, + final BigDecimal avgPrice, + final BigDecimal currentPrice) throws IllegalArgumentException { + if (ticker == null || shares == null || avgPrice == null || currentPrice == null) { + throw new IllegalArgumentException("Invalid holding data!"); + } + this.ticker = ticker; + this.shares = shares; + this.avgPrice = avgPrice; + this.currentPrice = currentPrice; + } + + /** @return the ticker symbol. */ + public String getTicker() { + return ticker; + } + + /** @return total shares owned. */ + public BigDecimal getShares() { + return shares; + } + + /** @return weighted average purchase price. */ + public BigDecimal getAvgPrice() { + return avgPrice; + } + + /** @return current market price. */ + public BigDecimal getCurrentPrice() { + return currentPrice; + } + + /** @return current market value of this position (shares * currentPrice). */ + public BigDecimal getValue() { + return shares.multiply(currentPrice); + } + + /** @return total cost paid for this position (shares * avgPrice). */ + public BigDecimal getCost() { + return shares.multiply(avgPrice); + } + + /** @return absolute profit and loss (value - cost). */ + public BigDecimal getPnl() { + return getValue().subtract(getCost()); + } + + /** @return profit and loss as a percentage of cost. */ + public double getPnlPct() { + BigDecimal cost = getCost(); + if (cost.signum() == 0) { + return 0.0; + } + return getPnl().divide(cost, 6, RoundingMode.HALF_UP).doubleValue() * 100.0; + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsActions.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsActions.java new file mode 100644 index 0000000..761e670 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsActions.java @@ -0,0 +1,13 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.stats; + +/** + * Enum representing all interactable actions in {@link StatsView}. + * + *

The stats widget is mostly an informational dashboard, so it currently + * has no top-level buttons of its own. {@code SELECT_HOLDING} is reserved + * for future use when individual holdings rows become clickable.

+ * */ +public enum StatsActions { + /** Reserved for future holding-row click handling. */ + SELECT_HOLDING; +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsController.java new file mode 100644 index 0000000..512a026 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsController.java @@ -0,0 +1,169 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.stats; + +import edu.ntnu.idi.idatt2003.g40.mappe.engine.Exchange; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Player; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Share; +import edu.ntnu.idi.idatt2003.g40.mappe.model.Stock; +import edu.ntnu.idi.idatt2003.g40.mappe.service.event.EventManager; +import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewController; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for {@link StatsView}. + * + *

+ * Aggregates the players cash, holdings and net-worth history into the + * shape the view expects, and listens to the {@link Exchange} and + * {@link Player} so the dashboard stays in sync as the game advances. + *

+ * + *

+ * Net-worth history is recorded by this controller: a new sample is + * appended each time the exchange advances to a new week. Holdings rows + * aggregate every {@link Share} the player owns of the same symbol into a + * single row with the weighted average purchase price. + *

+ */ +public class StatsController extends ViewController { + + /** The {@link Player} whose state is displayed. */ + private final Player player; + + /** The {@link Exchange} that drives the week-by-week simulation. */ + private final Exchange exchange; + + /** + * Snapshot of the players net worth at each recorded week. + * + *

+ * Initialised inside {@link #initInteractions()} rather than at the + * declaration site because field initializers run after the super + * constructor returns, but the super constructor itself invokes + * {@code initInteractions()}, leaving the field {@code null} during + * the first use otherwise. + *

+ */ + private List balanceHistory; + + /** + * Constructor. + * + * @param viewElement the {@link StatsView} this controller is attached to. + * @param eventManager the active {@link EventManager}. + * @param player the {@link Player} whose state is displayed. + * @param exchange the {@link Exchange} driving the simulation. + * + * @throws IllegalArgumentException if any argument is invalid. + */ + public StatsController(final StatsView viewElement, + final EventManager eventManager, + final Player player, + final Exchange exchange) + throws IllegalArgumentException { + this.player = player; + this.exchange = exchange; + super(viewElement, eventManager); + } + + /** {@inheritDoc} */ + @Override + protected void initInteractions() { + // Init state used during listener setup BEFORE wiring listeners. + // Field initializers don't run until after the super-constructor + // returns, but initInteractions is called from inside the super- + // constructor, so we must initialize the list here. + balanceHistory = new ArrayList<>(); + + // Seed the history with the players starting balance so the chart + // and the all-time P&L KPI have a meaningful baseline from week 1. + balanceHistory.add(player.getStartingMoney()); + pushSnapshot(); + + exchange.weekProperty().addListener((observable, o, n) -> { + balanceHistory.add(player.getNetWorth()); + pushSnapshot(); + }); + + player.getNetWorthAsFloatProperty().addListener((observable, o, n) -> { + pushSnapshot(); + }); + } + + /** + * Pushes the current player/exchange state into the view. + */ + private void pushSnapshot() { + List holdings = buildHoldings(); + int totalTrades = player.getPortfolio().getShares().size(); + int winningTrades = countWinningTrades(); + + getViewElement().setData( + player.getMoney(), + holdings, + balanceHistory, + totalTrades, + winningTrades); + } + + /** + * Aggregates the players shares per ticker into one {@link HoldingData} + * row each, with the weighted average purchase price and the current + * market price from the underlying stock. + */ + private List buildHoldings() { + Map byTicker = new LinkedHashMap<>(); + for (Share s : player.getPortfolio().getShares()) { + Stock stock = s.getStock(); + Aggregate agg = byTicker.computeIfAbsent( + stock.getSymbol(), k -> new Aggregate(stock)); + agg.shares = agg.shares.add(s.getQuantity()); + agg.cost = agg.cost.add(s.getQuantity().multiply(s.getPurchasePrice())); + } + + List result = new ArrayList<>(); + for (Aggregate agg : byTicker.values()) { + BigDecimal avgPrice = (agg.shares.signum() == 0) + ? BigDecimal.ZERO + : agg.cost.divide(agg.shares, 6, RoundingMode.HALF_UP); + result.add(new HoldingData( + agg.stock.getSymbol(), + agg.shares, + avgPrice, + agg.stock.getSalesPrice())); + } + return result; + } + + /** + * Counts the number of {@link Share} objects whose current market price + * is greater than or equal to the original purchase price. Used as a + * proxy for trade winrate in the absence of explicit per-trade + * outcome tracking. + */ + private int countWinningTrades() { + int wins = 0; + for (Share s : player.getPortfolio().getShares()) { + if (s.getStock().getSalesPrice().compareTo(s.getPurchasePrice()) >= 0) { + wins++; + } + } + return wins; + } + + /** Internal helper for aggregating shares of the same ticker. */ + private static final class Aggregate { + final Stock stock; + BigDecimal shares = BigDecimal.ZERO; + BigDecimal cost = BigDecimal.ZERO; + + Aggregate(final Stock stock) { + this.stock = stock; + } + } +} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsView.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsView.java new file mode 100644 index 0000000..8f35e97 --- /dev/null +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/stats/StatsView.java @@ -0,0 +1,634 @@ +package edu.ntnu.idi.idatt2003.g40.mappe.view.widgets.stats; + +import edu.ntnu.idi.idatt2003.g40.mappe.view.ViewElement; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Group; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.RowConstraints; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Arc; +import javafx.scene.shape.ArcType; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Line; +import javafx.scene.shape.StrokeLineCap; +import javafx.scene.text.Text; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Stats widget shown as the default center-view of + * {@link edu.ntnu.idi.idatt2003.g40.mappe.view.ingame.InGameView}. + * + *

Layout: a KPI strip across the top, then a balance-over-time chart + * panel and an allocation pie panel side by side, and a holdings list + * spanning the full width below. Navigation (back, market, settings, etc.) + * is handled by the shared top bar, so this widget contains no chrome of + * its own and fills the entire center area with no outer padding.

+ * */ +public class StatsView extends ViewElement { + + /** Pie slice palette - matches the mockup. */ + private static final String[] PIE_COLORS = { + "#27ae60", "#3498db", "#9b59b6", "#e67e22", + "#f1c40f", "#1abc9c", "#e74c3c", "#34495e" + }; + + /** Color for the cash slice of the allocation pie. */ + private static final String CASH_COLOR = "#bdc3c7"; + + /** KPI strip - five tiles across the top. */ + private Label kpiTotalValue; + private Label kpiTotalSub; + private Label kpiCashValue; + private Label kpiInvestedValue; + private Label kpiInvestedSub; + private Label kpiPnlValue; + private Label kpiPnlSub; + private Label kpiTradesValue; + private Label kpiTradesSub; + + /** Balance-over-time chart area. Lines, axes etc. are added on render. */ + private Pane balanceChartPane; + + /** Allocation pie. Arc segments are added on render. */ + private Group allocPie; + + /** Allocation legend - one row per slice. */ + private VBox allocLegend; + + /** Holdings list rows. */ + private VBox holdingsList; + + /** Current player cash. */ + private BigDecimal cash; + + /** Current holdings shown in the list and pie. */ + private List holdings; + + /** Net worth at every recorded week. */ + private List balanceHistory; + + /** Total number of trades made. */ + private int totalTrades; + + /** Number of winning trades. */ + private int winningTrades; + + /** Constructor. Widget without its own ViewEnum. */ + public StatsView() { + super(new VBox(), StatsActions.class); + } + + /** {@inheritDoc} */ + @Override + protected void initLayout() { + // Init all state used during layout/render BEFORE building the UI. + // Field initializers don't run until after the super-constructor + // returns, but initLayout is called from inside the super-constructor. + cash = BigDecimal.ZERO; + holdings = new ArrayList<>(); + balanceHistory = new ArrayList<>(); + totalTrades = 0; + winningTrades = 0; + + VBox root = getRootPane(); + root.setSpacing(0); + root.setFillWidth(true); + root.setPadding(Insets.EMPTY); + + GridPane content = buildContentGrid(); + VBox.setVgrow(content, Priority.ALWAYS); + root.getChildren().add(content); + + refresh(); + } + + /** + * Builds the 2-column / 3-row grid that holds the KPI strip, the chart + * and allocation panels, and the holdings panel. + * */ + private GridPane buildContentGrid() { + GridPane grid = new GridPane(); + grid.setHgap(12); + grid.setVgap(12); + grid.setPadding(Insets.EMPTY); + + ColumnConstraints col1 = new ColumnConstraints(); + col1.setPercentWidth(54.5); + col1.setHgrow(Priority.ALWAYS); + ColumnConstraints col2 = new ColumnConstraints(); + col2.setPercentWidth(45.5); + col2.setHgrow(Priority.ALWAYS); + grid.getColumnConstraints().addAll(col1, col2); + + RowConstraints r1 = new RowConstraints(); + RowConstraints r2 = new RowConstraints(); + r2.setVgrow(Priority.ALWAYS); + RowConstraints r3 = new RowConstraints(); + r3.setVgrow(Priority.SOMETIMES); + grid.getRowConstraints().addAll(r1, r2, r3); + + HBox kpiStrip = buildKpiStrip(); + VBox chartPanel = buildChartPanel(); + VBox allocPanel = buildAllocPanel(); + VBox holdingsPanel = buildHoldingsPanel(); + + grid.add(kpiStrip, 0, 0, 2, 1); + grid.add(chartPanel, 0, 1); + grid.add(allocPanel, 1, 1); + grid.add(holdingsPanel, 0, 2, 2, 1); + + return grid; + } + + /** + * Builds the top KPI strip with 5 evenly-sized tiles. + * */ + private HBox buildKpiStrip() { + kpiTotalValue = new Label("\u2014"); + kpiTotalSub = new Label("\u2014"); + VBox totalTile = buildKpiTile("total value", kpiTotalValue, kpiTotalSub); + + kpiCashValue = new Label("\u2014"); + Label cashSub = new Label("available"); + cashSub.getStyleClass().add("stats-kpi-sub"); + VBox cashTile = buildKpiTile("cash", kpiCashValue, cashSub); + + kpiInvestedValue = new Label("\u2014"); + kpiInvestedSub = new Label("\u2014"); + VBox investedTile = buildKpiTile("invested", kpiInvestedValue, kpiInvestedSub); + + kpiPnlValue = new Label("\u2014"); + kpiPnlSub = new Label("since week 1"); + kpiPnlSub.getStyleClass().add("stats-kpi-sub"); + VBox pnlTile = buildKpiTile("all-time P&L", kpiPnlValue, kpiPnlSub); + + kpiTradesValue = new Label("\u2014"); + kpiTradesSub = new Label("\u2014"); + VBox tradesTile = buildKpiTile("trades", kpiTradesValue, kpiTradesSub); + + HBox strip = new HBox(10, totalTile, cashTile, investedTile, pnlTile, tradesTile); + for (VBox tile : List.of(totalTile, cashTile, investedTile, pnlTile, tradesTile)) { + HBox.setHgrow(tile, Priority.ALWAYS); + tile.setMaxWidth(Double.MAX_VALUE); + } + strip.setFillHeight(true); + return strip; + } + + /** + * Helper that builds a single KPI tile with a label / value / sub-label. + * */ + private VBox buildKpiTile(final String labelText, + final Label valueLabel, + final Label subLabel) { + Label label = new Label(labelText); + label.getStyleClass().add("stats-kpi-label"); + valueLabel.getStyleClass().add("stats-kpi-value"); + + VBox tile = new VBox(2, label, valueLabel, subLabel); + tile.getStyleClass().add("stats-kpi"); + return tile; + } + + /** + * Builds the balance-over-time chart panel. + * */ + private VBox buildChartPanel() { + Label title = new Label("balance over time"); + title.getStyleClass().add("stats-panel-title"); + title.setMaxWidth(Double.MAX_VALUE); + + balanceChartPane = new Pane(); + balanceChartPane.setMinHeight(120); + VBox.setVgrow(balanceChartPane, Priority.ALWAYS); + // Re-render whenever the area is resized. + balanceChartPane.widthProperty().addListener((obs, o, n) -> renderBalanceChart()); + balanceChartPane.heightProperty().addListener((obs, o, n) -> renderBalanceChart()); + + VBox panel = new VBox(6, title, balanceChartPane); + panel.getStyleClass().add("stats-panel"); + panel.setMaxHeight(Double.MAX_VALUE); + return panel; + } + + /** + * Builds the allocation pie + legend panel. + * */ + private VBox buildAllocPanel() { + Label title = new Label("allocation"); + title.getStyleClass().add("stats-panel-title"); + title.setMaxWidth(Double.MAX_VALUE); + + allocPie = new Group(); + StackPane pieHolder = new StackPane(allocPie); + pieHolder.setMinSize(110, 110); + pieHolder.setPrefSize(110, 110); + pieHolder.setMaxSize(110, 110); + + allocLegend = new VBox(3); + HBox.setHgrow(allocLegend, Priority.ALWAYS); + + ScrollPane legendScroll = new ScrollPane(allocLegend); + legendScroll.setFitToWidth(true); + legendScroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + legendScroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + legendScroll.getStyleClass().add("stats-legend-scroll"); + HBox.setHgrow(legendScroll, Priority.ALWAYS); + + HBox wrap = new HBox(10, pieHolder, legendScroll); + wrap.setAlignment(Pos.CENTER_LEFT); + VBox.setVgrow(wrap, Priority.ALWAYS); + + VBox panel = new VBox(6, title, wrap); + panel.getStyleClass().add("stats-panel"); + panel.setMaxHeight(Double.MAX_VALUE); + return panel; + } + + /** + * Builds the holdings list panel. + * */ + private VBox buildHoldingsPanel() { + Label title = new Label("holdings"); + title.getStyleClass().add("stats-panel-title"); + title.setMaxWidth(Double.MAX_VALUE); + + holdingsList = new VBox(6); + + ScrollPane scroll = new ScrollPane(holdingsList); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scroll.getStyleClass().add("stats-holdings-scroll"); + VBox.setVgrow(scroll, Priority.ALWAYS); + + VBox panel = new VBox(6, title, scroll); + panel.getStyleClass().add("stats-panel"); + panel.setMinHeight(140); + panel.setMaxHeight(Double.MAX_VALUE); + return panel; + } + + /** {@inheritDoc} */ + @Override + protected void initStyling() { + getRootPane().getStyleClass().add("stats-container"); + } + + /** + * Updates all state shown in the widget and re-renders. + * + * @param cash current player cash. + * @param holdings current holdings (one row per ticker). + * @param balanceHistory recorded net worth per week. + * @param totalTrades number of trades the player has made. + * @param winningTrades number of those trades currently in profit. + * */ + public void setData(final BigDecimal cash, + final List holdings, + final List balanceHistory, + final int totalTrades, + final int winningTrades) { + this.cash = (cash == null) ? BigDecimal.ZERO : cash; + this.holdings = (holdings == null) ? new ArrayList<>() : new ArrayList<>(holdings); + this.balanceHistory = (balanceHistory == null) + ? new ArrayList<>() : new ArrayList<>(balanceHistory); + this.totalTrades = totalTrades; + this.winningTrades = winningTrades; + refresh(); + } + + /** + * Rebuilds every dynamic part of the widget (KPIs, chart, allocation, + * holdings list) from the current state. + * */ + public void refresh() { + if (kpiTotalValue == null) { + return; + } + renderKpis(); + renderBalanceChart(); + renderAllocation(); + renderHoldings(); + } + + // ---------- Renderers ---------- + + /** + * Recomputes derived values and updates the 5 KPI tiles. + * */ + private void renderKpis() { + BigDecimal invested = sumValue(holdings); + BigDecimal investedCost = sumCost(holdings); + BigDecimal totalValue = cash.add(invested); + + BigDecimal startBalance = balanceHistory.isEmpty() + ? totalValue : balanceHistory.getFirst(); + BigDecimal allTimePnl = totalValue.subtract(startBalance); + double allTimePnlPct = (startBalance.signum() == 0) + ? 0.0 : allTimePnl.doubleValue() / startBalance.doubleValue() * 100.0; + + BigDecimal investedPnl = invested.subtract(investedCost); + double investedPnlPct = (investedCost.signum() == 0) + ? 0.0 : investedPnl.doubleValue() / investedCost.doubleValue() * 100.0; + + double winrate = (totalTrades == 0) + ? 0.0 : (double) winningTrades / totalTrades * 100.0; + + kpiTotalValue.setText(formatMoney(totalValue)); + setSignedSub(kpiTotalSub, allTimePnl.doubleValue(), allTimePnlPct); + + kpiCashValue.setText(formatMoney(cash)); + + kpiInvestedValue.setText(formatMoney(invested)); + setSignedSub(kpiInvestedSub, investedPnl.doubleValue(), investedPnlPct); + + kpiPnlValue.setText(formatSigned(allTimePnl.doubleValue())); + kpiPnlValue.getStyleClass().removeAll("up", "down"); + kpiPnlValue.getStyleClass().add(allTimePnl.signum() >= 0 ? "up" : "down"); + + kpiTradesValue.setText(String.valueOf(totalTrades)); + kpiTradesSub.setText(String.format("%.0f%% winrate", winrate)); + } + + /** + * Sets a sub-label to "+X.X ($A.B%)" with the appropriate up/down style. + * */ + private void setSignedSub(final Label label, final double absolute, final double pct) { + label.setText(formatSigned(absolute) + " (" + formatSignedPct(pct) + ")"); + label.getStyleClass().removeAll("up", "down"); + label.getStyleClass().add(absolute >= 0 ? "up" : "down"); + } + + /** + * Rebuilds the balance-over-time chart inside the chart pane. Each + * segment is colored green or red depending on whether the value is + * up or down from the previous week. + * */ + private void renderBalanceChart() { + if (balanceChartPane == null) { + return; + } + balanceChartPane.getChildren().clear(); + + double w = balanceChartPane.getWidth(); + double h = balanceChartPane.getHeight(); + if (w <= 0 || h <= 0 || balanceHistory.size() < 2) { + return; + } + + double padL = 36, padR = 10, padT = 10, padB = 22; + double max = balanceHistory.stream().mapToDouble(BigDecimal::doubleValue).max().orElse(1.0); + double min = balanceHistory.stream().mapToDouble(BigDecimal::doubleValue).min().orElse(0.0); + double range = Math.max(max - min, 0.0001); + int n = balanceHistory.size(); + double stepX = (w - padL - padR) / (n - 1); + + // Horizontal grid lines + y-axis labels. + int yTicks = 4; + for (int i = 0; i <= yTicks; i++) { + double y = padT + (h - padT - padB) * (1 - i / (double) yTicks); + Line grid = new Line(padL, y, w - padR, y); + grid.setStroke(Color.rgb(0, 0, 0, 0.08)); + balanceChartPane.getChildren().add(grid); + + double v = min + range * i / yTicks; + Text yLabel = new Text(0, y + 3, "$" + (long) v); + yLabel.getStyleClass().add("stats-chart-axis"); + yLabel.setX(padL - 4 - yLabel.getLayoutBounds().getWidth()); + balanceChartPane.getChildren().add(yLabel); + } + + // Colored line segments. + for (int i = 1; i < n; i++) { + double x1 = padL + (i - 1) * stepX; + double y1 = padT + (h - padT - padB) * (1 - (balanceHistory.get(i - 1).doubleValue() - min) / range); + double x2 = padL + i * stepX; + double y2 = padT + (h - padT - padB) * (1 - (balanceHistory.get(i).doubleValue() - min) / range); + + Line seg = new Line(x1, y1, x2, y2); + boolean up = balanceHistory.get(i).compareTo(balanceHistory.get(i - 1)) >= 0; + seg.setStroke(Color.web(up ? "#27ae60" : "#c0392b")); + seg.setStrokeWidth(2.4); + seg.setStrokeLineCap(StrokeLineCap.ROUND); + balanceChartPane.getChildren().add(seg); + } + + // X-axis labels (week numbers). + for (int i = 0; i < n; i++) { + double x = padL + i * stepX; + Text xLabel = new Text(0, h - 6, String.valueOf(i + 1)); + xLabel.getStyleClass().add("stats-chart-axis"); + xLabel.setX(x - xLabel.getLayoutBounds().getWidth() / 2.0); + balanceChartPane.getChildren().add(xLabel); + } + + // Marker on the last point. + double xLast = padL + (n - 1) * stepX; + double yLast = padT + (h - padT - padB) * (1 - (balanceHistory.getLast().doubleValue() - min) / range); + Circle dot = new Circle(xLast, yLast, 3.5, Color.BLACK); + balanceChartPane.getChildren().add(dot); + } + + /** + * Rebuilds the allocation donut pie and its legend. + * */ + private void renderAllocation() { + if (allocPie == null) { + return; + } + allocPie.getChildren().clear(); + allocLegend.getChildren().clear(); + + BigDecimal invested = sumValue(holdings); + BigDecimal total = invested.add(cash); + if (total.signum() <= 0) { + return; + } + + // Build segments: one per holding, plus cash. + List segments = new ArrayList<>(); + for (int i = 0; i < holdings.size(); i++) { + HoldingData h = holdings.get(i); + if (h.getValue().signum() > 0) { + segments.add(new Segment( + h.getTicker(), + h.getValue().doubleValue(), + PIE_COLORS[i % PIE_COLORS.length] + )); + } + } + if (cash.signum() > 0) { + segments.add(new Segment("cash", cash.doubleValue(), CASH_COLOR)); + } + if (segments.isEmpty()) { + return; + } + + double cx = 55, cy = 55, r = 45; + double startAngle = 90; // start at top + double totalValue = total.doubleValue(); + + if (segments.size() == 1) { + // single slice fills the whole circle + Circle full = new Circle(cx, cy, r, Color.web(segments.getFirst().color)); + allocPie.getChildren().add(full); + } else { + for (Segment seg : segments) { + double sweep = (seg.value / totalValue) * 360.0; + Arc arc = new Arc(cx, cy, r, r, startAngle, -sweep); + arc.setType(ArcType.ROUND); + arc.setFill(Color.web(seg.color)); + allocPie.getChildren().add(arc); + startAngle -= sweep; + } + } + + // Donut hole. + Circle hole = new Circle(cx, cy, 22, Color.rgb(245, 245, 245, 0.95)); + allocPie.getChildren().add(hole); + + // Legend rows, sorted descending by value. + segments.stream() + .sorted(Comparator.comparingDouble(s -> s.value).reversed()) + .forEach(seg -> { + Circle dot = new Circle(5, Color.web(seg.color)); + + Label name = new Label(seg.label); + name.getStyleClass().add("stats-legend-label"); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + Label pct = new Label(String.format("%.1f%%", seg.value / totalValue * 100.0)); + pct.getStyleClass().add("stats-legend-pct"); + + HBox row = new HBox(6, dot, name, spacer, pct); + row.setAlignment(Pos.CENTER_LEFT); + allocLegend.getChildren().add(row); + }); + } + + /** + * Rebuilds the holdings list - one row per ticker, sorted by value + * descending. + * */ + private void renderHoldings() { + if (holdingsList == null) { + return; + } + holdingsList.getChildren().clear(); + + if (holdings.isEmpty()) { + Label empty = new Label("no holdings yet"); + empty.getStyleClass().add("stats-empty"); + holdingsList.getChildren().add(empty); + return; + } + + holdings.stream() + .sorted(Comparator.comparing(HoldingData::getValue).reversed()) + .forEach(h -> holdingsList.getChildren().add(buildHoldingRow(h))); + } + + /** + * Builds a single holdings row. + * */ + private HBox buildHoldingRow(final HoldingData h) { + Label ticker = new Label(h.getTicker()); + ticker.getStyleClass().add("stats-holding-ticker"); + ticker.setMinWidth(60); + + Label shares = new Label(String.format( + "%s \u00D7 avg $%.2f \u2192 now $%.2f", + stripTrailingZeros(h.getShares()), + h.getAvgPrice().doubleValue(), + h.getCurrentPrice().doubleValue() + )); + shares.getStyleClass().add("stats-holding-shares"); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + Label value = new Label(String.format("$%.2f", h.getValue().doubleValue())); + value.getStyleClass().add("stats-holding-value"); + + boolean up = h.getPnl().signum() >= 0; + Label pnl = new Label( + (up ? "+" : "") + String.format("%.1f%%", h.getPnlPct()) + ); + pnl.getStyleClass().addAll("stats-holding-pnl", up ? "up" : "down"); + pnl.setMinWidth(60); + pnl.setAlignment(Pos.CENTER); + + HBox row = new HBox(8, ticker, shares, spacer, value, pnl); + row.setAlignment(Pos.CENTER_LEFT); + row.getStyleClass().add("stats-holding"); + return row; + } + + // ---------- Helpers ---------- + + private static BigDecimal sumValue(final List holdings) { + BigDecimal sum = BigDecimal.ZERO; + for (HoldingData h : holdings) { + sum = sum.add(h.getValue()); + } + return sum; + } + + private static BigDecimal sumCost(final List holdings) { + BigDecimal sum = BigDecimal.ZERO; + for (HoldingData h : holdings) { + sum = sum.add(h.getCost()); + } + return sum; + } + + private static String formatMoney(final BigDecimal v) { + return "$" + (long) v.doubleValue(); + } + + private static String formatSigned(final double v) { + return (v >= 0 ? "+" : "") + "$" + (long) v; + } + + private static String formatSignedPct(final double v) { + return (v >= 0 ? "+" : "") + String.format("%.2f%%", v); + } + + /** + * Strips trailing zeros from a BigDecimal for compact display, e.g. + * {@code 2.00 -> 2}, {@code 2.50 -> 2.5}. + * */ + private static String stripTrailingZeros(final BigDecimal v) { + return v.stripTrailingZeros().toPlainString(); + } + + /** Internal data carrier for a single pie slice. */ + private static final class Segment { + final String label; + final double value; + final String color; + + Segment(final String label, final double value, final String color) { + this.label = label; + this.value = value; + this.color = color; + } + } +} diff --git a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/topbar/TopBarController.java b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/topbar/TopBarController.java index 9909b1d..86c5d2d 100644 --- a/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/topbar/TopBarController.java +++ b/src/main/java/edu/ntnu/idi/idatt2003/g40/mappe/view/widgets/topbar/TopBarController.java @@ -12,24 +12,38 @@ public class TopBarController extends ViewController { /** * Whether the market is currently the active center-view. * - *

When true, the quit/back button returns to the dashboard instead - * of exiting to the main menu.

- * */ + *

+ * When true, the quit/back button returns to the dashboard instead + * of exiting to the main menu. + *

+ */ private boolean inMarketView = false; + /** + * Whether the stats screen is currently the active center-view. + * + *

+ * When true, the quit/back button returns to the dashboard instead + * of exiting to the main menu. + *

+ */ + private boolean inStatsView = false; + /** * Whether the transactions screen is currently the active center-view. * - *

When true, the quit/back button returns to the dashboard instead - * of exiting to the main menu.

- * */ + *

+ * When true, the quit/back button returns to the dashboard instead + * of exiting to the main menu. + *

+ */ private boolean inTransactionsView = false; /** * {@inheritDoc}. */ public TopBarController(final TopBarView viewElement, final EventManager eventManager) - throws IllegalArgumentException { + throws IllegalArgumentException { super(viewElement, eventManager); } @@ -45,34 +59,43 @@ protected void initInteractions() { } /** - * Wires the top bar buttons to swap between the dashboard and market - * center-views inside the in-game view. + * Wires the top bar buttons to swap between the dashboard, stats, + * market and transactions center-views inside the in-game view. * - *

After this method is called:

+ *

+ * After this method is called: + *

*
    - *
  • {@code STATS} swaps the center to the dashboard and resets the - * quit/back button to "Quit".
  • - *
  • {@code MARKET} swaps the center to the market and switches the - * button to "Back".
  • - *
  • {@code EXIT} returns to the dashboard if the market is open, - * otherwise to the main menu.
  • + *
  • {@code STATS} swaps the center to the stats view and switches + * the quit/back button to "Back".
  • + *
  • {@code MARKET} swaps the center to the market and switches the + * button to "Back".
  • + *
  • {@code TRANSACTIONS} swaps the center to the transactions view + * and switches the button to "Back".
  • + *
  • {@code EXIT} returns to the dashboard if any sub-view is open, + * otherwise to the main menu.
  • *
* - * @param centerSwitcher callback that swaps the center-view (typically - * {@code inGameView::changeCenterView}). - * @param dashboardCenter root pane of the dashboard widget. - * @param marketCenter root pane of the market widget. - * @param transactionsCenter root pane of the transactions' widget. - * */ + * @param centerSwitcher callback that swaps the center-view (typically + * {@code inGameView::changeCenterView}). + * @param dashboardCenter root pane of the dashboard widget (home). + * @param marketCenter root pane of the market widget. + * @param statsCenter root pane of the stats widget. + * @param transactionsCenter root pane of the transactions widget. + * @param onTransactionUpdate callback invoked when entering transactions. + */ public void setMarketIntegration(final Consumer centerSwitcher, - final Node dashboardCenter, - final Node marketCenter, - final Node transactionsCenter, final Runnable onTransactionUpdate) { + final Node dashboardCenter, + final Node marketCenter, + final Node statsCenter, + final Node transactionsCenter, + final Runnable onTransactionUpdate) { getViewElement().setOnAction(TopBarActions.EXIT, () -> { - if (inMarketView || inTransactionsView) { + if (inMarketView || inStatsView || inTransactionsView) { centerSwitcher.accept(dashboardCenter); getViewElement().setQuitText("Quit"); inMarketView = false; + inStatsView = false; inTransactionsView = false; } else { changeScene(ViewEnum.MAIN_MENU); @@ -80,9 +103,10 @@ public void setMarketIntegration(final Consumer centerSwitcher, }); getViewElement().setOnAction(TopBarActions.STATS, () -> { - centerSwitcher.accept(dashboardCenter); - getViewElement().setQuitText("Quit"); + centerSwitcher.accept(statsCenter); + getViewElement().setQuitText("Back"); inMarketView = false; + inStatsView = true; inTransactionsView = false; }); @@ -90,6 +114,7 @@ public void setMarketIntegration(final Consumer centerSwitcher, centerSwitcher.accept(marketCenter); getViewElement().setQuitText("Back"); inMarketView = true; + inStatsView = false; inTransactionsView = false; }); @@ -98,7 +123,8 @@ public void setMarketIntegration(final Consumer centerSwitcher, onTransactionUpdate.run(); getViewElement().setQuitText("Back"); inMarketView = false; + inStatsView = false; inTransactionsView = true; }); } -} +} \ No newline at end of file diff --git a/src/main/resources/styles.css b/src/main/resources/styles.css index 05ec5ed..71fe005 100644 --- a/src/main/resources/styles.css +++ b/src/main/resources/styles.css @@ -1,6 +1,5 @@ .root { -fx-font-family: "Aptos"; - -fx-text-fill: black; } /* Container for the buttons */ @@ -149,9 +148,11 @@ .title-text { -fx-font-size: 100px; } + /* ------------- TOP BAR ------------- */ .top-bar { - -fx-background-color: rgba(69, 69, 69, 0.7); /* Nice */ + -fx-background-color: rgba(69, 69, 69, 0.7); + /* Nice */ -fx-margin: 10; -fx-padding: 20 1%; -fx-min-width: 100%; @@ -165,7 +166,7 @@ -fx-font-style: italic; -fx-background-color: #f0f0f0; -fx-background-radius: 10; - -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 5, 0, 0, 2); + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.2), 5, 0, 0, 2); } .top-bar-menu-button:hover { @@ -230,42 +231,25 @@ -fx-pref-width: 100; -fx-max-width: 100; -fx-font-weight: bold; - -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 2, 0, 0, 1); + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.2), 2, 0, 0, 1); -fx-cursor: hand; } /* --------------- IN GAME VIEW ------------- */ -.complete-market-sidebar { - -fx-border-color: #000000; - -fx-border-width: 0 2 0 0; - -fx-border-style: solid; - -fx-padding: 20; - -fx-spacing: 10; -} - .market-sidebar { - -fx-padding: 10; - -fx-spacing: 10; - -fx-alignment: TOP_CENTER; - - -fx-min-width: 150; - -fx-pref-width: 150; - + -fx-padding: 20; } .stock-button { -fx-background-color: #e0e0e0; - -fx-alignment: CENTER_LEFT; - -fx-padding: 10 15 10 15; -fx-font-weight: bold; -fx-font-style: italic; -fx-text-fill: black; - -fx-font-size: 13; - + -fx-font-size: 24; } -.stock-button:hover { +.stock-button : hover { -fx-cursor: hand; -fx-scale-x: 1.05; -fx-scale-y: 1.05; @@ -277,7 +261,7 @@ -fx-padding: 10 40; -fx-font-size: 24; -fx-font-weight: bold; - -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 5, 0, 0, 2); + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.2), 5, 0, 0, 2); } .buy-button { @@ -289,14 +273,10 @@ -fx-text-fill: black; -fx-font-style: italic; -fx-font-weight: bold; - - -fx-min-height: 30; - -fx-pref-height: 45; - -fx-max-height: 60; - - -fx-min-width: 60; - -fx-pref-width: 80; - -fx-max-width: 200; + -fx-min-height: 60; + -fx-pref-height: 80; + -fx-min-width: 150; + -fx-max-width: 250; } .sell-button { @@ -306,129 +286,19 @@ -fx-text-fill: black; -fx-font-style: italic; -fx-font-weight: bold; - - -fx-min-height: 30; - -fx-pref-height: 45; - -fx-max-height: 60; - - -fx-min-width: 60; - -fx-pref-width: 80; - -fx-max-width: 200; -} - -.buy-button:hover, .sell-button:hover { - -fx-cursor: hand; - -fx-scale-x: 1.05; - -fx-scale-y: 1.05; -} -.qtyBtn { - -fx-background-color: rgba(140, 140, 140, 0.6); - - -fx-background-radius: 50; - - -fx-text-fill: white; - -fx-font-weight: bold; - -fx-font-size: 14px; - - -fx-min-width: 60; - -fx-pref-width: 75; - -fx-max-width: 90; - - -fx-min-height: 20; - -fx-pref-height: 30; - -fx-max-height: 40; + -fx-min-height: 60; + -fx-pref-height: 80; + -fx-min-width: 150; + -fx-max-width: 250; } -.qtyBtn:hover { +.buy-button:hover, +.sell-button:hover { -fx-cursor: hand; -fx-scale-x: 1.05; -fx-scale-y: 1.05; } -.qtyTextField { - -fx-alignment: CENTER; - - -fx-min-width: 50; - -fx-pref-width: 75; - -fx-max-width: 100; - - -fx-min-height: 30; - -fx-pref-height: 45; - -fx-max-height: 60; -} - -.scroll-pane { - -fx-background-color: transparent; - -fx-border-color: transparent; - -fx-padding: 0; -} - -.scroll-pane > .viewport { - -fx-background-color: transparent; -} - -.combo-box { - -fx-background-color: #f4f4f4; - -fx-border-color: #d1d1d1; - -fx-border-radius: 5; - -fx-background-radius: 5; -} - -.combo-box .list-cell { - -fx-text-fill: #333333; -} - -.dashboard-sidebar-scrollPane { - -fx-min-width: 150; -} - -.dashboard-mainContent-VBox { - -fx-padding: 30; - -fx-spacing: 20; - -fx-alignment: TOP_LEFT; -} - -.dashboard-chart { - -fx-min-height: 200; - -fx-pref-height: 650; -} - -.dashboard-header { - -fx-spacing: 20; -} - -.dashboard-stockIdentity { - -fx-spacing: 5; -} - -.dashboard-priceStats { - -fx-spacing: 5; -} - -.dashboard-lowhigh { - -fx-spacing: 10; -} - -.dashboard-grid { - -fx-alignment: CENTER; -} - -.dashboard-leftQty { - -fx-alignment: CENTER_RIGHT; -} - -.dashboard-rightQty { - -fx-alignment: CENTER_LEFT; -} - -.dashboard-qtySection { - -fx-alignment: CENTER; -} - -.dashboard-tradeBtns { - -fx-alignment: CENTER; -} - /* ------------- MARKET VIEW ------------- */ /* Root pane fills the center area with a light translucent background. */ .market-container { @@ -461,7 +331,7 @@ -fx-font-weight: bold; -fx-font-size: 13; -fx-cursor: hand; - -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 2, 0, 0, 1); + -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.1), 2, 0, 0, 1); } .market-filter-btn.active { @@ -477,7 +347,7 @@ -fx-padding: 0; } -.market-scroll > .viewport { +.market-scroll>.viewport { -fx-background-color: transparent; } @@ -565,85 +435,156 @@ -fx-padding: 40; } -/* ---------------- TRANSACTIONS VIEW ---------------- */ -.transactions-root { - -fx-spacing: 20; - -fx-padding: 15; - -fx-alignment: TOP_CENTER; +/* ------------- STATS VIEW ------------- */ +/* Root pane fills the center area with a light translucent background. */ +.stats-container { + -fx-background-color: rgba(245, 245, 245, 0.95); } -.transactions-root .label { +/* Generic panel (chart, allocation, holdings). */ +.stats-panel { + -fx-background-color: rgba(255, 255, 255, 0.75); + -fx-background-radius: 14; + -fx-border-color: rgba(0, 0, 0, 0.12); + -fx-border-radius: 14; + -fx-padding: 10 14; +} + +.stats-panel-title { + -fx-font-style: italic; + -fx-font-weight: bold; + -fx-font-size: 14; + -fx-text-fill: #333333; + -fx-border-color: transparent transparent rgba(0, 0, 0, 0.15) transparent; + -fx-border-width: 0 0 1 0; + -fx-padding: 0 0 4 0; +} + +/* KPI tiles in the top strip. */ +.stats-kpi { + -fx-background-color: rgba(255, 255, 255, 0.75); + -fx-background-radius: 14; + -fx-border-color: rgba(0, 0, 0, 0.12); + -fx-border-radius: 14; + -fx-padding: 8 12; +} + +.stats-kpi-label { + -fx-font-style: italic; + -fx-font-weight: bold; + -fx-font-size: 11; + -fx-text-fill: #555555; +} + +.stats-kpi-value { + -fx-font-style: italic; + -fx-font-weight: bold; + -fx-font-size: 20; -fx-text-fill: black; } -.transactions-searchBar { - -fx-spacing: 10; - -fx-alignment: CENTER_LEFT; - -fx-border-color: #000000; - -fx-border-width: 1.5; - -fx-background-color: #E0E0E0; - -fx-padding: 10; +.stats-kpi-value.up { + -fx-text-fill: #27ae60; } -.transactions-searchLabel { - -fx-font-size: 14; +.stats-kpi-value.down { + -fx-text-fill: #c0392b; } -.transactions-searchField { - -fx-background-color: white; - -fx-border-color: #CCCCCC; +.stats-kpi-sub { + -fx-font-style: italic; + -fx-font-weight: bold; + -fx-font-size: 11; + -fx-text-fill: #555555; } -.transactions-weekField { - -fx-pref-width: 75px; - -fx-min-width: 75px; - -fx-background-color: white; - -fx-border-color: #000000; - -fx-border-width: 1.5px; - -fx-alignment: CENTER; +.stats-kpi-sub.up { + -fx-text-fill: #27ae60; +} + +.stats-kpi-sub.down { + -fx-text-fill: #c0392b; } -.transactions-weekField .list-cell { +/* Balance-over-time chart axis labels. */ +.stats-chart-axis { + -fx-font-size: 9; + -fx-font-style: italic; + -fx-fill: #555555; +} + +/* Allocation legend rows. */ +.stats-legend-label { + -fx-font-style: italic; + -fx-font-weight: bold; + -fx-font-size: 12; -fx-text-fill: black; +} + +.stats-legend-pct { + -fx-font-style: italic; + -fx-font-size: 12; + -fx-text-fill: #555555; +} + +.stats-legend-scroll, +.stats-holdings-scroll { + -fx-background: transparent; + -fx-background-color: transparent; + -fx-padding: 0; +} + +.stats-legend-scroll>.viewport, +.stats-holdings-scroll>.viewport { + -fx-background-color: transparent; +} + +/* A single row in the holdings list. */ +.stats-holding { + -fx-background-color: rgba(255, 255, 255, 0.6); + -fx-background-radius: 10; + -fx-padding: 6 8; +} + +.stats-holding-ticker { + -fx-font-style: italic; -fx-font-weight: bold; - -fx-alignment: center; + -fx-font-size: 16; + -fx-text-fill: black; } -.transactions-transactionCard { - -fx-spacing: 15; - -fx-padding: 25; - -fx-alignment: TOP_CENTER; - -fx-pref-width: 260; - -fx-pref-height: 320; - -fx-background-color: #D6D6D6; - -fx-background-radius: 35; - -fx-border-color: #000000; - -fx-border-radius: 35; - -fx-border-width: 1.5; +.stats-holding-shares { + -fx-font-style: italic; + -fx-font-size: 11; + -fx-text-fill: #555555; } -.transactions-typeLabel { - -fx-font-size: 28; +.stats-holding-value { + -fx-font-style: italic; -fx-font-weight: bold; + -fx-font-size: 13; + -fx-text-fill: black; } -.transactions-infoBox { - -fx-spacing: 8; - -fx-alignment: CENTER; +.stats-holding-pnl { + -fx-font-style: italic; + -fx-font-size: 12; + -fx-padding: 2 8; + -fx-background-radius: 8; + -fx-text-fill: white; } -.transactions-cardText { - -fx-font-size: 14; +.stats-holding-pnl.up { + -fx-background-color: #27ae60; } -.transactions-cardsContainer { - -fx-spacing: 25; - -fx-alignment: CENTER_LEFT; - -fx-padding: 10; +.stats-holding-pnl.down { + -fx-background-color: #c0392b; } -.transactions-scrollPane { - -fx-fit-to-height: true; - -fx-background: transparent; - -fx-background-color: transparent; +/* Empty-state label shown when there are no holdings. */ +.stats-empty { + -fx-font-style: italic; + -fx-text-fill: #555555; + -fx-padding: 20; } \ No newline at end of file