Jump to content

Module:WhiskerWiki growth

From WhiskerWiki

Documentation for this module may be created at Module:WhiskerWiki growth/doc

-- Module:WhiskerWiki growth
--[[
Renders WhiskerWiki's own growth numbers from JSON:WhiskerWiki_growth, which the
R pipeline refreshes weekly (scripts/12_wiki_growth.R). What each count means
is defined once, in helper_functions/00_wiki_growth_helper_functions.R, and
copied into the JSON's "definitions" block -- this module only displays.

  p.card       the "WhiskerWiki at a glance" box for the Main Page
               (Template:WhiskerWiki growth card)
  p.show       numbers and blocks for the growth page (Template:WhiskerWiki growth)
  p.bar_chart  one bar per year, scaled to the tallest; shared by the card's
               mini chart and the growth page

Colors are --ww-chart-* tokens in MediaWiki:Common.css (light + dark).

Ported from HopperWiki's Module:HopperWiki growth -- same JSON shape and
blocks; the figures, labels and themes are WhiskerWiki's (species, higher
taxa, identification guides, places; images, audio recordings, documents).
--]]
local json = require("Module:JSON pipeline")
local p    = {}

local PAGE         = "JSON:WhiskerWiki_growth"
local DEFAULT_LINK = "WhiskerWiki:Growth"
local SITE         = "WhiskerWiki"
-- The small mark after the card title (like HopperWiki's). nil = no logo.
local LOGO         = "File:WhiskerWiki logo no text.png"

local MONTHS = { "January", "February", "March", "April", "May", "June", "July",
                 "August", "September", "October", "November", "December" }

-- json.load() hands numbers back as strings; everything numeric goes through here.
local function num(x)
    return tonumber(x) or 0
end

local function fmt_number(n)
    if n == nil then return "—" end
    local s = tostring(math.floor(num(n) + 0.5)):reverse():gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "")
    return s   -- one value only: gsub's second return (the match count) would leak into callers' argument lists
end

-- "2022-11" -> "November 2022"
local function month_name(period)
    local y, m = tostring(period or ""):match("^(%d%d%d%d)%-(%d%d)")
    if not y then return nil end
    return (MONTHS[tonumber(m)] or "") .. " " .. y
end

local function list_of(data, key)
    return type(data[key]) == "table" and data[key] or {}
end

local function is_true(x)
    return x == true or x == "true"
end

--[[
Year-end rows (the latest row of each year), oldest first:
{ {year = 2022, row = {...}}, ... }. The current year's entry is its latest row.
--]]
function p.yearly_rows(history)
    local by_year, years = {}, {}
    for _, row in ipairs(history or {}) do
        local y = tonumber(tostring(row.period or ""):sub(1, 4))
        if y then
            if not by_year[y] then table.insert(years, y) end
            if not by_year[y] or row.period > by_year[y].period then by_year[y] = row end
        end
    end
    table.sort(years)
    local out = {}
    for _, y in ipairs(years) do table.insert(out, { year = y, row = by_year[y] }) end
    return out
end

-- How much `measure` grew in `current`'s calendar year: current minus the last row of any earlier year.
-- Returns delta, year -- or nil when there's no earlier year to compare with.
local function this_year_delta(history, current, measure)
    local year = tostring(current.period or ""):sub(1, 4)
    local base
    for _, row in ipairs(history or {}) do
        if tostring(row.period):sub(1, 4) < year and (not base or row.period > base.period) then base = row end
    end
    if not base or current[measure] == nil or base[measure] == nil then return nil end
    return num(current[measure]) - num(base[measure]), year
end

--[[
Vertical bars, one per yearly row, scaled to the tallest.
  rows     from p.yearly_rows()
  measure  a history field, e.g. "articles"
  opts     noun         word used in each bar's hover text ("articles")
           show_values  print the value above each bar
           class        extra class on the wrapper (size variants)
--]]
function p.bar_chart(rows, measure, opts)
    opts = opts or {}
    local max = 0
    for _, r in ipairs(rows) do max = math.max(max, num(r.row[measure])) end
    if max == 0 then return "" end

    local bars = {}
    for _, r in ipairs(rows) do
        local v = num(r.row[measure])
        local height = math.max(2, math.floor(v / max * 100 + 0.5))   -- a zero year still shows a sliver
        table.insert(bars, string.format(
            '<div class="ww-growth-bar-col" title="%s: %s %s">' ..
                '<div class="ww-growth-bar-value">%s</div>' ..
                '<div class="ww-growth-bar-track"><div class="ww-growth-bar" style="height:%d%%;"></div></div>' ..
                '<div class="ww-growth-bar-year">%s</div>' ..
            '</div>',
            r.year, fmt_number(v), mw.text.encode(opts.noun or measure),
            opts.show_values and fmt_number(v) or "",
            height, r.year
        ))
    end
    return '<div class="ww-growth-bars' .. (opts.class and (" " .. opts.class) or "") .. '">' ..
        table.concat(bars) .. '</div>'
end

local function cell(label, value, note)
    return string.format(
        '<div class="ib-h-cell"><div class="ib-h-label">%s</div><div class="ib-h-value ww-growth-value">%s</div>%s</div>',
        label, value, note and ('<div class="ww-growth-note">' .. note .. '</div>') or ""
    )
end

--[[
The range-map strip: what's behind the generated state and county pages (data.geography). "" when the JSON
has no geography block (the pipeline machine lacked the shapefile outputs that week).
--]]
local function map_strip(data)
    local g = list_of(data, "geography")
    if not g.range_maps then return "" end
    local function stat_html(value, label)
        return string.format('<span class="ww-growth-map-stat"><b>%s</b> %s</span>', fmt_number(value), label)
    end
    return '<div class="ww-growth-map">' ..
        '<div class="ww-growth-caption"><span>Mapped from hand-curated range maps</span></div>' ..
        '<div class="ww-growth-map-stats">' ..
            stat_html(g.range_maps, "species range maps") ..
            stat_html(g.counties_mapped, "counties in " .. fmt_number(g.states_mapped) .. " states") ..
            stat_html(g.species_county_records, "species–county records") ..
        '</div>' ..
        '<div class="ww-growth-map-note">Every range is intersected with every state and county boundary, ' ..
            'and every place is linked to Wikidata.</div>' ..
    '</div>'
end

--[[
The Main Page card. Optional args (direct or from the calling template):
  link   page the "See how we've grown" link points to (default DEFAULT_LINK)
--]]
function p.card(frame)
    local args   = frame.args or {}
    local parent = frame:getParent() and frame:getParent().args or {}
    -- An empty link= (e.g. forwarded by a proxy template like Template:Wwg) means "use the default".
    local function given(x) return x ~= nil and mw.text.trim(x) ~= "" and x or nil end
    local link   = given(args.link) or given(parent.link) or DEFAULT_LINK

    local data, err = json.load(PAGE)
    if not data then
        return '<span class="error">' .. SITE .. ' growth data unavailable: ' .. tostring(err) .. '</span>'
    end

    local current = list_of(data, "current")
    local catalog = list_of(data, "catalog")
    local history = list_of(data, "history")
    local guides  = list_of(list_of(data, "themes"), "guides")

    local function grew(measure)
        local delta, year = this_year_delta(history, current, measure)
        if delta and delta > 0 then return "+" .. fmt_number(delta) .. " in " .. year end
        return nil
    end

    -- The link only appears once its page exists, so the card never shows a red link while that page is unwritten.
    local target = mw.title.new(link)
    local link_html = (target and target.exists)
        and string.format('<span class="ww-growth-link">[[%s|See how we\'ve grown →]]</span>', link)
        or ""
    local logo_html = LOGO
        and string.format('<span class="ww-growth-logo">[[%s|x20px|link=WhiskerWiki:About|alt=%s logo]]</span>', LOGO, SITE)
        or ""

    local header = string.format(
        '<div class="ib-h-header">' ..
            '<span class="ib-h-badge">WW</span>' ..
            '<span class="ww-growth-title">%s</span>' ..
            '%s%s' ..
        '</div>',
        data.online_since and ("Growing since " .. month_name(data.online_since)) or (SITE .. " at a glance"),
        logo_html, link_html
    )

    local cells = table.concat({
        cell("Articles",              fmt_number(current.articles),          grew("articles")),
        cell("Species write-ups",     fmt_number(current.species_write_ups), grew("species_write_ups")),
        cell("Species covered",       fmt_number(catalog.species_pages)),
        cell("Photos &amp; images",   fmt_number(current.images),            grew("images")),
        cell("Identification guides", fmt_number(guides.articles)),
        cell("Audio recordings",      fmt_number(current.audio),             grew("audio")),
    })

    local chart = p.bar_chart(p.yearly_rows(history), "articles", { noun = "articles", show_values = true })

    local html = string.format(
        '<div class="ib-h ww-growth-box">' ..
            '%s' ..
            '<div class="ib-h-cells ib-h-cols-3">%s</div>' ..
            '%s' ..
            '<div class="ww-growth-chart">' ..
                '<div class="ww-growth-caption"><span>Articles by year</span><span>Updated %s</span></div>' ..
                '%s' ..
            '</div>' ..
        '</div>',
        header, cells, map_strip(data), mw.text.encode(data.last_updated or "unknown"), chart
    )

    return frame:preprocess(html)
end

--[[ ==========================================================================
WhiskerWiki:Growth page blocks -- {{WhiskerWiki growth|<what>}} -> p.show
============================================================================ ]]

-- "2026-09-10T17:45:42Z" -> "September 10, 2026" (month = true -> "September 2026")
local function date_text(stamp, month_only)
    local y, m, d = tostring(stamp or ""):match("^(%d%d%d%d)%-(%d%d)%-(%d%d)")
    if not y then
        y, m = tostring(stamp or ""):match("^(%d%d%d%d)%-(%d%d)")
        if not y then return "" end
        month_only = true
    end
    local month = MONTHS[tonumber(m)] or ""
    if month_only then return month .. " " .. y end
    return string.format("%s %d, %s", month, tonumber(d), y)
end

local function ordinal(n)
    local s = fmt_number(n)
    local last2, last = n % 100, n % 10
    if last2 >= 11 and last2 <= 13 then return s .. "th" end
    return s .. (({ "st", "nd", "rd" })[last] or "th")
end

-- Articles sorted oldest first by the date they qualified (undated ones left out).
local function dated_articles(data, filter)
    local out = {}
    for _, a in ipairs(list_of(data, "articles")) do
        if a.since and a.since ~= "" and (not filter or filter(a)) then table.insert(out, a) end
    end
    table.sort(out, function(x, y) return x.since < y.since end)
    return out
end

---------------------------------------------------------------------------
-- Inline numbers: {{WhiskerWiki growth|articles}}, {{WhiskerWiki growth|total_words|floor=10000}}
---------------------------------------------------------------------------
local STAT_ALIASES = { species = "species_pages", guides = "guide_pages", counties = "county_pages", states = "state_pages" }

-- Every figure the JSON is meant to carry, so a missing one can be told apart from a typo.
local KNOWN_FIGURES = {
    articles = true, species_write_ups = true, documentation_pages = true, images = true, audio = true,
    documents = true, contributors = true, total_words = true,
    species_pages = true, higher_taxon_pages = true, guide_pages = true, state_pages = true, county_pages = true,
    geography_pages = true,
    species_articles = true, higher_taxa_articles = true, guides_articles = true, geography_articles = true,
    range_maps = true, states_mapped = true, counties_mapped = true, species_county_records = true,
    median_species_per_county = true,
}

-- Whole months from the first month online (online_since) to the last update, or nil.
local function months_online(data)
    local y0, m0 = tostring(data.online_since or ""):match("^(%d%d%d%d)%-(%d%d)")
    local y1, m1 = tostring(data.last_updated or ""):match("^(%d%d%d%d)%-(%d%d)")
    if not y0 or not y1 then return nil end
    return (tonumber(y1) - tonumber(y0)) * 12 + (tonumber(m1) - tonumber(m0))
end

local NUMBER_WORDS = { "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
                       "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen",
                       "eighteen", "nineteen", "twenty" }

-- 47 months -> "three and a half years": rounded DOWN to the half year, so it never overstates.
-- Lowercase, for mid-sentence; wrap in {{ucfirst:...}} to start a sentence.
local function age_in_words(months)
    local halves = math.floor(months / 6)
    local years, half = math.floor(halves / 2), halves % 2 == 1
    if years == 0 then return half and "half a year" or "less than half a year" end
    if years == 1 then return half and "a year and a half" or "one year" end
    local n = NUMBER_WORDS[years] or tostring(years)
    return n .. (half and " and a half years" or " years")
end

local function stat(data, key, args)
    key = STAT_ALIASES[key] or key
    local current, catalog = list_of(data, "current"), list_of(data, "catalog")
    if key == "last_updated" then return date_text(data.last_updated) end
    if key == "years_online" or key == "age" then
        local months = months_online(data)
        if not months then return "" end
        if key == "age" then return age_in_words(months) end
        return tostring(math.floor(months / 12))
    end
    if not KNOWN_FIGURES[key] then
        return '<span class="error">Unknown ' .. SITE .. ' growth figure: ' .. mw.text.encode(key) .. '</span>'
    end
    local theme = key:match("^(.+)_articles$")
    local theme_row = theme and list_of(list_of(data, "themes"), theme) or {}
    local value = current[key] or catalog[key] or list_of(data, "geography")[key] or data[key] or theme_row.articles
    if value == nil then
        return '<span class="error">' .. mw.text.encode(key) .. ' is not in ' .. PAGE ..
            ' yet -- rerun scripts/12_wiki_growth.R with WW_GROWTH_FORCE=1</span>'
    end
    local v = num(value)
    local floor_to = tonumber(args.floor)
    if floor_to and floor_to > 0 then v = math.floor(v / floor_to) * floor_to end
    return fmt_number(v)
end

---------------------------------------------------------------------------
-- charts: one small yearly chart per measure, with its total and this year's growth
---------------------------------------------------------------------------
local CHARTS = {
    { measure = "articles",          title = "Articles",          noun = "articles" },
    { measure = "species_write_ups", title = "Species write-ups", noun = "species write-ups" },
    { measure = "images",            title = "Photos & images",   noun = "images" },
    { measure = "audio",             title = "Audio recordings",  noun = "audio recordings" },
}

local function charts(data)
    local history, current = list_of(data, "history"), list_of(data, "current")
    local rows = p.yearly_rows(history)
    local out = {}
    for _, c in ipairs(CHARTS) do
        local delta, year = this_year_delta(history, current, c.measure)
        local note = (delta and delta > 0) and ('<div class="ww-growth-note">+' .. fmt_number(delta) .. ' in ' .. year .. '</div>') or ""
        table.insert(out, string.format(
            '<div class="ww-growth-panel">' ..
                '<div class="ib-h-label">%s</div>' ..
                '<div class="ib-h-value ww-growth-value">%s</div>%s' ..
                '%s' ..
            '</div>',
            mw.text.encode(c.title), fmt_number(current[c.measure]), note,
            p.bar_chart(rows, c.measure, { noun = c.noun, show_values = true })
        ))
    end
    return '<div class="ww-growth-panels">' .. table.concat(out) .. '</div>'
end

---------------------------------------------------------------------------
-- milestones: generated from the dated article list (exact day + which article) and the monthly history
---------------------------------------------------------------------------
local MILESTONES = {
    { measure = "articles",          from = "articles", noun = "article",          at = { 1, 25, 50, 100, 150, 200, 250, 300, 400, 500, 750, 1000 } },
    { measure = "species_write_ups", from = "articles", noun = "species write-up", at = { 1, 10, 25, 50, 75, 100, 150, 200, 300, 500 },
      filter = function(a) return is_true(a.species) end },
    { measure = "guides",            from = "articles", noun = "identification guide", at = { 1, 5, 10, 25, 50, 100 },
      filter = function(a) return is_true(a.guide) end },
    { measure = "images",            from = "history",  noun = "image",            at = { 1, 100, 500, 1000, 2000, 3000, 5000, 7500, 10000 } },
    { measure = "audio",             from = "history",  noun = "audio recording",  at = { 1, 10, 25, 50, 100, 250, 500 } },
}

local function milestones(data)
    local events = {}
    for _, m in ipairs(MILESTONES) do
        if m.from == "articles" then
            local list = dated_articles(data, m.filter)
            for _, k in ipairs(m.at) do
                local a = list[k]
                if a then
                    local what = (k == 1) and ("First " .. m.noun) or (ordinal(k) .. " " .. m.noun)
                    table.insert(events, { sort = a.since, when = date_text(a.since), what = what, title = a.title })
                end
            end
        else
            local history = list_of(data, "history")
            for _, k in ipairs(m.at) do
                for _, row in ipairs(history) do     -- history is oldest first
                    if num(row[m.measure]) >= k then
                        local what = (k == 1) and ("First " .. m.noun) or (ordinal(k) .. " " .. m.noun)
                        table.insert(events, { sort = row.period, when = date_text(row.period, true), what = what })
                        break
                    end
                end
            end
        end
    end
    if #events == 0 then return "" end
    table.sort(events, function(x, y) return x.sort < y.sort end)

    -- One page reaching two milestones at once (the first article was also the first species write-up) is one line.
    local merged, by_page = {}, {}
    for _, e in ipairs(events) do
        local key = e.title and (e.sort .. "|" .. e.title)
        if key and by_page[key] then
            by_page[key].what = by_page[key].what .. " and " .. e.what:gsub("^First", "first")
        else
            table.insert(merged, e)
            if key then by_page[key] = e end
        end
    end

    local items = {}
    for _, e in ipairs(merged) do
        local text = e.title and string.format("%s: [[%s]]", e.what, e.title) or e.what
        table.insert(items, string.format(
            '<li><span class="ww-growth-when">%s</span><span class="ww-growth-what">%s</span></li>', e.when, text))
    end
    return '<ul class="ww-growth-milestones">' .. table.concat(items) .. '</ul>'
end

---------------------------------------------------------------------------
-- recent: newest articles, tagged species / guide
--   n = how many articles to list (default 8)
---------------------------------------------------------------------------
local function recent(data, args)
    local n = tonumber(args.n) or 8
    local all = dated_articles(data)

    local items = {}
    for i = #all, math.max(1, #all - n + 1), -1 do
        local a = all[i]
        local tag = is_true(a.species) and ' <span class="ww-growth-tag">species</span>'
            or is_true(a.guide) and ' <span class="ww-growth-tag">guide</span>'
            or ""
        table.insert(items, string.format(
            '<li><span class="ww-growth-when">%s</span><span class="ww-growth-what">[[%s]]%s</span></li>',
            date_text(a.since), a.title, tag))
    end
    return '<ul class="ww-growth-recent">' .. table.concat(items) .. '</ul>'
end

---------------------------------------------------------------------------
-- almost there: pages 50-99 words long, species first, each with a progress bar toward 100 words
--   n = max pages per group (default 10)
---------------------------------------------------------------------------
local ARTICLE_MIN_WORDS = 100   -- mirrors GROWTH_ARTICLE_MIN_WORDS in the R helper

local function almost_there(data, args)
    local n = tonumber(args.n) or 10
    local species, other = {}, {}
    for _, row in ipairs(list_of(data, "almost_there")) do
        table.insert(is_true(row.species) and species or other, row)
    end
    local function group(title, rows)
        if #rows == 0 then return "" end
        local items = {}
        for i = 1, math.min(#rows, n) do
            local words = num(rows[i].words)
            local to_go = math.max(0, ARTICLE_MIN_WORDS - words)
            table.insert(items, string.format(
                '<li><span class="ww-growth-what">[[%s]]</span>' ..
                '<span class="ww-growth-progress" title="%d of %d words"><span style="width:%d%%;"></span></span>' ..
                '<span class="ww-growth-togo">%d word%s to go</span></li>',
                rows[i].title, words, ARTICLE_MIN_WORDS, math.floor(words / ARTICLE_MIN_WORDS * 100), to_go, to_go == 1 and "" or "s"))
        end
        return '<div class="ww-growth-almost-group"><div class="ib-h-label">' .. title .. '</div>' ..
            '<ul class="ww-growth-almost">' .. table.concat(items) .. '</ul></div>'
    end
    return group("Species", species) .. group("Other pages", other)
end

---------------------------------------------------------------------------
-- catalog: what WhiskerWiki covers, as a cell grid like the card
---------------------------------------------------------------------------
local CATALOG = {
    { key = "species_pages",      label = "Species" },
    { key = "higher_taxon_pages", label = "Higher taxa" },
    { key = "guide_pages",        label = "Identification guides" },
    { key = "state_pages",        label = "States" },
    { key = "county_pages",       label = "Counties" },
}

local function catalog(data)
    local c = list_of(data, "catalog")
    local cells = {}
    for _, item in ipairs(CATALOG) do
        if c[item.key] then table.insert(cells, cell(item.label, fmt_number(c[item.key]))) end
    end
    return '<div class="ib-h ww-growth-box"><div class="ib-h-cells ib-h-cols-3">' .. table.concat(cells) .. '</div></div>'
end

---------------------------------------------------------------------------
-- definitions: the rules behind every number, straight from the JSON
---------------------------------------------------------------------------
-- Only the numbers the card and growth page show; documents, contributors and documentation pages are still
-- defined in the JSON for anyone using them inline.
local DEFINITIONS = {
    { key = "articles",            label = "Article" },
    { key = "species_write_ups",   label = "Species write-up" },
    { key = "images",              label = "Photos & images" },
    { key = "audio",               label = "Audio recordings" },
    { key = "geography",           label = "States and counties" },
    { key = "reconstructed",       label = "Reconstructed history" },
    { key = "snapshot",            label = "Weekly snapshots" },
}

local function definitions(data)
    local defs = list_of(data, "definitions")
    local items = {}
    for _, d in ipairs(DEFINITIONS) do
        if defs[d.key] then table.insert(items, string.format("<li><b>%s:</b> %s</li>", mw.text.encode(d.label), mw.text.encode(defs[d.key]))) end
    end
    return '<ul class="ww-growth-definitions">' .. table.concat(items) .. '</ul>'
end

---------------------------------------------------------------------------
-- themes: for each main page family, how many pages exist and how many are written up as articles
---------------------------------------------------------------------------
-- Only the families people write up. Places (states, counties) are still in the JSON's themes, but every one is
-- generated by the pipeline, so its bar would sit at 0% forever.
local THEMES = {
    { key = "species",     label = "Species" },
    { key = "higher_taxa", label = "Higher taxa" },
    { key = "guides",      label = "Guides" },
}

local function percent(part, whole)
    if whole <= 0 then return "0%" end
    local pct = part / whole * 100
    if pct > 0 and pct < 10 then return string.format("%.1f%%", pct) end
    return string.format("%d%%", math.floor(pct + 0.5))
end

local function themes(data)
    local t = list_of(data, "themes")
    local rows = {}
    for _, th in ipairs(THEMES) do
        local row = t[th.key]
        if type(row) == "table" then
            local pages, articles = num(row.pages), num(row.articles)
            local width = pages > 0 and math.max(1, math.floor(articles / pages * 100 + 0.5)) or 0
            table.insert(rows, string.format(
                '<li>' ..
                    '<span class="ww-growth-theme">%s</span>' ..
                    '<span class="ww-growth-theme-count"><b>%s</b> of %s pages written up</span>' ..
                    '<span class="ww-growth-progress" title="%s of %s"><span style="width:%d%%;"></span></span>' ..
                    '<span class="ww-growth-togo">%s</span>' ..
                '</li>',
                th.label, fmt_number(articles), fmt_number(pages), fmt_number(articles), fmt_number(pages),
                width, percent(articles, pages)
            ))
        end
    end
    if #rows == 0 then return "" end
    return '<ul class="ww-growth-almost ww-growth-themes">' .. table.concat(rows) .. '</ul>'
end

local function geography(data)
    local strip = map_strip(data)
    if strip == "" then return "" end
    return '<div class="ib-h ww-growth-box">' .. strip .. '</div>'
end

local BLOCKS = {
    geography = geography, charts = charts, milestones = milestones, recent = recent,
    ["almost there"] = almost_there, almost_there = almost_there, catalog = catalog, definitions = definitions,
    themes = themes,
}

--[[
{{WhiskerWiki growth|<what>|...}}
  <what> = charts | geography | milestones | recent | almost there | themes | catalog | definitions | card
         = any figure: articles, species_write_ups, documentation_pages, images, audio, documents, contributors,
           total_words, species_pages (or species), higher_taxon_pages, guide_pages (or guides),
           state_pages (or states), county_pages (or counties), geography_pages, last_updated, years_online,
           age ("three and a half years"; {{ucfirst:...}} to start a sentence),
           species_articles, higher_taxa_articles, guides_articles, geography_articles,
           range_maps, states_mapped, counties_mapped, species_county_records, median_species_per_county
  floor=N   round a figure down to a multiple of N ("over 50,000 words")
  n=        see recent / almost there
--]]
function p.show(frame)
    local args = {}
    for k, v in pairs(frame:getParent() and frame:getParent().args or {}) do args[k] = v end
    for k, v in pairs(frame.args or {}) do args[k] = v end
    local what = mw.text.trim(tostring(args[1] or ""))
    if what == "card" then return p.card(frame) end

    local data, err = json.load(PAGE)
    if not data then
        return '<span class="error">' .. SITE .. ' growth data unavailable: ' .. tostring(err) .. '</span>'
    end

    local block = BLOCKS[what]
    if block then return frame:preprocess(block(data, args)) end
    return stat(data, what, args)
end

function p.data()
    return json.load(PAGE)
end

return p
Cookies help us deliver our services. By using our services, you agree to our use of cookies.