feat: pie chart for nutrients

This commit is contained in:
Natalia Cholewa 2026-01-23 13:48:48 +01:00
commit c1301dad28
4 changed files with 230 additions and 2 deletions

View file

@ -0,0 +1,210 @@
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(
"Protein",
info.protein()
),
new PieChart.Data(
"Fat",
info.fat()
),
new PieChart.Data(
"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() {
}
@Override
public LocaleManager getLocaleManager() {
return this.localeManager;
}
/**
* Refresh the data in this pie chart.
*/
private 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);
this.pieChart.getData().setAll(
this.getPieChartData()
);
}
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.exception.UpdateException;
import client.scenes.FoodpalApplicationCtrl; import client.scenes.FoodpalApplicationCtrl;
import client.scenes.nutrition.NutritionPieChartCtrl;
import client.service.ShoppingListService; import client.service.ShoppingListService;
import client.utils.Config; import client.utils.Config;
import client.utils.ConfigService; import client.utils.ConfigService;
@ -106,6 +107,9 @@ public class RecipeDetailCtrl implements LocaleAware {
@FXML @FXML
private ComboBox<String> langSelector; private ComboBox<String> langSelector;
@FXML
private NutritionPieChartCtrl pieChartController;
private ListView<Recipe> getParentRecipeList() { private ListView<Recipe> getParentRecipeList() {
return this.appCtrl.recipeList; return this.appCtrl.recipeList;
} }
@ -185,11 +189,14 @@ public class RecipeDetailCtrl implements LocaleAware {
// expose the scaled view to list controllers // expose the scaled view to list controllers
this.ingredientListController.refetchFromRecipe(this.recipeView.getScaled()); this.ingredientListController.refetchFromRecipe(this.recipeView.getScaled());
this.stepListController.refetchFromRecipe(this.recipeView.getScaled()); this.stepListController.refetchFromRecipe(this.recipeView.getScaled());
this.pieChartController.setRecipe(recipe);
return; return;
} }
this.ingredientListController.refetchFromRecipe(recipe); this.ingredientListController.refetchFromRecipe(recipe);
this.stepListController.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 --> <!-- Preparation -->
<fx:include source="RecipeStepList.fxml" fx:id="stepList" <fx:include source="RecipeStepList.fxml" fx:id="stepList"
VBox.vgrow="ALWAYS" maxWidth="Infinity" /> VBox.vgrow="ALWAYS" maxWidth="Infinity" />
<Label fx:id="inferredServeSizeLabel" /> <HBox spacing="20">
<Label fx:id="inferredKcalLabel" /> <VBox spacing="0">
<Label fx:id="inferredServeSizeLabel" />
<Label fx:id="inferredKcalLabel" />
</VBox>
<fx:include fx:id="pieChart" source="../nutrition/NutritionPieChart.fxml" />
</HBox>
</VBox> </VBox>