Merge branch 'feature/pie-chart' into 'main'

Pie chart for nutrients

See merge request cse1105/2025-2026/teams/csep-team-76!94
This commit is contained in:
Natalia Cholewa 2026-01-23 21:44:30 +01:00
commit b942862dc9
12 changed files with 286 additions and 2 deletions

View file

@ -0,0 +1,234 @@
package client.scenes.nutrition;
import client.utils.LocaleAware;
import client.utils.LocaleManager;
import com.google.inject.Inject;
import commons.FormalIngredient;
import commons.Ingredient;
import commons.Recipe;
import commons.VagueIngredient;
import javafx.fxml.FXML;
import javafx.scene.chart.PieChart;
import java.util.List;
import java.util.Objects;
import java.util.logging.Logger;
public class NutritionPieChartCtrl implements LocaleAware {
/**
* Nutrition info for a recipe or an ingredient.
*
* @param protein The protein this recipe/ingredient has
* @param fat The fat this recipe/ingredient has
* @param carbs The carbs this recipe/ingredient has
*/
private record CompleteNutritionInfo(
double protein,
double fat,
double carbs
) {
/**
* Create a new {@link CompleteNutritionInfo} with zeroed values.
*
* @return A {@link CompleteNutritionInfo} with zeroed values.
*/
static CompleteNutritionInfo zero() {
return new CompleteNutritionInfo(0.0, 0.0, 0.0);
}
/**
* Check whether this instance has all values set to zero.
*
* @return Whether all values (protein, carbs, fat) are zero.
*/
public boolean isZero() {
return this.protein() == 0.0
&& this.carbs() == 0.0
&& this.fat() == 0.0;
}
/**
* Scale this object by an amount. Multiplies all values by amount.
*
* @param amount The amount to scale it by.
* @return The newly scaled nutrition info.
*/
public CompleteNutritionInfo scaled(double amount) {
return new CompleteNutritionInfo(
this.protein() * amount,
this.fat() * amount,
this.carbs() * amount
);
}
/**
* Add another nutrition info object to this object.
*
* @param rhs The nutrition info object to add.
* @return A new nutrition info object with the sum of both objects' nutrients.
*/
public CompleteNutritionInfo add(CompleteNutritionInfo rhs) {
return new CompleteNutritionInfo(
this.protein() + rhs.protein(),
this.fat() + rhs.fat(),
this.carbs() + rhs.carbs()
);
}
}
private final LocaleManager localeManager;
private final Logger logger = Logger.getLogger(NutritionPieChartCtrl.class.getName());
@FXML
PieChart pieChart;
private Recipe recipe;
@Inject
NutritionPieChartCtrl(
LocaleManager manager
) {
this.localeManager = manager;
}
/**
* Get the data for this pie chart based on the current recipe.
* <p>
* This accumulates all the nutrients (properly scaled) from all ingredients in this recipe
* and returns a summary with data points.
* </p>
* <p>
* The list will be of length 3 if any nutrients are added, or 0 if all the nutrients
* sum up to be 0.
* </p>
* @return The data for this pie chart, as a labeled list of data.
*/
private List<PieChart.Data> getPieChartData() {
CompleteNutritionInfo info = this.recipe
.getIngredients()
.stream()
.map(ri -> {
Ingredient i = ri.getIngredient();
logger.info("Mapping ingredient " + i.toString());
switch (ri) {
case FormalIngredient fi -> {
return new CompleteNutritionInfo(
i.getProteinPer100g(),
i.getFatPer100g(),
i.getCarbsPer100g()
)
.scaled(fi.getBaseAmount());
}
case VagueIngredient vi -> {
return new CompleteNutritionInfo(
i.getProteinPer100g(),
i.getFatPer100g(),
i.getCarbsPer100g()
)
.scaled(vi.getBaseAmount());
}
default -> {
return null;
}
}
})
.filter(Objects::nonNull)
.reduce(CompleteNutritionInfo::add)
.orElseGet(CompleteNutritionInfo::zero);
this.logger.info( "Updated data: " + info.toString() );
if (info.isZero()) {
return List.of();
}
return List.of(
new PieChart.Data(
this.getLocaleString("menu.nutrition.protein"),
info.protein()
),
new PieChart.Data(
this.getLocaleString("menu.nutrition.fat"),
info.fat()
),
new PieChart.Data(
this.getLocaleString("menu.nutrition.carbs"),
info.carbs()
)
);
}
/**
* Set the current recipe to be displayed in the pie chart.
* The pie chart will be refreshed.
* <p>
* The pie chart will disappear if all the nutrients in the recipe
* are zero.
* </p>
* @param recipe The recipe to display.
*/
public void setRecipe(Recipe recipe) {
this.recipe = recipe;
this.refresh();
}
@Override
public void updateText() {
List<PieChart.Data> data = this.pieChart.getData();
if (data.isEmpty()) return;
final int EXPECTED_DATA_SIZE = 3;
if (data.size() != EXPECTED_DATA_SIZE) return;
data.get(0).setName(this.getLocaleString("menu.nutrition.protein"));
data.get(1).setName(this.getLocaleString("menu.nutrition.fat"));
final int TWO = 2;
data.get(TWO).setName(this.getLocaleString("menu.nutrition.carbs"));
}
@Override
public LocaleManager getLocaleManager() {
return this.localeManager;
}
/**
* Refresh the data in this pie chart.
*/
@SuppressWarnings("checkstyle:MagicNumber")
public void refresh() {
if (this.recipe == null) {
this.pieChart.setVisible(false);
logger.info("Refreshing pie chart with no recipe");
return;
}
logger.info("Refreshing pie chart with recipe");
this.pieChart.setVisible(true);
if (this.pieChart.getData().isEmpty()) {
this.pieChart.getData().setAll(
this.getPieChartData()
);
} else {
List<PieChart.Data> fresh = this.getPieChartData();
List<PieChart.Data> data = this.pieChart.getData();
if (fresh.isEmpty()) {
data.clear();
return;
}
data.get(0).setPieValue(fresh.get(0).getPieValue());
data.get(1).setPieValue(fresh.get(1).getPieValue());
final int TWO = 2;
data.get(TWO).setPieValue(fresh.get(TWO).getPieValue());
}
}
public void initializeComponents() {
final double START_ANGLE = 60.0;
this.pieChart.setClockwise(true);
this.pieChart.setStartAngle(START_ANGLE);
this.refresh();
}
}

View file

@ -2,6 +2,7 @@ package client.scenes.recipe;
import client.exception.UpdateException;
import client.scenes.FoodpalApplicationCtrl;
import client.scenes.nutrition.NutritionPieChartCtrl;
import client.service.ShoppingListService;
import client.utils.Config;
import client.utils.ConfigService;
@ -106,6 +107,9 @@ public class RecipeDetailCtrl implements LocaleAware {
@FXML
private ComboBox<String> langSelector;
@FXML
private NutritionPieChartCtrl pieChartController;
private ListView<Recipe> getParentRecipeList() {
return this.appCtrl.recipeList;
}
@ -185,11 +189,14 @@ public class RecipeDetailCtrl implements LocaleAware {
// expose the scaled view to list controllers
this.ingredientListController.refetchFromRecipe(this.recipeView.getScaled());
this.stepListController.refetchFromRecipe(this.recipeView.getScaled());
this.pieChartController.setRecipe(recipe);
return;
}
this.ingredientListController.refetchFromRecipe(recipe);
this.stepListController.refetchFromRecipe(recipe);
this.pieChartController.setRecipe(recipe);
}
/**

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.chart.PieChart?>
<PieChart fx:controller="client.scenes.nutrition.NutritionPieChartCtrl" fx:id="pieChart" xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" />

View file

@ -42,6 +42,11 @@
<!-- Preparation -->
<fx:include source="RecipeStepList.fxml" fx:id="stepList"
VBox.vgrow="ALWAYS" maxWidth="Infinity" />
<HBox spacing="20">
<VBox spacing="0">
<Label fx:id="inferredServeSizeLabel" />
<Label fx:id="inferredKcalLabel" />
</VBox>
<fx:include fx:id="pieChart" source="../nutrition/NutritionPieChart.fxml" />
</HBox>
</VBox>

View file

@ -38,6 +38,10 @@ menu.search=Search...
menu.label.selected-langs=Languages
menu.nutrition.carbs=Carbohydrates
menu.nutrition.protein=Protein
menu.nutrition.fat=Fat
lang.en.display=English
lang.nl.display=Dutch
lang.pl.display=Polish

View file

@ -40,6 +40,10 @@ menu.label.selected-langs=Languages
menu.shopping.title=Shopping list
menu.nutrition.carbs=Carbohydrates
menu.nutrition.protein=Protein
menu.nutrition.fat=Fat
lang.en.display=English
lang.nl.display=Dutch
lang.pl.display=Polish

View file

@ -38,6 +38,10 @@ menu.label.selected-langs=Talen
menu.shopping.title=Boodschappenlijst
menu.nutrition.carbs=Koolhydraten
menu.nutrition.protein=Eiwitten
menu.nutrition.fat=Vetten
menu.search=Zoeken...
lang.en.display=Engels
lang.nl.display=Nederlands

View file

@ -40,6 +40,10 @@ menu.label.selected-langs=Języki
menu.shopping.title=Lista zakupów
menu.nutrition.carbs=W?glowodany
menu.nutrition.protein=Bia?ka
menu.nutrition.fat=T?uszcze
lang.en.display=Inglisz
lang.nl.display=Holenderski
lang.pl.display=Polski

View file

@ -40,6 +40,10 @@ menu.label.selected-langs=toki wile
menu.shopping.title=ijo wile mani mute
menu.nutrition.carbs=kipisi moku suwi
menu.nutrition.protein=kipisi moku walo
menu.nutrition.fat=kipisi moku suli
lang.en.display=toki Inli
lang.nl.display=toki Netelan
lang.pl.display=toki Posuka

View file

@ -40,6 +40,10 @@ menu.label.selected-langs=Diller
menu.shopping.title=Al??veri? listesi
menu.nutrition.carbs=Karbonhidratlar
menu.nutrition.protein=Protein
menu.nutrition.fat=Ya?
lang.en.display=English
lang.nl.display=Nederlands
lang.pl.display=Polski

View file

@ -29,6 +29,10 @@ menu.search=搜索
menu.label.selected-langs=语言
menu.nutrition.carbs=?????
menu.nutrition.protein=???
menu.nutrition.fat=??
lang.en.display=English
lang.nl.display=Nederlands
lang.pl.display=Polski

View file

@ -29,6 +29,10 @@ menu.search=搜索
menu.label.selected-langs=語言
menu.nutrition.carbs=?????
menu.nutrition.protein=???
menu.nutrition.fat=??
lang.en.display=English
lang.nl.display=Nederlands
lang.pl.display=Polski