diff --git a/examples/food.json b/examples/food.json new file mode 100644 index 000000000..d64d2f1ad --- /dev/null +++ b/examples/food.json @@ -0,0 +1,1944 @@ +{ + "code": "5060292302201", + "product": { + "_id": "5060292302201", + "_keywords": [ + "snack", + "food", + "oil", + "anything", + "potato", + "appetizer", + "artificial", + "plant-based", + "cereal", + "and", + "in", + "popchip", + "preservative", + "barbeque", + "vegetarian", + "sunflower", + "chip", + "frie", + "potatoe", + "no", + "crisp", + "beverage", + "salty" + ], + "added_countries_tags": [], + "additives_debug_tags": [], + "additives_n": 2, + "additives_old_n": 2, + "additives_old_tags": [ + "en:e330", + "en:e160c" + ], + "additives_original_tags": [ + "en:e330", + "en:e160c" + ], + "additives_prev_original_tags": [ + "en:e330", + "en:e160c" + ], + "additives_tags": [ + "en:e160c", + "en:e330" + ], + "additives_tags_n": null, + "allergens": "en:milk", + "allergens_debug_tags": [], + "allergens_from_ingredients": "en:milk, milk", + "allergens_from_user": "(en) en:milk", + "allergens_hierarchy": [ + "en:milk" + ], + "allergens_tags": [ + "en:milk" + ], + "amino_acids_prev_tags": [], + "amino_acids_tags": [], + "brands": "Popchips", + "brands_debug_tags": [], + "brands_tags": [ + "popchips" + ], + "carbon_footprint_from_known_ingredients_debug": "en:potato 54% x 0.6 = 32.4 g - ", + "carbon_footprint_percent_of_known_ingredients": 54, + "categories": "Plant-based foods and beverages, Plant-based foods, Snacks, Cereals and potatoes, Salty snacks, Appetizers, Chips and fries, Crisps, Potato crisps, Potato crisps in sunflower oil", + "categories_hierarchy": [ + "en:plant-based-foods-and-beverages", + "en:plant-based-foods", + "en:snacks", + "en:cereals-and-potatoes", + "en:salty-snacks", + "en:appetizers", + "en:chips-and-fries", + "en:crisps", + "en:potato-crisps", + "en:potato-crisps-in-sunflower-oil" + ], + "categories_lc": "en", + "categories_old": "Plant-based foods and beverages, Plant-based foods, Snacks, Cereals and potatoes, Salty snacks, Appetizers, Chips and fries, Crisps, Potato crisps, Potato crisps in sunflower oil", + "categories_properties": { + "agribalyse_food_code:en": "4004", + "ciqual_food_code:en": "4004" + }, + "categories_properties_tags": [ + "all-products", + "categories-known", + "agribalyse-food-code-4004", + "agribalyse-food-code-known", + "agribalyse-proxy-food-code-unknown", + "ciqual-food-code-4004", + "ciqual-food-code-known", + "agribalyse-known", + "agribalyse-4004" + ], + "categories_tags": [ + "en:plant-based-foods-and-beverages", + "en:plant-based-foods", + "en:snacks", + "en:cereals-and-potatoes", + "en:salty-snacks", + "en:appetizers", + "en:chips-and-fries", + "en:crisps", + "en:potato-crisps", + "en:potato-crisps-in-sunflower-oil" + ], + "category_properties": { + "ciqual_food_name:en": "Potato crisps", + "ciqual_food_name:fr": "Chips de pommes de terre, standard" + }, + "checkers_tags": [], + "ciqual_food_name_tags": [ + "potato-crisps" + ], + "cities_tags": [], + "code": "5060292302201", + "codes_tags": [ + "code-13", + "5060292302xxx", + "506029230xxxx", + "50602923xxxxx", + "5060292xxxxxx", + "506029xxxxxxx", + "50602xxxxxxxx", + "5060xxxxxxxxx", + "506xxxxxxxxxx", + "50xxxxxxxxxxx", + "5xxxxxxxxxxxx" + ], + "compared_to_category": "en:potato-crisps-in-sunflower-oil", + "complete": 0, + "completeness": 0.8875, + "correctors_tags": [ + "tacite", + "tacite-mass-editor", + "yuka.VjQwdU5yUUlpdmxUbjhWa3BFenc4ZGt1NDVLUFZtNm9NdWdOSWc9PQ", + "openfoodfacts-contributors", + "swipe-studio", + "yuka.sY2b0xO6T85zoF3NwEKvllZnctbb-gn-LDr4mHzUyem0FYPXMO5by7b5NKg", + "kiliweb", + "packbot", + "foodless", + "yuka.sY2b0xO6T85zoF3NwEKvlmBZVPXu-gnlBU3miFTQ-NeSIbDaMdUtu4fLGas" + ], + "countries": "France, United Kingdom", + "countries_debug_tags": [], + "countries_hierarchy": [ + "en:france", + "en:united-kingdom" + ], + "countries_lc": "en", + "countries_tags": [ + "en:france", + "en:united-kingdom" + ], + "created_t": 1433338177, + "creator": "kyzh", + "data_quality_bugs_tags": [], + "data_quality_errors_tags": [], + "data_quality_info_tags": [ + "en:packaging-data-incomplete", + "en:ingredients-percent-analysis-ok", + "en:carbon-footprint-from-known-ingredients-but-not-from-meat-or-fish", + "en:ecoscore-extended-data-computed", + "en:ecoscore-extended-data-less-precise-than-agribalyse", + "en:food-groups-1-known", + "en:food-groups-2-known", + "en:food-groups-3-unknown" + ], + "data_quality_tags": [ + "en:packaging-data-incomplete", + "en:ingredients-percent-analysis-ok", + "en:carbon-footprint-from-known-ingredients-but-not-from-meat-or-fish", + "en:ecoscore-extended-data-computed", + "en:ecoscore-extended-data-less-precise-than-agribalyse", + "en:food-groups-1-known", + "en:food-groups-2-known", + "en:food-groups-3-unknown", + "en:nutrition-value-very-low-for-category-energy", + "en:nutrition-value-very-low-for-category-fat", + "en:nutrition-value-very-low-for-category-carbohydrates", + "en:nutrition-value-very-high-for-category-sugars", + "en:ecoscore-origins-of-ingredients-origins-are-100-percent-unknown", + "en:ecoscore-production-system-no-label" + ], + "data_quality_warnings_tags": [ + "en:nutrition-value-very-low-for-category-energy", + "en:nutrition-value-very-low-for-category-fat", + "en:nutrition-value-very-low-for-category-carbohydrates", + "en:nutrition-value-very-high-for-category-sugars", + "en:ecoscore-origins-of-ingredients-origins-are-100-percent-unknown", + "en:ecoscore-production-system-no-label" + ], + "data_sources": "App - yuka, Apps, App - Horizon", + "data_sources_tags": [ + "app-yuka", + "apps", + "app-horizon" + ], + "debug_param_sorted_langs": [ + "en", + "fr" + ], + "ecoscore_data": { + "adjustments": { + "origins_of_ingredients": { + "aggregated_origins": [ + { + "origin": "en:unknown", + "percent": 100 + } + ], + "epi_score": 0, + "epi_value": -5, + "origins_from_origins_field": [ + "en:unknown" + ], + "transportation_scores": { + "ad": 0, + "al": 0, + "at": 0, + "ax": 0, + "ba": 0, + "be": 0, + "bg": 0, + "ch": 0, + "cy": 0, + "cz": 0, + "de": 0, + "dk": 0, + "dz": 0, + "ee": 0, + "eg": 0, + "es": 0, + "fi": 0, + "fo": 0, + "fr": 0, + "gg": 0, + "gi": 0, + "gr": 0, + "hr": 0, + "hu": 0, + "ie": 0, + "il": 0, + "im": 0, + "is": 0, + "it": 0, + "je": 0, + "lb": 0, + "li": 0, + "lt": 0, + "lu": 0, + "lv": 0, + "ly": 0, + "ma": 0, + "mc": 0, + "md": 0, + "me": 0, + "mk": 0, + "mt": 0, + "nl": 0, + "no": 0, + "pl": 0, + "ps": 0, + "pt": 0, + "ro": 0, + "rs": 0, + "se": 0, + "si": 0, + "sj": 0, + "sk": 0, + "sm": 0, + "sy": 0, + "tn": 0, + "tr": 0, + "ua": 0, + "uk": 0, + "us": 0, + "va": 0, + "world": 0, + "xk": 0 + }, + "transportation_values": { + "ad": 0, + "al": 0, + "at": 0, + "ax": 0, + "ba": 0, + "be": 0, + "bg": 0, + "ch": 0, + "cy": 0, + "cz": 0, + "de": 0, + "dk": 0, + "dz": 0, + "ee": 0, + "eg": 0, + "es": 0, + "fi": 0, + "fo": 0, + "fr": 0, + "gg": 0, + "gi": 0, + "gr": 0, + "hr": 0, + "hu": 0, + "ie": 0, + "il": 0, + "im": 0, + "is": 0, + "it": 0, + "je": 0, + "lb": 0, + "li": 0, + "lt": 0, + "lu": 0, + "lv": 0, + "ly": 0, + "ma": 0, + "mc": 0, + "md": 0, + "me": 0, + "mk": 0, + "mt": 0, + "nl": 0, + "no": 0, + "pl": 0, + "ps": 0, + "pt": 0, + "ro": 0, + "rs": 0, + "se": 0, + "si": 0, + "sj": 0, + "sk": 0, + "sm": 0, + "sy": 0, + "tn": 0, + "tr": 0, + "ua": 0, + "uk": 0, + "us": 0, + "va": 0, + "world": 0, + "xk": 0 + }, + "values": { + "ad": -5, + "al": -5, + "at": -5, + "ax": -5, + "ba": -5, + "be": -5, + "bg": -5, + "ch": -5, + "cy": -5, + "cz": -5, + "de": -5, + "dk": -5, + "dz": -5, + "ee": -5, + "eg": -5, + "es": -5, + "fi": -5, + "fo": -5, + "fr": -5, + "gg": -5, + "gi": -5, + "gr": -5, + "hr": -5, + "hu": -5, + "ie": -5, + "il": -5, + "im": -5, + "is": -5, + "it": -5, + "je": -5, + "lb": -5, + "li": -5, + "lt": -5, + "lu": -5, + "lv": -5, + "ly": -5, + "ma": -5, + "mc": -5, + "md": -5, + "me": -5, + "mk": -5, + "mt": -5, + "nl": -5, + "no": -5, + "pl": -5, + "ps": -5, + "pt": -5, + "ro": -5, + "rs": -5, + "se": -5, + "si": -5, + "sj": -5, + "sk": -5, + "sm": -5, + "sy": -5, + "tn": -5, + "tr": -5, + "ua": -5, + "uk": -5, + "us": -5, + "va": -5, + "world": -5, + "xk": -5 + }, + "warning": "origins_are_100_percent_unknown" + }, + "packaging": { + "non_recyclable_and_non_biodegradable_materials": 1, + "packagings": [ + { + "ecoscore_material_score": 0, + "ecoscore_shape_ratio": 1, + "material": "en:plastic", + "non_recyclable_and_non_biodegradable": "maybe", + "shape": "en:pack" + } + ], + "score": 0, + "value": -10 + }, + "production_system": { + "labels": [], + "value": 0, + "warning": "no_label" + }, + "threatened_species": {} + }, + "agribalyse": { + "agribalyse_food_code": "4004", + "co2_agriculture": 1.2992636, + "co2_consumption": 0, + "co2_distribution": 0.029120657, + "co2_packaging": 0.28581962, + "co2_processing": 0.39294234, + "co2_total": 2.2443641, + "co2_transportation": 0.23728203, + "code": "4004", + "dqr": "2.45", + "ef_agriculture": 0.18214682, + "ef_consumption": 0, + "ef_distribution": 0.0098990521, + "ef_packaging": 0.021558384, + "ef_processing": 0.057508389, + "ef_total": 0.29200269, + "ef_transportation": 0.020894187, + "is_beverage": 0, + "name_en": "Potato crisps", + "name_fr": "Chips de pommes de terre, standard", + "score": 78 + }, + "grade": "b", + "grades": { + "ad": "b", + "al": "b", + "at": "b", + "ax": "b", + "ba": "b", + "be": "b", + "bg": "b", + "ch": "b", + "cy": "b", + "cz": "b", + "de": "b", + "dk": "b", + "dz": "b", + "ee": "b", + "eg": "b", + "es": "b", + "fi": "b", + "fo": "b", + "fr": "b", + "gg": "b", + "gi": "b", + "gr": "b", + "hr": "b", + "hu": "b", + "ie": "b", + "il": "b", + "im": "b", + "is": "b", + "it": "b", + "je": "b", + "lb": "b", + "li": "b", + "lt": "b", + "lu": "b", + "lv": "b", + "ly": "b", + "ma": "b", + "mc": "b", + "md": "b", + "me": "b", + "mk": "b", + "mt": "b", + "nl": "b", + "no": "b", + "pl": "b", + "ps": "b", + "pt": "b", + "ro": "b", + "rs": "b", + "se": "b", + "si": "b", + "sj": "b", + "sk": "b", + "sm": "b", + "sy": "b", + "tn": "b", + "tr": "b", + "ua": "b", + "uk": "b", + "us": "b", + "va": "b", + "world": "b", + "xk": "b" + }, + "missing": { + "labels": 1, + "origins": 1 + }, + "missing_data_warning": 1, + "score": 63, + "scores": { + "ad": 63, + "al": 63, + "at": 63, + "ax": 63, + "ba": 63, + "be": 63, + "bg": 63, + "ch": 63, + "cy": 63, + "cz": 63, + "de": 63, + "dk": 63, + "dz": 63, + "ee": 63, + "eg": 63, + "es": 63, + "fi": 63, + "fo": 63, + "fr": 63, + "gg": 63, + "gi": 63, + "gr": 63, + "hr": 63, + "hu": 63, + "ie": 63, + "il": 63, + "im": 63, + "is": 63, + "it": 63, + "je": 63, + "lb": 63, + "li": 63, + "lt": 63, + "lu": 63, + "lv": 63, + "ly": 63, + "ma": 63, + "mc": 63, + "md": 63, + "me": 63, + "mk": 63, + "mt": 63, + "nl": 63, + "no": 63, + "pl": 63, + "ps": 63, + "pt": 63, + "ro": 63, + "rs": 63, + "se": 63, + "si": 63, + "sj": 63, + "sk": 63, + "sm": 63, + "sy": 63, + "tn": 63, + "tr": 63, + "ua": 63, + "uk": 63, + "us": 63, + "va": 63, + "world": 63, + "xk": 63 + }, + "status": "known" + }, + "ecoscore_extended_data": { + "impact": { + "ef_single_score_log_stddev": 0.0664290643574977, + "likeliest_impacts": { + "Climate_change": 0.0835225930657116, + "EF_single_score": 0.0132996566234689 + }, + "likeliest_recipe": { + "en:Oak_smoked_sea_salti_yeast_extract": 0.103505496656251, + "en:e160c": 0.10350549665625, + "en:e330": 0.10350549665625, + "en:flavouring": 0.10350549665625, + "en:garlic_powder": 0.103505496656251, + "en:milk": 1.55847864453775, + "en:onion": 0.15510736429208, + "en:potato": 69.2208020730349, + "en:potato_starch": 10.5320407294931, + "en:rice_flour": 13.8595510001351, + "en:salt": 1.3345917157533, + "en:spice": 0.10350549665625, + "en:sugar": 10.2883618334396, + "en:sunflower_oil": 14.1645835312727, + "en:tomato_powder": 0.10350549665625, + "en:water": 6.24510964041154, + "en:yeast_powder": 0.103505496656251 + }, + "mass_ratio_uncharacterized": 0.0244618467395455, + "uncharacterized_ingredients": { + "impact": [ + "en:yeast-powder", + "en:flavouring", + "en:Oak smoked sea salti yeast extract", + "en:e160c", + "en:e330" + ], + "nutrition": [ + "en:flavouring", + "en:Oak smoked sea salti yeast extract" + ] + }, + "uncharacterized_ingredients_mass_proportion": { + "impact": 0.0244618467395455, + "nutrition": 0.0106506947223728 + }, + "uncharacterized_ingredients_ratio": { + "impact": 0.3125, + "nutrition": 0.125 + }, + "warnings": [ + "Fermentation agents are present in the product (en:yeast-powder). Carbohydrates and sugars mass balance will not be considered to estimate potential recipes", + "The product has a high number of impact uncharacterized ingredients: 31%" + ] + } + }, + "ecoscore_extended_data_version": "4", + "ecoscore_grade": "b", + "ecoscore_score": 63, + "ecoscore_tags": [ + "b" + ], + "editors": [ + "kyzh", + "tacite" + ], + "editors_tags": [ + "yuka.VjQwdU5yUUlpdmxUbjhWa3BFenc4ZGt1NDVLUFZtNm9NdWdOSWc9PQ", + "yuka.sY2b0xO6T85zoF3NwEKvllZnctbb-gn-LDr4mHzUyem0FYPXMO5by7b5NKg", + "tacite", + "kyzh", + "foodless", + "packbot", + "openfoodfacts-contributors", + "kiliweb", + "yuka.sY2b0xO6T85zoF3NwEKvlmBZVPXu-gnlBU3miFTQ-NeSIbDaMdUtu4fLGas", + "ecoscore-impact-estimator", + "swipe-studio", + "tacite-mass-editor" + ], + "emb_codes": "", + "emb_codes_20141016": "", + "emb_codes_debug_tags": [], + "emb_codes_orig": "", + "emb_codes_tags": [], + "entry_dates_tags": [ + "2015-06-03", + "2015-06", + "2015" + ], + "expiration_date": "11/05/2016", + "expiration_date_debug_tags": [], + "food_groups": "en:appetizers", + "food_groups_tags": [ + "en:salty-snacks", + "en:appetizers" + ], + "fruits-vegetables-nuts_100g_estimate": 0, + "generic_name": "", + "generic_name_en": "", + "generic_name_en_debug_tags": [], + "generic_name_fr": "", + "generic_name_fr_debug_tags": [], + "id": "5060292302201", + "image_front_small_url": "https://images.openfoodfacts.org/images/products/506/029/230/2201/front_en.23.200.jpg", + "image_front_thumb_url": "https://images.openfoodfacts.org/images/products/506/029/230/2201/front_en.23.100.jpg", + "image_front_url": "https://images.openfoodfacts.org/images/products/506/029/230/2201/front_en.23.400.jpg", + "image_ingredients_small_url": "https://images.openfoodfacts.org/images/products/506/029/230/2201/ingredients_en.11.200.jpg", + "image_ingredients_thumb_url": "https://images.openfoodfacts.org/images/products/506/029/230/2201/ingredients_en.11.100.jpg", + "image_ingredients_url": "https://images.openfoodfacts.org/images/products/506/029/230/2201/ingredients_en.11.400.jpg", + "image_nutrition_small_url": "https://images.openfoodfacts.org/images/products/506/029/230/2201/nutrition_en.32.200.jpg", + "image_nutrition_thumb_url": "https://images.openfoodfacts.org/images/products/506/029/230/2201/nutrition_en.32.100.jpg", + "image_nutrition_url": "https://images.openfoodfacts.org/images/products/506/029/230/2201/nutrition_en.32.400.jpg", + "image_small_url": "https://images.openfoodfacts.org/images/products/506/029/230/2201/front_en.23.200.jpg", + "image_thumb_url": "https://images.openfoodfacts.org/images/products/506/029/230/2201/front_en.23.100.jpg", + "image_url": "https://images.openfoodfacts.org/images/products/506/029/230/2201/front_en.23.400.jpg", + "images": { + "1": { + "sizes": { + "100": { + "h": 74, + "w": 100 + }, + "400": { + "h": 296, + "w": 400 + }, + "full": { + "h": 1482, + "w": 2000 + } + }, + "uploaded_t": 1433338177, + "uploader": "kyzh" + }, + "2": { + "sizes": { + "100": { + "h": 74, + "w": 100 + }, + "400": { + "h": 296, + "w": 400 + }, + "full": { + "h": 1482, + "w": 2000 + } + }, + "uploaded_t": 1433338194, + "uploader": "kyzh" + }, + "3": { + "sizes": { + "100": { + "h": 74, + "w": 100 + }, + "400": { + "h": 296, + "w": 400 + }, + "full": { + "h": 1482, + "w": 2000 + } + }, + "uploaded_t": 1433338203, + "uploader": "kyzh" + }, + "4": { + "sizes": { + "100": { + "h": 74, + "w": 100 + }, + "400": { + "h": 296, + "w": 400 + }, + "full": { + "h": 1482, + "w": 2000 + } + }, + "uploaded_t": 1433338215, + "uploader": "kyzh" + }, + "5": { + "sizes": { + "100": { + "h": 74, + "w": 100 + }, + "400": { + "h": 296, + "w": 400 + }, + "full": { + "h": 1482, + "w": 2000 + } + }, + "uploaded_t": 1433338229, + "uploader": "kyzh" + }, + "6": { + "sizes": { + "100": { + "h": 74, + "w": 100 + }, + "400": { + "h": 296, + "w": 400 + }, + "full": { + "h": 1482, + "w": 2000 + } + }, + "uploaded_t": 1433338245, + "uploader": "kyzh" + }, + "7": { + "sizes": { + "100": { + "h": 43, + "w": 100 + }, + "400": { + "h": 171, + "w": 400 + }, + "full": { + "h": 846, + "w": 1974 + } + }, + "uploaded_t": "1508236270", + "uploader": "kiliweb" + }, + "8": { + "sizes": { + "100": { + "h": 100, + "w": 82 + }, + "400": { + "h": 400, + "w": 326 + }, + "full": { + "h": 1140, + "w": 930 + } + }, + "uploaded_t": 1620505759, + "uploader": "kiliweb" + }, + "9": { + "sizes": { + "100": { + "h": 56, + "w": 100 + }, + "400": { + "h": 225, + "w": 400 + }, + "full": { + "h": 569, + "w": 1011 + } + }, + "uploaded_t": 1656075071, + "uploader": "kiliweb" + }, + "front": { + "geometry": "1421x1825-0-95", + "imgid": "1", + "normalize": "false", + "rev": "9", + "sizes": { + "100": { + "h": 100, + "w": 78 + }, + "200": { + "h": 200, + "w": 156 + }, + "400": { + "h": 400, + "w": 311 + }, + "full": { + "h": 1825, + "w": 1421 + } + }, + "white_magic": "true" + }, + "front_en": { + "angle": 0, + "coordinates_image_size": "full", + "geometry": "0x0--1--1", + "imgid": "8", + "normalize": null, + "rev": "23", + "sizes": { + "100": { + "h": 100, + "w": 82 + }, + "200": { + "h": 200, + "w": 163 + }, + "400": { + "h": 400, + "w": 326 + }, + "full": { + "h": 1140, + "w": 930 + } + }, + "white_magic": null, + "x1": "-1", + "x2": "-1", + "y1": "-1", + "y2": "-1" + }, + "ingredients": { + "geometry": "1730x526-125-304", + "imgid": "5", + "normalize": "false", + "ocr": 1, + "orientation": "0", + "rev": "11", + "sizes": { + "100": { + "h": 30, + "w": 100 + }, + "200": { + "h": 61, + "w": 200 + }, + "400": { + "h": 122, + "w": 400 + }, + "full": { + "h": 526, + "w": 1730 + } + }, + "white_magic": "false" + }, + "ingredients_en": { + "geometry": "1730x526-125-304", + "imgid": "5", + "normalize": "false", + "ocr": 1, + "orientation": "0", + "rev": "11", + "sizes": { + "100": { + "h": 30, + "w": 100 + }, + "200": { + "h": 61, + "w": 200 + }, + "400": { + "h": 122, + "w": 400 + }, + "full": { + "h": 526, + "w": 1730 + } + }, + "white_magic": "false" + }, + "nutrition": { + "geometry": "1131x920-150-794", + "imgid": "3", + "normalize": "false", + "ocr": 1, + "orientation": "0", + "rev": "10", + "sizes": { + "100": { + "h": 81, + "w": 100 + }, + "200": { + "h": 163, + "w": 200 + }, + "400": { + "h": 325, + "w": 400 + }, + "full": { + "h": 920, + "w": 1131 + } + }, + "white_magic": "false" + }, + "nutrition_en": { + "angle": 0, + "coordinates_image_size": "full", + "geometry": "0x0--1--1", + "imgid": "9", + "normalize": null, + "rev": "32", + "sizes": { + "100": { + "h": 56, + "w": 100 + }, + "200": { + "h": 113, + "w": 200 + }, + "400": { + "h": 225, + "w": 400 + }, + "full": { + "h": 569, + "w": 1011 + } + }, + "white_magic": null, + "x1": "-1", + "x2": "-1", + "y1": "-1", + "y2": "-1" + } + }, + "informers_tags": [ + "kyzh", + "tacite", + "tacite-mass-editor", + "yuka.VjQwdU5yUUlpdmxUbjhWa3BFenc4ZGt1NDVLUFZtNm9NdWdOSWc9PQ", + "openfoodfacts-contributors" + ], + "ingredients": [ + { + "id": "en:potato", + "percent": 54, + "percent_estimate": 54, + "percent_max": 54, + "percent_min": 54, + "processing": "en:dried", + "rank": 1, + "text": "potatoes", + "vegan": "yes", + "vegetarian": "yes" + }, + { + "from_palm_oil": "no", + "id": "en:sunflower-oil", + "percent_estimate": 28.75, + "percent_max": 46, + "percent_min": 11.5, + "rank": 2, + "text": "sunflower oil", + "vegan": "yes", + "vegetarian": "yes" + }, + { + "has_sub_ingredients": "yes", + "id": "en:coating", + "percent_estimate": 8.625, + "percent_max": 33.3333333333333, + "percent_min": 0, + "rank": 3, + "text": "seasoning", + "vegan": "ignore", + "vegetarian": "ignore" + }, + { + "id": "en:rice-flour", + "percent_estimate": 4.3125, + "percent_max": 17.25, + "percent_min": 0, + "rank": 4, + "text": "rice flour", + "vegan": "yes", + "vegetarian": "yes" + }, + { + "id": "en:potato-starch", + "percent_estimate": 4.3125, + "percent_max": 11.5, + "percent_min": 0, + "rank": 5, + "text": "potato starch", + "vegan": "yes", + "vegetarian": "yes" + }, + { + "id": "en:sugar", + "percent_estimate": 4.3125, + "percent_max": 33.3333333333333, + "percent_min": 0, + "text": "sugar", + "vegan": "yes", + "vegetarian": "yes" + }, + { + "has_sub_ingredients": "yes", + "id": "en:whey-powder", + "percent_estimate": 2.15625, + "percent_max": 16.6666666666667, + "percent_min": 0, + "text": "whey powder", + "vegan": "no", + "vegetarian": "maybe" + }, + { + "id": "en:salt", + "percent_estimate": 1.078125, + "percent_max": 11.1111111111111, + "percent_min": 0, + "text": "salt", + "vegan": "yes", + "vegetarian": "yes" + }, + { + "id": "en:onion", + "percent_estimate": 0.5390625, + "percent_max": 8.33333333333333, + "percent_min": 0, + "processing": "en:powder", + "text": "onion", + "vegan": "yes", + "vegetarian": "yes" + }, + { + "id": "en:yeast-powder", + "percent_estimate": 0.26953125, + "percent_max": 6.66666666666667, + "percent_min": 0, + "text": "yeast powder", + "vegan": "yes", + "vegetarian": "yes" + }, + { + "id": "en:garlic-powder", + "percent_estimate": 0.134765625, + "percent_max": 5.55555555555556, + "percent_min": 0, + "text": "garlic powder", + "vegan": "yes", + "vegetarian": "yes" + }, + { + "id": "en:tomato-powder", + "percent_estimate": 0.0673828125, + "percent_max": 4.76190476190476, + "percent_min": 0, + "text": "tomato powder", + "vegan": "yes", + "vegetarian": "yes" + }, + { + "id": "en:oak-smoked-sea-salti-yeast-extract", + "percent_estimate": 0.03369140625, + "percent_max": 4.16666666666667, + "percent_min": 0, + "text": "Oak smoked sea salti yeast extract" + }, + { + "id": "en:flavouring", + "percent_estimate": 0.016845703125, + "percent_max": 3.7037037037037, + "percent_min": 0, + "text": "flavourings", + "vegan": "maybe", + "vegetarian": "maybe" + }, + { + "id": "en:spice", + "percent_estimate": 0.0084228515625, + "percent_max": 3.33333333333333, + "percent_min": 0, + "text": "spices", + "vegan": "yes", + "vegetarian": "yes" + }, + { + "has_sub_ingredients": "yes", + "id": "en:acid", + "percent_estimate": 0.00421142578125, + "percent_max": 3.03030303030303, + "percent_min": 0, + "text": "acid" + }, + { + "has_sub_ingredients": "yes", + "id": "en:colour", + "percent_estimate": 0.00421142578125, + "percent_max": 2.77777777777778, + "percent_min": 0, + "text": "colour" + }, + { + "id": "en:milk", + "percent_estimate": 2.15625, + "percent_max": 16.6666666666667, + "percent_min": 0, + "text": "milk", + "vegan": "no", + "vegetarian": "yes" + }, + { + "id": "en:e330", + "percent_estimate": 0.00421142578125, + "percent_max": 3.03030303030303, + "percent_min": 0, + "text": "citric acid", + "vegan": "yes", + "vegetarian": "yes" + }, + { + "id": "en:e160c", + "percent_estimate": 0.00421142578125, + "percent_max": 2.77777777777778, + "percent_min": 0, + "text": "paprika extract", + "vegan": "yes", + "vegetarian": "yes" + } + ], + "ingredients_analysis": { + "en:non-vegan": [ + "en:whey-powder", + "en:milk" + ], + "en:palm-oil-content-unknown": [ + "en:oak-smoked-sea-salti-yeast-extract" + ], + "en:vegan-status-unknown": [ + "en:oak-smoked-sea-salti-yeast-extract" + ], + "en:vegetarian-status-unknown": [ + "en:oak-smoked-sea-salti-yeast-extract" + ] + }, + "ingredients_analysis_tags": [ + "en:palm-oil-free", + "en:non-vegan", + "en:vegetarian" + ], + "ingredients_debug": [ + "54% dried potatoes", + ",", + null, + null, + null, + " sunflower oil", + ",", + null, + null, + null, + " seasoning ", + "(", + "(", + null, + null, + "sugar", + ",", + null, + null, + null, + " whey powder ", + "[", + "[", + null, + null, + "milk]", + ",", + null, + null, + null, + " salt", + ",", + null, + null, + null, + " onion powder", + ",", + null, + null, + null, + " yeast powder", + ",", + null, + null, + null, + " garlic powder", + ",", + null, + null, + null, + " tomato powder", + ",", + null, + null, + null, + " Oak smoked sea salti yeast extract", + ",", + null, + null, + null, + " flavourings", + ",", + null, + null, + null, + " spices", + ",", + null, + null, + null, + " acid", + ":", + ":", + null, + null, + " citric acid", + ",", + null, + null, + null, + " colour", + ":", + ":", + null, + null, + " paprika extract)", + ",", + null, + null, + null, + " rice flour", + ",", + null, + null, + null, + " potato starch." + ], + "ingredients_from_or_that_may_be_from_palm_oil_n": 0, + "ingredients_from_palm_oil_n": 0, + "ingredients_from_palm_oil_tags": [], + "ingredients_hierarchy": [ + "en:potato", + "en:vegetable", + "en:root-vegetable", + "en:sunflower-oil", + "en:oil-and-fat", + "en:vegetable-oil-and-fat", + "en:vegetable-oil", + "en:coating", + "en:rice-flour", + "en:flour", + "en:rice", + "en:potato-starch", + "en:starch", + "en:sugar", + "en:added-sugar", + "en:disaccharide", + "en:whey-powder", + "en:dairy", + "en:whey", + "en:salt", + "en:onion", + "en:yeast-powder", + "en:yeast", + "en:garlic-powder", + "en:garlic", + "en:tomato-powder", + "en:tomato", + "en:oak-smoked-sea-salti-yeast-extract", + "en:flavouring", + "en:spice", + "en:condiment", + "en:acid", + "en:colour", + "en:milk", + "en:e330", + "en:e160c" + ], + "ingredients_ids_debug": [ + "54-dried-potatoes", + "sunflower-oil", + "seasoning", + "sugar", + "whey-powder", + "milk", + "salt", + "onion-powder", + "yeast-powder", + "garlic-powder", + "tomato-powder", + "oak-smoked-sea-salti-yeast-extract", + "flavourings", + "spices", + "acid", + "citric-acid", + "colour", + "paprika-extract", + "rice-flour", + "potato-starch" + ], + "ingredients_n": 20, + "ingredients_n_tags": [ + "20", + "11-20" + ], + "ingredients_original_tags": [ + "en:potato", + "en:sunflower-oil", + "en:coating", + "en:rice-flour", + "en:potato-starch", + "en:sugar", + "en:whey-powder", + "en:salt", + "en:onion", + "en:yeast-powder", + "en:garlic-powder", + "en:tomato-powder", + "en:oak-smoked-sea-salti-yeast-extract", + "en:flavouring", + "en:spice", + "en:acid", + "en:colour", + "en:milk", + "en:e330", + "en:e160c" + ], + "ingredients_percent_analysis": 1, + "ingredients_tags": [ + "en:potato", + "en:vegetable", + "en:root-vegetable", + "en:sunflower-oil", + "en:oil-and-fat", + "en:vegetable-oil-and-fat", + "en:vegetable-oil", + "en:coating", + "en:rice-flour", + "en:flour", + "en:rice", + "en:potato-starch", + "en:starch", + "en:sugar", + "en:added-sugar", + "en:disaccharide", + "en:whey-powder", + "en:dairy", + "en:whey", + "en:salt", + "en:onion", + "en:yeast-powder", + "en:yeast", + "en:garlic-powder", + "en:garlic", + "en:tomato-powder", + "en:tomato", + "en:oak-smoked-sea-salti-yeast-extract", + "en:flavouring", + "en:spice", + "en:condiment", + "en:acid", + "en:colour", + "en:milk", + "en:e330", + "en:e160c" + ], + "ingredients_text": "54% dried potatoes, sunflower oil, seasoning (sugar, whey powder [milk], salt, onion powder, yeast powder, garlic powder, tomato powder, Oak smoked sea salti yeast extract, flavourings, spices, acid: citric acid, colour: paprika extract), rice flour, potato starch.", + "ingredients_text_debug": "54% dried potatoes, sunflower oil, seasoning (sugar, whey powder [milk], salt, onion powder, yeast powder, garlic powder, tomato powder, Oak smoked sea salti yeast extract, flavourings, spices, acid: citric acid, colour: paprika extract), rice flour, potato starch.", + "ingredients_text_debug_tags": [], + "ingredients_text_en": "54% dried potatoes, sunflower oil, seasoning (sugar, whey powder [milk], salt, onion powder, yeast powder, garlic powder, tomato powder, Oak smoked sea salti yeast extract, flavourings, spices, acid: citric acid, colour: paprika extract), rice flour, potato starch.", + "ingredients_text_fr": "", + "ingredients_text_fr_debug_tags": [], + "ingredients_text_with_allergens": "54% dried potatoes, sunflower oil, seasoning (sugar, whey powder [milk], salt, onion powder, yeast powder, garlic powder, tomato powder, Oak smoked sea salti yeast extract, flavourings, spices, acid: citric acid, colour: paprika extract), rice flour, potato starch.", + "ingredients_text_with_allergens_en": "54% dried potatoes, sunflower oil, seasoning (sugar, whey powder [milk], salt, onion powder, yeast powder, garlic powder, tomato powder, Oak smoked sea salti yeast extract, flavourings, spices, acid: citric acid, colour: paprika extract), rice flour, potato starch.", + "ingredients_that_may_be_from_palm_oil_n": 0, + "ingredients_that_may_be_from_palm_oil_tags": [], + "ingredients_with_specified_percent_n": 1, + "ingredients_with_specified_percent_sum": 54, + "ingredients_with_unspecified_percent_n": 15, + "ingredients_with_unspecified_percent_sum": 46, + "interface_version_created": "20120622", + "interface_version_modified": "20150316.jqm2", + "known_ingredients_n": 35, + "labels": "Vegetarian, No preservatives, No artificial anything", + "labels_hierarchy": [ + "en:vegetarian", + "en:no-preservatives", + "en:No artificial anything" + ], + "labels_lc": "en", + "labels_old": "Vegetarian, No preservatives, No artificial anything", + "labels_tags": [ + "en:vegetarian", + "en:no-preservatives", + "en:no-artificial-anything" + ], + "lang": "en", + "lang_debug_tags": [], + "languages": { + "en:english": 5 + }, + "languages_codes": { + "en": 5 + }, + "languages_hierarchy": [ + "en:english" + ], + "languages_tags": [ + "en:english", + "en:1" + ], + "last_edit_dates_tags": [ + "2022-06-24", + "2022-06", + "2022" + ], + "last_editor": "kiliweb", + "last_image_dates_tags": [ + "2022-06-24", + "2022-06", + "2022" + ], + "last_image_t": 1656075071, + "last_modified_by": "kiliweb", + "last_modified_t": 1656075071, + "lc": "en", + "link": "", + "link_debug_tags": [], + "main_countries_tags": [], + "manufacturing_places": "European Union", + "manufacturing_places_debug_tags": [], + "manufacturing_places_tags": [ + "european-union" + ], + "max_imgid": "9", + "minerals_prev_tags": [], + "minerals_tags": [], + "misc_tags": [ + "en:nutrition-fruits-vegetables-nuts-estimate-from-ingredients", + "en:nutrition-all-nutriscore-values-known", + "en:nutriscore-computed", + "en:ecoscore-extended-data-computed", + "en:ecoscore-extended-data-version-4", + "en:ecoscore-missing-data-warning", + "en:ecoscore-missing-data-labels", + "en:ecoscore-missing-data-origins", + "en:ecoscore-computed" + ], + "no_nutrition_data": "", + "nova_group": 4, + "nova_group_debug": "", + "nova_groups": "4", + "nova_groups_markers": { + "3": [ + [ + "categories", + "en:salty-snacks" + ], + [ + "ingredients", + "en:salt" + ], + [ + "ingredients", + "en:starch" + ], + [ + "ingredients", + "en:sugar" + ], + [ + "ingredients", + "en:vegetable-oil" + ] + ], + "4": [ + [ + "additives", + "en:e160c" + ], + [ + "ingredients", + "en:colour" + ], + [ + "ingredients", + "en:flavouring" + ], + [ + "ingredients", + "en:whey" + ] + ] + }, + "nova_groups_tags": [ + "en:4-ultra-processed-food-and-drink-products" + ], + "nucleotides_prev_tags": [], + "nucleotides_tags": [], + "nutrient_levels": { + "fat": "moderate", + "salt": "high", + "saturated-fat": "low", + "sugars": "moderate" + }, + "nutrient_levels_tags": [ + "en:fat-in-moderate-quantity", + "en:saturated-fat-in-low-quantity", + "en:sugars-in-moderate-quantity", + "en:salt-in-high-quantity" + ], + "nutriments": { + "carbohydrates": 15, + "carbohydrates_100g": 15, + "carbohydrates_serving": 3.45, + "carbohydrates_unit": "g", + "carbohydrates_value": 15, + "carbon-footprint-from-known-ingredients_100g": 32.4, + "carbon-footprint-from-known-ingredients_product": 7.45, + "carbon-footprint-from-known-ingredients_serving": 7.45, + "energy": 1757, + "energy-kcal": 420, + "energy-kcal_100g": 420, + "energy-kcal_serving": 96.6, + "energy-kcal_unit": "kcal", + "energy-kcal_value": 420, + "energy_100g": 1757, + "energy_serving": 404, + "energy_unit": "kcal", + "energy_value": 420, + "fat": 15, + "fat_100g": 15, + "fat_serving": 3.45, + "fat_unit": "g", + "fat_value": 15, + "fiber": 3.9, + "fiber_100g": 3.9, + "fiber_serving": 0.897, + "fiber_unit": "g", + "fiber_value": 3.9, + "fruits-vegetables-nuts-estimate-from-ingredients_100g": 0, + "fruits-vegetables-nuts-estimate-from-ingredients_serving": 0, + "nova-group": 4, + "nova-group_100g": 4, + "nova-group_serving": 4, + "nutrition-score-fr": 12, + "nutrition-score-fr_100g": 12, + "proteins": 5.7, + "proteins_100g": 5.7, + "proteins_serving": 1.31, + "proteins_unit": "g", + "proteins_value": 5.7, + "salt": 2.1, + "salt_100g": 2.1, + "salt_serving": 0.483, + "salt_unit": "g", + "salt_value": 2.1, + "saturated-fat": 1.4, + "saturated-fat_100g": 1.4, + "saturated-fat_serving": 0.322, + "saturated-fat_unit": "g", + "saturated-fat_value": 1.4, + "sodium": 0.84, + "sodium_100g": 0.84, + "sodium_serving": 0.193, + "sodium_unit": "g", + "sodium_value": 0.84, + "sugars": 8.7, + "sugars_100g": 8.7, + "sugars_serving": 2, + "sugars_unit": "g", + "sugars_value": 8.7 + }, + "nutriscore_data": { + "energy": 1757, + "energy_points": 5, + "energy_value": 1757, + "fiber": 3.9, + "fiber_points": 4, + "fiber_value": 3.9, + "fruits_vegetables_nuts_colza_walnut_olive_oils": 0, + "fruits_vegetables_nuts_colza_walnut_olive_oils_points": 0, + "fruits_vegetables_nuts_colza_walnut_olive_oils_value": 0, + "grade": "d", + "is_beverage": 0, + "is_cheese": 0, + "is_fat": 0, + "is_water": 0, + "negative_points": 16, + "positive_points": 4, + "proteins": 5.7, + "proteins_points": 3, + "proteins_value": 5.7, + "saturated_fat": 1.4, + "saturated_fat_points": 1, + "saturated_fat_ratio": 9.33333333333333, + "saturated_fat_ratio_points": 0, + "saturated_fat_ratio_value": 9.3, + "saturated_fat_value": 1.4, + "score": 12, + "sodium": 840, + "sodium_points": 9, + "sodium_value": 840, + "sugars": 8.7, + "sugars_points": 1, + "sugars_value": 8.7 + }, + "nutriscore_grade": "d", + "nutriscore_score": 12, + "nutriscore_score_opposite": -12, + "nutrition_data": "on", + "nutrition_data_per": "100g", + "nutrition_data_prepared": "", + "nutrition_data_prepared_per": "100g", + "nutrition_data_prepared_per_debug_tags": [], + "nutrition_grade_fr": "d", + "nutrition_grades": "d", + "nutrition_grades_tags": [ + "d" + ], + "nutrition_score_beverage": 0, + "nutrition_score_debug": "", + "nutrition_score_warning_fruits_vegetables_nuts_estimate_from_ingredients": 1, + "nutrition_score_warning_fruits_vegetables_nuts_estimate_from_ingredients_value": 0, + "origins": "", + "origins_hierarchy": [], + "origins_lc": "en", + "origins_old": "", + "origins_tags": [], + "other_nutritional_substances_tags": [], + "packaging": "Plastic, en:mixed plastic film-packet", + "packaging_hierarchy": [ + "en:plastic", + "en:mixed plastic film-packet" + ], + "packaging_lc": "en", + "packaging_old": "Plastic, Mixed plastic-packet", + "packaging_old_before_taxonomization": "Plastic, en:mixed plastic-packet", + "packaging_tags": [ + "en:plastic", + "en:mixed-plastic-film-packet" + ], + "packagings": [ + { + "material": "en:plastic", + "shape": "en:pack" + } + ], + "photographers_tags": [ + "kyzh", + "kiliweb" + ], + "pnns_groups_1": "Salty snacks", + "pnns_groups_1_tags": [ + "salty-snacks", + "known" + ], + "pnns_groups_2": "Appetizers", + "pnns_groups_2_tags": [ + "appetizers", + "known" + ], + "popularity_key": 20900000020, + "popularity_tags": [ + "bottom-25-percent-scans-2019", + "bottom-20-percent-scans-2019", + "bottom-15-percent-scans-2019", + "top-90-percent-scans-2019", + "top-10000-gb-scans-2019", + "top-50000-gb-scans-2019", + "top-100000-gb-scans-2019", + "top-country-gb-scans-2019", + "bottom-25-percent-scans-2020", + "top-80-percent-scans-2020", + "top-85-percent-scans-2020", + "top-90-percent-scans-2020", + "top-5000-gb-scans-2020", + "top-10000-gb-scans-2020", + "top-50000-gb-scans-2020", + "top-100000-gb-scans-2020", + "top-country-gb-scans-2020", + "top-100000-scans-2021", + "at-least-5-scans-2021", + "top-75-percent-scans-2021", + "top-80-percent-scans-2021", + "top-85-percent-scans-2021", + "top-90-percent-scans-2021", + "top-5000-gb-scans-2021", + "top-10000-gb-scans-2021", + "top-50000-gb-scans-2021", + "top-100000-gb-scans-2021", + "top-country-gb-scans-2021", + "at-least-5-gb-scans-2021", + "top-5000-ie-scans-2021", + "top-10000-ie-scans-2021", + "top-50000-ie-scans-2021", + "top-100000-ie-scans-2021", + "top-1000-mu-scans-2021", + "top-5000-mu-scans-2021", + "top-10000-mu-scans-2021", + "top-50000-mu-scans-2021", + "top-100000-mu-scans-2021" + ], + "product_name": "Barbeque Potato Chips", + "product_name_en": "Barbeque Potato Chips", + "product_name_fr": "", + "product_name_fr_debug_tags": [], + "product_quantity": "23", + "purchase_places": "", + "purchase_places_debug_tags": [], + "purchase_places_tags": [], + "quantity": "23 g", + "quantity_debug_tags": [], + "removed_countries_tags": [], + "rev": 32, + "scans_n": 10, + "selected_images": { + "front": { + "display": { + "en": "https://images.openfoodfacts.org/images/products/506/029/230/2201/front_en.23.400.jpg" + }, + "small": { + "en": "https://images.openfoodfacts.org/images/products/506/029/230/2201/front_en.23.200.jpg" + }, + "thumb": { + "en": "https://images.openfoodfacts.org/images/products/506/029/230/2201/front_en.23.100.jpg" + } + }, + "ingredients": { + "display": { + "en": "https://images.openfoodfacts.org/images/products/506/029/230/2201/ingredients_en.11.400.jpg" + }, + "small": { + "en": "https://images.openfoodfacts.org/images/products/506/029/230/2201/ingredients_en.11.200.jpg" + }, + "thumb": { + "en": "https://images.openfoodfacts.org/images/products/506/029/230/2201/ingredients_en.11.100.jpg" + } + }, + "nutrition": { + "display": { + "en": "https://images.openfoodfacts.org/images/products/506/029/230/2201/nutrition_en.32.400.jpg" + }, + "small": { + "en": "https://images.openfoodfacts.org/images/products/506/029/230/2201/nutrition_en.32.200.jpg" + }, + "thumb": { + "en": "https://images.openfoodfacts.org/images/products/506/029/230/2201/nutrition_en.32.100.jpg" + } + } + }, + "serving_quantity": "23", + "serving_size": "23 g", + "serving_size_debug_tags": [], + "sortkey": 1535456524, + "states": "en:to-be-completed, en:nutrition-facts-completed, en:ingredients-completed, en:expiration-date-completed, en:packaging-code-to-be-completed, en:characteristics-to-be-completed, en:origins-to-be-completed, en:categories-completed, en:brands-completed, en:packaging-completed, en:quantity-completed, en:product-name-completed, en:photos-to-be-validated, en:packaging-photo-to-be-selected, en:nutrition-photo-selected, en:ingredients-photo-selected, en:front-photo-selected, en:photos-uploaded", + "states_hierarchy": [ + "en:to-be-completed", + "en:nutrition-facts-completed", + "en:ingredients-completed", + "en:expiration-date-completed", + "en:packaging-code-to-be-completed", + "en:characteristics-to-be-completed", + "en:origins-to-be-completed", + "en:categories-completed", + "en:brands-completed", + "en:packaging-completed", + "en:quantity-completed", + "en:product-name-completed", + "en:photos-to-be-validated", + "en:packaging-photo-to-be-selected", + "en:nutrition-photo-selected", + "en:ingredients-photo-selected", + "en:front-photo-selected", + "en:photos-uploaded" + ], + "states_tags": [ + "en:to-be-completed", + "en:nutrition-facts-completed", + "en:ingredients-completed", + "en:expiration-date-completed", + "en:packaging-code-to-be-completed", + "en:characteristics-to-be-completed", + "en:origins-to-be-completed", + "en:categories-completed", + "en:brands-completed", + "en:packaging-completed", + "en:quantity-completed", + "en:product-name-completed", + "en:photos-to-be-validated", + "en:packaging-photo-to-be-selected", + "en:nutrition-photo-selected", + "en:ingredients-photo-selected", + "en:front-photo-selected", + "en:photos-uploaded" + ], + "stores": "", + "stores_debug_tags": [], + "stores_tags": [], + "teams": "swipe-studio", + "teams_tags": [ + "swipe-studio" + ], + "traces": "", + "traces_debug_tags": [], + "traces_from_ingredients": "", + "traces_from_user": "(en) ", + "traces_hierarchy": [], + "traces_tags": [], + "unique_scans_n": 8, + "unknown_ingredients_n": 1, + "unknown_nutrients_tags": [], + "update_key": "update20221107", + "vitamins_prev_tags": [], + "vitamins_tags": [] + }, + "status": 1, + "status_verbose": "product found" +} diff --git a/examples/tree.py b/examples/tree.py new file mode 100644 index 000000000..756fb2362 --- /dev/null +++ b/examples/tree.py @@ -0,0 +1,32 @@ +import json + +from textual.app import App, ComposeResult +from textual.widgets import Header, Footer, Tree + + +with open("food.json") as data_file: + data = json.load(data_file) + +from rich import print + +print(data) + + +class TreeApp(App): + + BINDINGS = [("a", "add", "Add node")] + + def compose(self) -> ComposeResult: + yield Header() + yield Footer() + yield Tree("Root") + + def action_add(self) -> None: + tree = self.query_one(Tree) + + tree.add_json(data) + + +if __name__ == "__main__": + app = TreeApp() + app.run() diff --git a/src/textual/widget.py b/src/textual/widget.py index fad5ed4d8..2281c90d5 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -857,6 +857,18 @@ class Widget(DOMNode): content_region = self.region.shrink(self.styles.gutter) return content_region + @property + def scrollable_content_region(self) -> Region: + """Gets an absolute region containing the scrollable content (minus padding, border, and scrollbars). + + Returns: + Region: Screen region that contains a widget's content. + """ + content_region = self.region.shrink(self.styles.gutter).shrink( + self.scrollbar_gutter + ) + return content_region + @property def content_offset(self) -> Offset: """An offset from the Widget origin where the content begins. @@ -1622,7 +1634,7 @@ class Widget(DOMNode): Returns: Offset: The distance that was scrolled. """ - window = self.content_region.at_offset(self.scroll_offset) + window = self.scrollable_content_region.at_offset(self.scroll_offset) if spacing is not None: window = window.shrink(spacing) diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 2d42a7d3c..d4deac65a 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -1,8 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from operator import attrgetter -from typing import ClassVar, Generic, NewType, TypeVar +from typing import Callable, ClassVar, Generic, NewType, TypeVar import rich.repr from rich.segment import Segment @@ -51,25 +50,31 @@ class TreeNode(Generic[TreeDataType]): parent: TreeNode[TreeDataType] | None, id: NodeID, label: Text, - data: TreeDataType, + data: TreeDataType | None = None, *, expanded: bool = True, + allow_expand: bool = True, ) -> None: self._tree = tree self._parent = parent self.id = id self.label = label - self.data: TreeDataType = data + self.data: TreeDataType = data if data is not None else tree._data_factory() self._expanded = expanded self.children: list[TreeNode] = [] self._hover = False self._selected = False + self._allow_expand = allow_expand def __rich_repr__(self) -> rich.repr.Result: yield self.label.plain yield self.data + def _reset(self) -> None: + self._hover = False + self._selected = False + @property def expanded(self) -> bool: return self._expanded @@ -93,7 +98,12 @@ class TreeNode(Generic[TreeDataType]): ) def add( - self, label: TextType, data: TreeDataType, expanded: bool = True + self, + label: TextType, + data: TreeDataType | None = None, + *, + expanded: bool = True, + allow_expand: bool = True, ) -> TreeNode[TreeDataType]: """Add a node to the sub-tree. @@ -111,6 +121,7 @@ class TreeNode(Generic[TreeDataType]): text_label = label node = self._tree._add_node(self, text_label, data) node._expanded = expanded + node._allow_expand = allow_expand self.children.append(node) self._tree.invalidate() return node @@ -133,7 +144,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): } Tree > .tree--guides { - color: $success; + color: $success-darken-3; } Tree > .tree--guides-hover { @@ -162,8 +173,6 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): """ - show_root = reactive(True) - COMPONENT_CLASSES: ClassVar[set[str]] = { "tree--label", "tree--guides", @@ -174,9 +183,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): "tree--highlight-line", } + show_root = reactive(True) hover_line = var(-1) cursor_line = var(-1) - guide_depth = var(4, init=False) + show_guides = reactive(True) + guide_depth = reactive(4, init=False) auto_expand = var(True) LINES: dict[str, tuple[str, str, str, str]] = { @@ -210,18 +221,18 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): def __init__( self, label: TextType, - data: TreeDataType, + data: TreeDataType | None = None, + data_factory: Callable[[], TreeDataType] = dict, *, name: str | None = None, id: str | None = None, classes: str | None = None, ) -> None: super().__init__(name=name, id=id, classes=classes) - if isinstance(label, str): - text_label = Text.from_markup(label) - else: - text_label = label + text_label = self.process_label(label) + + self._data_factory = data_factory self._updates = 0 self._nodes: dict[NodeID, TreeNode[TreeDataType]] = {} self._current_id = 0 @@ -230,15 +241,38 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): self._line_cache: LRUCache[LineCacheKey, list[Segment]] = LRUCache(1024) self._tree_lines_cached: list[_TreeLine] | None = None + @classmethod + def process_label(cls, label: TextType): + """Process a str or Text in to a label. + + Args: + label (TextType): Label. + + Returns: + Text: A Rich Text object. + """ + if isinstance(label, str): + text_label = Text.from_markup(label) + else: + text_label = label + first_line = text_label.split()[0] + return first_line + def _add_node( - self, parent: TreeNode[TreeDataType] | None, label: Text, data: TreeDataType + self, + parent: TreeNode[TreeDataType] | None, + label: Text, + data: TreeDataType | None, ) -> TreeNode[TreeDataType]: - node = TreeNode(self, parent, self._new_id(), label, data) + node_data = data if data is not None else self._data_factory() + node = TreeNode(self, parent, self._new_id(), label, node_data) self._nodes[node.id] = node self._updates += 1 return node - def render_label(self, node: TreeNode[TreeDataType]) -> Text: + def render_label( + self, node: TreeNode[TreeDataType], base_style: Style, style: Style + ) -> Text: """Render a label for the given node. Override this to modify how labels are rendered. Args: @@ -247,7 +281,16 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): Returns: Text: A Rich Text object containing the label. """ - return node.label + node_label = node.label.copy() + node_label.stylize(style) + + if node._allow_expand: + prefix = ("▼ " if node.expanded else "▶ ", base_style) + else: + prefix = ("", base_style) + + text = Text.assemble(prefix, node_label) + return text def clear(self) -> None: """Clear all nodes under root.""" @@ -266,6 +309,36 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): self._updates += 1 self.refresh() + def add_json(self, json_data: object) -> None: + + from rich.highlighter import ReprHighlighter + + highlighter = ReprHighlighter() + + def add_node(name: str, node: TreeNode, data: object) -> None: + if isinstance(data, dict): + node.label = Text(f"{{}} {name}") + for key, value in data.items(): + new_node = node.add("") + add_node(key, new_node, value) + elif isinstance(data, list): + node.label = Text(f"[] {name}") + for index, value in enumerate(data): + new_node = node.add("") + add_node(str(index), new_node, value) + else: + node._allow_expand = False + if name: + label = Text.assemble( + Text.from_markup(f"[b]{name}[/b]="), highlighter(repr(data)) + ) + else: + label = Text(repr(data)) + node.label = label + + add_node("", self.root, json_data) + self.invalidate() + def validate_cursor_line(self, value: int) -> int: return clamp(value, 0, len(self._tree_lines) - 1) @@ -273,8 +346,10 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): return clamp(value, 2, 10) def invalidate(self) -> None: + self._line_cache.clear() self._tree_lines_cached = None self._updates += 1 + self.root._reset() self.refresh() def _on_mouse_move(self, event: events.MouseMove): @@ -328,6 +403,9 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): def watch_guide_depth(self, guide_depth: int) -> None: self.invalidate() + def watch_show_root(self, show_root: bool) -> None: + self.invalidate() + def scroll_to_line(self, line: int) -> None: self.scroll_to_region(Region(0, line, self.size.width, 1)) @@ -374,7 +452,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): for last, child in loop_last(node.children): add_node(child_path, child, last=last) - add_node([], root, True) + if self.show_root: + add_node([], root, True) + else: + for node in self.root.children: + add_node([], node, True) self._tree_lines_cached = lines guide_depth = self.guide_depth @@ -383,11 +465,15 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): self.virtual_size = Size(width, len(lines)) def render_line(self, y: int) -> list[Segment]: - width, height = self.size + width = self.size.width scroll_x, scroll_y = self.scroll_offset - y += scroll_y style = self.rich_style - return self._render_line(y, scroll_x, scroll_x + width, style) + return self._render_line( + y + scroll_y, + scroll_x, + scroll_x + width, + style, + ) def _render_line( self, y: int, x1: int, x2: int, base_style: Style @@ -404,96 +490,106 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): cache_key = ( y, + is_hover, width, self._updates, y == self.hover_line, y == self.cursor_line, self.has_focus, - tuple(node._hover for node in line.path), - tuple(node._selected for node in line.path), + tuple((node._hover, node._selected, node.expanded) for node in line.path), ) if cache_key in self._line_cache: - return self._line_cache[cache_key] - - base_guide_style = self.get_component_rich_style("tree--guides", partial=True) - guide_hover_style = base_guide_style + self.get_component_rich_style( - "tree--guides-hover", partial=True - ) - guide_selected_style = base_guide_style + self.get_component_rich_style( - "tree--guides-selected", partial=True - ) - - hover = self.root._hover - selected = self.root._selected and self.has_focus - - def get_guides(style: Style) -> tuple[str, str, str, str]: - """Get the guide strings for a given style. - - Args: - style (Style): A Style object. - - Returns: - tuple[str, str, str, str]: Strings for space, vertical, terminator and cross. - """ - lines = self.LINES["default"] - if style.bold: - lines = self.LINES["bold"] - elif style.underline2: - lines = self.LINES["double"] - - guide_depth = max(0, self.guide_depth - 2) - lines = tuple( - f"{vertical}{horizontal * guide_depth} " - for vertical, horizontal in lines - ) - return lines - - if is_hover: - line_style = self.get_component_rich_style("tree--highlight-line") + segments = self._line_cache[cache_key] else: - line_style = base_style - - guides = Text(style=line_style) - guides_append = guides.append - - guide_style = base_guide_style - for node in line.path[1:]: - if hover: - guide_style = guide_hover_style - if selected: - guide_style = guide_selected_style - - space, vertical, _, _ = get_guides(guide_style) - guide = space if node.last else vertical - if node != line.path[-1]: - guides_append(guide, style=guide_style) - hover = hover or node._hover - selected = (selected or node._selected) and self.has_focus - - if len(line.path) > 1: - _, _, terminator, cross = get_guides(guide_style) - if line.last: - guides.append(terminator, style=guide_style) - else: - guides.append(cross, style=guide_style) - - label = self.render_label(line.path[-1]).copy() - label.stylize(self.get_component_rich_style("tree--label", partial=True)) - if self.hover_line == y: - label.stylize( - self.get_component_rich_style("tree--highlight", partial=True) + base_guide_style = self.get_component_rich_style( + "tree--guides", partial=True + ) + guide_hover_style = base_guide_style + self.get_component_rich_style( + "tree--guides-hover", partial=True + ) + guide_selected_style = base_guide_style + self.get_component_rich_style( + "tree--guides-selected", partial=True ) - if self.cursor_line == y and self.has_focus: - label.stylize(self.get_component_rich_style("tree--cursor", partial=False)) - label.stylize(Style(meta={"node": line.node.id, "line": y})) - guides.append(label) + hover = self.root._hover + selected = self.root._selected and self.has_focus + + def get_guides(style: Style) -> tuple[str, str, str, str]: + """Get the guide strings for a given style. + + Args: + style (Style): A Style object. + + Returns: + tuple[str, str, str, str]: Strings for space, vertical, terminator and cross. + """ + if self.show_guides: + lines = self.LINES["default"] + if style.bold: + lines = self.LINES["bold"] + elif style.underline2: + lines = self.LINES["double"] + else: + lines = (" ", " ", " ", " ") + + guide_depth = max(0, self.guide_depth - 2) + lines = tuple( + f"{vertical}{horizontal * guide_depth} " + for vertical, horizontal in lines + ) + return lines + + if is_hover: + line_style = self.get_component_rich_style("tree--highlight-line") + else: + line_style = base_style + + guides = Text(style=line_style) + guides_append = guides.append + + guide_style = base_guide_style + for node in line.path[1:]: + if hover: + guide_style = guide_hover_style + if selected: + guide_style = guide_selected_style + + space, vertical, _, _ = get_guides(guide_style) + guide = space if node.last else vertical + if node != line.path[-1]: + guides_append(guide, style=guide_style) + hover = hover or node._hover + selected = (selected or node._selected) and self.has_focus + + if len(line.path) > 1: + _, _, terminator, cross = get_guides(guide_style) + if line.last: + guides.append(terminator, style=guide_style) + else: + guides.append(cross, style=guide_style) + + label_style = self.get_component_rich_style("tree--label", partial=True) + if self.hover_line == y: + label_style += self.get_component_rich_style( + "tree--highlight", partial=True + ) + if self.cursor_line == y and self.has_focus: + label_style += self.get_component_rich_style( + "tree--cursor", partial=False + ) + + label = self.render_label(line.path[-1], line_style, label_style).copy() + label.stylize(Style(meta={"node": line.node.id, "line": y})) + guides.append(label) + + segments = list(guides.render(self.app.console)) + segments = line_pad( + segments, 0, self.virtual_size.width - guides.cell_len, line_style + ) + self._line_cache[cache_key] = segments - segments = list(guides.render(self.app.console)) - segments = line_pad(segments, 0, width - guides.cell_len, line_style) segments = line_crop(segments, x1, x2, width) - self._line_cache[cache_key] = segments return segments def _on_resize(self) -> None: