Spaces:
Running
Running
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> | |
| <meta name="generator" content="Observable Framework v1.13.4"> | |
| <title>Country Explorer | Michelin Star Per-Capita Dashboard</title> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" crossorigin> | |
| <link rel="preload" as="style" href="./_import/style.7818a7a2.css"> | |
| <link rel="preload" as="style" href="./_observablehq/stdlib/inputs.f7bf0e12.css"> | |
| <link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900&display=swap" crossorigin> | |
| <link rel="stylesheet" type="text/css" href="./_import/style.7818a7a2.css"> | |
| <link rel="stylesheet" type="text/css" href="./_observablehq/stdlib/inputs.f7bf0e12.css"> | |
| <link rel="modulepreload" href="./_observablehq/client.793bdc92.js"> | |
| <link rel="modulepreload" href="./_observablehq/runtime.607a8173.js"> | |
| <link rel="modulepreload" href="./_observablehq/stdlib.23790ac1.js"> | |
| <link rel="modulepreload" href="./_npm/htl@1.0.0/11521f02.js"> | |
| <link rel="modulepreload" href="./_observablehq/stdlib/inputs.f7175400.js"> | |
| <link rel="modulepreload" href="./_npm/@observablehq/plot@0.6.17/93ce672e.js"> | |
| <link rel="modulepreload" href="./_npm/isoformat@0.2.1/604cb63b.js"> | |
| <link rel="modulepreload" href="./_npm/d3@7.9.0/080cf928.js"> | |
| <link rel="modulepreload" href="./_npm/interval-tree-1d@1.0.4/f53f17b2.js"> | |
| <link rel="modulepreload" href="./_npm/d3-array@3.2.4/b8e751b6.js"> | |
| <link rel="modulepreload" href="./_npm/d3-axis@3.0.0/c662ea05.js"> | |
| <link rel="modulepreload" href="./_npm/d3-brush@3.0.0/d162d8c6.js"> | |
| <link rel="modulepreload" href="./_npm/d3-chord@3.0.1/5cedd0be.js"> | |
| <link rel="modulepreload" href="./_npm/d3-color@3.1.0/65d51007.js"> | |
| <link rel="modulepreload" href="./_npm/d3-contour@4.0.2/c576a91a.js"> | |
| <link rel="modulepreload" href="./_npm/d3-delaunay@6.0.4/8b43826d.js"> | |
| <link rel="modulepreload" href="./_npm/d3-dispatch@3.0.1/3fb06f14.js"> | |
| <link rel="modulepreload" href="./_npm/d3-drag@3.0.0/20b76948.js"> | |
| <link rel="modulepreload" href="./_npm/d3-dsv@3.0.1/097745a2.js"> | |
| <link rel="modulepreload" href="./_npm/d3-ease@3.0.1/8683b012.js"> | |
| <link rel="modulepreload" href="./_npm/d3-fetch@3.0.1/5c319046.js"> | |
| <link rel="modulepreload" href="./_npm/d3-force@3.0.0/33924f3e.js"> | |
| <link rel="modulepreload" href="./_npm/d3-format@3.1.2/8d67a1ae.js"> | |
| <link rel="modulepreload" href="./_npm/d3-geo@3.1.1/8037bd0b.js"> | |
| <link rel="modulepreload" href="./_npm/d3-hierarchy@3.1.2/a9ba0229.js"> | |
| <link rel="modulepreload" href="./_npm/d3-interpolate@3.0.1/e0a47f97.js"> | |
| <link rel="modulepreload" href="./_npm/d3-path@3.1.0/f9e9e0ab.js"> | |
| <link rel="modulepreload" href="./_npm/d3-polygon@3.0.1/2da8eb3b.js"> | |
| <link rel="modulepreload" href="./_npm/d3-quadtree@3.0.1/09002e01.js"> | |
| <link rel="modulepreload" href="./_npm/d3-random@3.0.1/5108797c.js"> | |
| <link rel="modulepreload" href="./_npm/d3-scale@4.0.2/c9f2571b.js"> | |
| <link rel="modulepreload" href="./_npm/d3-scale-chromatic@3.1.0/f3c78e0d.js"> | |
| <link rel="modulepreload" href="./_npm/d3-selection@3.0.0/28717d05.js"> | |
| <link rel="modulepreload" href="./_npm/d3-shape@3.2.0/7c6e2a50.js"> | |
| <link rel="modulepreload" href="./_npm/d3-time@3.1.0/3d1d57c6.js"> | |
| <link rel="modulepreload" href="./_npm/d3-time-format@4.1.0/643581a8.js"> | |
| <link rel="modulepreload" href="./_npm/d3-timer@3.0.1/e99232db.js"> | |
| <link rel="modulepreload" href="./_npm/d3-transition@3.0.1/bd07fd9a.js"> | |
| <link rel="modulepreload" href="./_npm/d3-zoom@3.0.0/d09f7cc9.js"> | |
| <link rel="modulepreload" href="./_npm/binary-search-bounds@2.0.5/5f640032.js"> | |
| <link rel="modulepreload" href="./_npm/internmap@2.0.3/af036d7e.js"> | |
| <link rel="modulepreload" href="./_npm/delaunator@5.1.0/7b06abe7.js"> | |
| <link rel="modulepreload" href="./_npm/robust-predicates@3.0.3/b42f30a7.js"> | |
| <script type="module"> | |
| import {define} from "./_observablehq/client.793bdc92.js"; | |
| import {registerFile} from "./_observablehq/stdlib.23790ac1.js"; | |
| registerFile("./data/country_metrics.json", {"name":"./data/country_metrics.json","mimeType":"application/json","path":"./_file/data/country_metrics.156e9867.json","lastModified":1784512367150,"size":17835}); | |
| define({id: "ef5edd4f", inputs: ["FileAttachment","view","Inputs"], outputs: ["countryMetrics","tierOptions","metricOptions","sortOptions","scopeOptions","metricLabels","allCountryNames","tierField","metric","sortDirection","minTierRestaurants","rowLimit","displayScope","focusCountry"], body: async (FileAttachment,view,Inputs) => { | |
| const countryMetrics = await FileAttachment("./data/country_metrics.json").json(); | |
| const tierOptions = [ | |
| "total_restaurants", | |
| "one_star_restaurants", | |
| "two_star_restaurants", | |
| "three_star_restaurants" | |
| ]; | |
| const metricOptions = [ | |
| "total_restaurants", | |
| "total_stars", | |
| "one_star_restaurants", | |
| "two_star_restaurants", | |
| "three_star_restaurants", | |
| "stars_per_100k", | |
| "stars_per_10b_gdp", | |
| "gdp_per_capita", | |
| "population", | |
| "gdp" | |
| ]; | |
| const sortOptions = ["descending", "ascending"]; | |
| const scopeOptions = ["top", "all", "single"]; | |
| const metricLabels = { | |
| total_restaurants: "Aggregate restaurants", | |
| total_stars: "Total stars", | |
| one_star_restaurants: "1-star restaurants", | |
| two_star_restaurants: "2-star restaurants", | |
| three_star_restaurants: "3-star restaurants", | |
| stars_per_100k: "Stars per 100k residents", | |
| stars_per_10b_gdp: "Stars per $10B GDP", | |
| gdp_per_capita: "GDP per capita", | |
| population: "Population", | |
| gdp: "GDP current USD" | |
| }; | |
| const allCountryNames = countryMetrics | |
| .map((d) => d.country_name) | |
| .sort((a, b) => a.localeCompare(b)); | |
| const tierField = view( | |
| Inputs.select(tierOptions, { label: "Star tier", value: "total_restaurants" }) | |
| ); | |
| const metric = view( | |
| Inputs.select(metricOptions, { label: "Chart metric", value: "total_restaurants" }) | |
| ); | |
| const sortDirection = view(Inputs.select(sortOptions, { label: "Sort", value: "descending" })); | |
| const minTierRestaurants = view( | |
| Inputs.range([1, 100], { | |
| step: 1, | |
| label: "Minimum tier restaurants", | |
| value: 10 | |
| }) | |
| ); | |
| const rowLimit = view(Inputs.range([10, 52], { step: 1, label: "Rows shown", value: 25 })); | |
| const displayScope = view(Inputs.select(scopeOptions, { label: "Display", value: "top" })); | |
| const focusCountry = view(Inputs.select(allCountryNames, { label: "Country", value: "France" })); | |
| return {countryMetrics,tierOptions,metricOptions,sortOptions,scopeOptions,metricLabels,allCountryNames,tierField,metric,sortDirection,minTierRestaurants,rowLimit,displayScope,focusCountry}; | |
| }}); | |
| define({id: "0a5d9bf6", inputs: ["countryMetrics","displayScope","focusCountry","tierField","minTierRestaurants","metric","sortDirection","rowLimit"], outputs: ["filteredRows","visibleRows","chartRows","chartDomain"], body: (countryMetrics,displayScope,focusCountry,tierField,minTierRestaurants,metric,sortDirection,rowLimit) => { | |
| const filteredRows = countryMetrics | |
| .filter((d) => displayScope !== "single" || d.country_name === focusCountry) | |
| .filter((d) => Number(d[tierField] ?? 0) >= minTierRestaurants) | |
| .toSorted((a, b) => { | |
| const left = Number(a[metric] ?? 0); | |
| const right = Number(b[metric] ?? 0); | |
| return sortDirection === "ascending" ? left - right : right - left; | |
| }); | |
| const visibleRows = displayScope === "all" ? filteredRows : filteredRows.slice(0, rowLimit); | |
| const chartRows = visibleRows; | |
| const chartDomain = chartRows.map((d) => d.country_name); | |
| return {filteredRows,visibleRows,chartRows,chartDomain}; | |
| }}); | |
| define({id: "21f3aac0", inputs: ["display","html","filteredRows","tierField","metricLabels"], body: (display,html,filteredRows,tierField,metricLabels) => { | |
| display( | |
| html`<div class="summary-row"> | |
| <div><strong>${filteredRows.length}</strong><span>matching countries</span></div> | |
| <div> | |
| <strong>${filteredRows.reduce((sum, d) => sum + Number(d.total_restaurants ?? 0), 0)}</strong | |
| ><span>aggregate restaurants</span> | |
| </div> | |
| <div> | |
| <strong>${filteredRows.reduce((sum, d) => sum + Number(d.total_stars ?? 0), 0)}</strong | |
| ><span>total stars</span> | |
| </div> | |
| <div> | |
| <strong>${filteredRows.reduce((sum, d) => sum + Number(d[tierField] ?? 0), 0)}</strong | |
| ><span>${metricLabels[tierField]}</span> | |
| </div> | |
| </div>` | |
| ); | |
| }}); | |
| define({id: "bad467d5", inputs: ["display","html","Plot","chartRows","metricLabels","metric","chartDomain"], body: (display,html,Plot,chartRows,metricLabels,metric,chartDomain) => { | |
| display( | |
| html`<div class="chart-frame"> | |
| ${Plot.plot({ | |
| theme: "dark", | |
| width: 1120, | |
| height: Math.max(440, chartRows.length * 32), | |
| marginLeft: 170, | |
| x: { | |
| grid: true, | |
| label: metricLabels[metric], | |
| type: metric === "population" || metric === "gdp" ? "log" : "linear" | |
| }, | |
| y: { label: null, domain: chartDomain }, | |
| marks: [ | |
| Plot.ruleX([0]), | |
| Plot.barX(chartRows, { | |
| x: metric, | |
| y: "country_name", | |
| fill: "#22d3ee", | |
| title: (d) => | |
| `${d.country_name}\n${metricLabels[metric]}: ${Number(d[metric]).toLocaleString(undefined, { maximumFractionDigits: 4 })}\n1-star: ${d.one_star_restaurants}\n2-star: ${d.two_star_restaurants}\n3-star: ${d.three_star_restaurants}` | |
| }), | |
| Plot.text(chartRows, { | |
| x: metric, | |
| y: "country_name", | |
| text: (d) => Number(d[metric]).toLocaleString(undefined, { maximumFractionDigits: 2 }), | |
| dx: 6, | |
| fill: "#f8fafc", | |
| textAnchor: "start" | |
| }) | |
| ] | |
| })} | |
| </div>` | |
| ); | |
| }}); | |
| define({id: "88f9d25f", inputs: ["display","Inputs","visibleRows"], body: (display,Inputs,visibleRows) => { | |
| display( | |
| Inputs.table(visibleRows, { | |
| columns: [ | |
| "country_name", | |
| "country", | |
| "total_restaurants", | |
| "total_stars", | |
| "one_star_restaurants", | |
| "two_star_restaurants", | |
| "three_star_restaurants", | |
| "stars_per_100k", | |
| "stars_per_10b_gdp", | |
| "gdp_per_capita" | |
| ], | |
| format: { | |
| stars_per_100k: (d) => Number(d).toFixed(4), | |
| stars_per_10b_gdp: (d) => Number(d).toFixed(4), | |
| gdp_per_capita: (d) => `$${Number(d).toLocaleString(undefined, { maximumFractionDigits: 0 })}` | |
| } | |
| }) | |
| ); | |
| }}); | |
| </script> | |
| </head> | |
| <body> | |
| <input id="observablehq-sidebar-toggle" type="checkbox" title="Toggle sidebar"> | |
| <label id="observablehq-sidebar-backdrop" for="observablehq-sidebar-toggle"></label> | |
| <nav id="observablehq-sidebar"> | |
| <ol> | |
| <label id="observablehq-sidebar-close" for="observablehq-sidebar-toggle"></label> | |
| <li class="observablehq-link"><a href="./">Michelin Star Per-Capita Dashboard</a></li> | |
| </ol> | |
| <ol> | |
| <li class="observablehq-link"><a href="./nz">NZ Aggregate</a></li> | |
| <li class="observablehq-link"><a href="./gdp-stars">GDP & Stars</a></li> | |
| <li class="observablehq-link observablehq-link-active"><a href="./explorer">Country Explorer</a></li> | |
| <li class="observablehq-link"><a href="./pivot">Pivot Table</a></li> | |
| <li class="observablehq-link"><a href="./sources">Data & Sources</a></li> | |
| </ol> | |
| </nav> | |
| <script>{const e=document.querySelector("#observablehq-sidebar"),o=document.querySelector("#observablehq-sidebar-toggle"),r=sessionStorage.getItem("observablehq-sidebar");r?o.checked=r==="true":o.indeterminate=!0;for(const t of document.querySelectorAll("#observablehq-sidebar summary")){const s=t.parentElement;switch(sessionStorage.getItem(`observablehq-sidebar:${t.textContent}`)){case"true":s.open=!0;break;case"false":s.classList.contains("observablehq-section-active")||(s.open=!1);break}}addEventListener("beforeunload",()=>sessionStorage.setItem("observablehq-sidebar-scrolly",`${e.scrollTop}`));const a=sessionStorage.getItem("observablehq-sidebar-scrolly");a!=null&&(e.style.cssText="overflow: hidden;",e.scrollTop=+a,e.style.cssText="");}</script> | |
| <div id="observablehq-center"> | |
| <aside id="observablehq-toc" data-selector="h1:not(:first-of-type)[id], h2:first-child[id], :not(h1) + h2[id]"> | |
| <nav> | |
| <div>Contents</div> | |
| <ol> | |
| <li class="observablehq-secondary-link"><a href="#controls">Controls</a></li> | |
| <li class="observablehq-secondary-link"><a href="#filtered-countries">Filtered Countries</a></li> | |
| </ol> | |
| </nav> | |
| </aside> | |
| <main id="observablehq-main" class="observablehq"> | |
| <h1 id="country-explorer" tabindex="-1"><a class="observablehq-header-anchor" href="#country-explorer">Country Explorer</a></h1> | |
| <p>Filter the aggregate country dataset by star tier, displayed countries, metric, and sort order.</p> | |
| <h2 id="controls" tabindex="-1"><a class="observablehq-header-anchor" href="#controls">Controls</a></h2> | |
| <div class="observablehq observablehq--block"><!--:ef5edd4f:--></div> | |
| <div class="observablehq observablehq--block"><!--:0a5d9bf6:--></div> | |
| <div class="observablehq observablehq--block"><!--:21f3aac0:--></div> | |
| <div class="observablehq observablehq--block"><!--:bad467d5:--></div> | |
| <h2 id="filtered-countries" tabindex="-1"><a class="observablehq-header-anchor" href="#filtered-countries">Filtered Countries</a></h2> | |
| <div class="observablehq observablehq--block"><!--:88f9d25f:--></div> | |
| </main> | |
| <footer id="observablehq-footer"> | |
| <nav><a rel="prev" href="./gdp-stars"><span>GDP & Stars</span></a><a rel="next" href="./pivot"><span>Pivot Table</span></a></nav> | |
| <div>Built with <a href="https://observablehq.com/" target="_blank" rel="noopener noreferrer">Observable</a> on <a title="2026-07-20T01:52:42">Jul 20, 2026</a>.</div> | |
| </footer> | |
| </div> | |
| </body> | |
| </html> | |