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