Grenada decriminalises cannabis
2026-02-20
Parliament of Grenada — Drug Abuse (Prevention and Control) (Amendment) Act, No. 1 of 2026 (assented 13 February, published 20 February 2026) 1 unchanged
Changelog f8116b2
67 history entries added7 history entries edited2 scores changed41 country profiles edited12 files of code, design or docs +2,185 −142 lines
2026-02-20
Parliament of Grenada — Drug Abuse (Prevention and Control) (Amendment) Act, No. 1 of 2026 (assented 13 February, published 20 February 2026) 1 unchanged
possession 1 · cultivation 0 · enforcement 1 · sharing 0 · consumption 1 · access 0
1999-12-10
Law 874873 on narcotic drugs, psychotropic substances and precursors set state drug policy after independence. The Criminal Code punishes trafficking with and without intent to sell.
Закон Республики Таджикистан № 873 «О наркотических средствах, психотропных веществах и прекурсорах», 10 December 1999 (National Legislation Centre) 1 unchanged
20001999-08-19
Закон Республики Узбекистан № 813-I «О наркотических средствах и психотропных веществах», 19 August 1999 (Lex.uz) 1 unchanged
true
true
1872-03-25
An imperial ordinance on the trade in medicines limited the sale ofpharmacy goods listed Indian hemp (Herba Cannabis Indicae) among the drugs that could be sold to the public only in pharmacies. It regulated a medicine; it did not prohibit possession or use.
Verordnung, betreffend den Verkehr mit Apothekerwaaren. Vom 25. März 1872, Reichsgesetzblatt 1872 Nr. 11 S. 85 (Wikisource)
Wikipedia — Cannabis in Germany
true
enforcement 4 → 3
Private personal use is not punishable and courts accept personal cultivation as a defence. Public use is a minor offence. A 2026 reform lets trafficking penalties reach small amounts.
Private users are not punished, but the lack of thresholds means people are still arrested and charged with micro-trafficking, which carries prison of 541 days to five years. A reform in force since April 2026 lets trafficking penalties apply to small amounts when the substance is judged capable of serious harm, so quantity protects users less than before. It is under challenge at the Constitutional Court, and in May 2026 the Supreme Court still required evidence of intent to traffic.
ep26, bio26, tni, ley
El Planteo — Chile aprobó penas más duras para el cannabis. El Tribunal Constitucional podría anularlas (8 May 2026) BioBioChile — Marihuana y TC: diputados opositores buscan frenar ley que endurece penas por porte y consumo (15 April 2026) 3 unchanged
consumption 1 → 0
Boletín Oficial — Resolución 1780/2025, Ministerio de Salud (23 May 2025) 4 unchanged
regulated
Clubs work under the medical cannabis register REPROCANN, as civil associations or foundations growing for registered patients; there is no recreational club model. Resolution 1780/2025 of the health ministry tightened the rules: an association must be legally constituted with the programme's aims in its statutes, file a list of its users with their consent, appoint a medical director and a technical officer, report lab tests for each batch and declare its plants and patients every six months, and may grow at no more than three addresses. Its certificate lasts one year, against three for individual growers. The ministry justified the change by abuse of the earlier rules, mostly by people using the solidarity-grower permit to sell.
res1780
Leafly — Why Belgians urge you to pull your plant (updated 28 July 2020) 4 unchanged
underground
Clubs have no legal basis; they lean on the directive that gives lowest priority to an adult's possession of 3 g or one plant, arguing that members may pool their one plant each. The first, Trekt Uw Plant in Antwerp, was founded in 2006, went to court twice and was acquitted in 2010; others followed in Hasselt, Liège, Andenne and Namur. In 2020 there were five registered clubs, the largest with about 400 members. The courts have not accepted the model: organisers of the Mambo Social Club, raided in 2013, were convicted of possession, production and sale, the court ruling that the tolerance for personal possession cannot be stretched to a shared growing room, and the club stopped growing.
leafly20, wikibe
grey
unclear
illegal
banned
According to a 2025 country sheet by the harm-reduction group Mainline, Article 490 of the Penal Code prohibits growing, selling, carrying, holding and using narcotic drugs except as allowed by order of the health ministry. The sheet does not mention seeds, and the code itself was not read.
ml
Sechat — 40 patient associations have habeas corpus for cannabis cultivation in Brazil (figures from the Kaya Mind Brazilian Yearbook of Medicinal Cannabis 2024) 5 unchanged
tolerated
Brazil's clubs are patient associations rather than recreational ones. No law licenses them to grow; they rely on court orders, usually preventive habeas corpus, obtained one association at a time. A 2024 industry yearbook counted 259 cannabis associations and three federations, serving about 147,000 patients, roughly a fifth of the country's medical cannabis patients. Only 40 held an individual or collective court authorisation to cultivate: 23 in the Southeast, 11 in the Northeast, 4 in the South, 2 in the Centre- West and none in the North. The rest work without that protection.
sechat40
Victoria News — Police raid Victoria Cannabis Buyers Club one day after 4/20 (21 April 2026) The Globe and Mail — Cannabis Act review and the closing of the B.C. Compassion Club Society (26 September 2022) Health Canada — Canadian Cannabis Survey 2024: Summary 4 unchanged
The legal market has taken most of the trade. In Health Canada's 2024 survey, 72% of people who used cannabis usually bought it from a legal shop or website, up from 37% in 2019, and only 3% from an illegal shop, website or dealer, down from 16%. Another 15% got it from friends or family and 5% grew their own. Users spent about 53 dollars a month on legal cannabis and 8 dollars on illegal, and paid on average 8.41 dollars for a gram of dried flower.
ccs24
underground
Legalisation built a system of licensed producers and shops and left no room for members' clubs: cannabis may only be sold with a federal or provincial licence. The medical compassion clubs that had sold openly since the 1990s were not absorbed into it, and those that carried on are treated as unlicensed sellers. The best-documented cases are in British Columbia. The B.C. Compassion Club Society in Vancouver closed in 2022 after about 25 years of selling without a licence. The Victoria Cannabis Buyers Club has been raided four times by the provincial Community Safety Unit, in 2019, 2020, 2022 and April 2026, and the courts upheld a 3.2 million dollar penalty against it in 2024; the unit says it sells contrary to both the provincial licensing act and the federal Cannabis Act. By 2022 the province reported having shut 186 unlicensed retailers. Patient groups asked the federal review of the Act to allow some form of storefront medical sale.
vn26, gm22
Prohibition Partners — Switzerland adult-use cannabis market overview 2025 (16 October 2025) 4 unchanged
regulated
Clubs exist legally only inside the scientific pilot trials allowed since 2021. Seven trials were running in 2025, in Basel, Zurich, Lausanne, Geneva (Vernier), Bern, Biel and Lucerne, with about 10,000 active buyers; each may enrol up to 5,000 participants, who may buy up to 10 g of THC a month. Cannabis social clubs are one of the permitted sales points, next to pharmacies, specialist shops and a non-profit shop in Lausanne. The trials are run by universities, cities and foundations and end between 2026 and 2028. Outside them a club has no legal footing. A parliamentary committee backed a draft legalisation law in February 2025 built on a limited number of licensed outlets and home growing, not clubs.
pp25
Cultivation remainsGrowing without authorisation carries five to ten years in prison and a criminal offencefine of 500,000 to 50 million francs under Article 3 of Law 2022-407.
l407, cr
Loi n° 2022-407 du 13 juin 2022 relative à la lutte contre le trafic et l'usage illicites des stupéfiants — incriminations et peines (Loidici.biz) 3 unchanged
grey
unclear
illegal
banned
Article 3 of Law 2022-407 punishes growing a listed drug without authorisation with five to ten years in prison and a fine of 500,000 to 50 million francs. The offence articles do not mention seeds. The text read is a chapter-by-chapter copy on a legal information site.
l407
grey
unclear
illegal
banned
Article 8 of Law 97/019 bans growing the cannabis plant anywhere in the country and obliges landholders to destroy plantations that appear on their land. The law speaks of plants and substances and does not mention seeds.
loi97
El Planteo — Clubes de cannabis en Colombia: ¿cómo funcionan? (13 July 2026) El País (Cali) — Cali: un recorrido por la 'capital' de los clubes cannábicos (15 March 2026) 5 unchanged
tolerated
No law provides for cannabis clubs: there is no licence or legal category for them. An estimated 40 to 50 clubs and associations nevertheless operate, mostly in Bogotá, Cali and Medellín; there is no official count. They lean on rights that belong to individuals — the 20 g personal dose, home growing of up to 20 plants and court rulings on stockpiling and non-profit shared use — which lawyers point out do not extend to organisations. Some grow and share among members for a monthly or yearly fee, others are private rooms where each member brings their own dose; selling cannabis is the line clubs may not cross, and the penal code's cultivation and supply offences remain a risk. No closures or prosecutions of clubs were known by mid-2026, and police in Cali were reported to send park smokers to a club. A national federation of clubs, grouping 35 ventures in early 2026, is preparing a regulation proposal for the health ministry.
planteo, elpais
Magazín Konopí — Konopné kluby v Česku (19 December 2022) Expats.cz — Cannabis cafés and self-growing: Czechia presents draft of new marijuana law (11 January 2024) Advokátní deník — Drogové trestné činy ve světle aktuální novely trestního zákoníku (4 April 2026) 5 unchanged
none
The law has no place for cannabis clubs. An impact study for a regulated market in 2022 proposed licensed non-profit associations of up to 50 members growing five plants each, and the government draft of January 2024 still included clubs, but the reform in force since 2026 covers only personal growing and possession. Growing together and dividing the harvest is not treated as each person's own growing: a legal commentary on the new code classes it as joint commission of the supply offence, and growing for someone else as an attempt at it. An earlier attempt by the former drug coordinator to open a medically oriented club in Brno failed for lack of a permit from the medicines agency. The sources read report no clubs operating openly.
mk22, ex24, advd, expats
Deutscher Hanfverband — Der erste Zwischenbericht zur Evaluation des CanG: Entwicklung des Cannabismarktes (EKOCAN, October 2025) Hanf Journal — 455 Cannabis-Clubs genehmigt: Deutschlands Anbauvereinigungen wachsen trotz Bürokratie (13 August 2026) 6 unchanged
regulated
Non-commercial cultivation associations have been legal since 1 July 2024, each needing a permit from its state. By August 2026 a nationwide count found 455 approved out of 896 applications, with North Rhine-Westphalia and Lower Saxony ahead and Bavaria far behind. Clubs complain of building rules, security requirements and paperwork that differ from state to state. Even at full production they would cover only about 3.5 to 4.3% of estimated demand.
hj26
The government's evaluation put adult demand at about 743 tonnes a year. In 2024 the cultivation associations supplied less than 0.1% of it and medical cannabis from pharmacies roughly 12 to 14%; home growing could not be measured. The evaluators concluded that illegal production was probably still the main origin of the cannabis consumed, reaching users mostly through friends and acquaintances, with street dealing playing a minor part, and that the illegal share appeared to have shrunk in the first year.
dhv25
Law 04-18 of December 2004 prohibits possession, with prison and a fine for personal use; courts can order detoxification treatment in place of the sentence for users who comply. Sources disagree onSince the exactMay 2023 amendment the range for buying, using or holding for personal use is two months to two years and a fine of 20,000 to 50,000 dinars.
tsa23, wiki, trip
CultivationGrowing cannabis knowingly carries life imprisonment under the 2023 amendment; when it is for personal use, two to five years and a serious crimefine of 200,000 to 500,000 dinars.
tsa23, wiki
TSA — Trafic de drogue et d'êtres humains : l'Algérie durcit sa législation (21 May 2023) 4 unchanged
grey
unclear
illegal
banned
The 2015 Penal Code punishes growing a controlled plant with three to five years in prison (Article 387), or with six to twelve months or a fine when it is for personal use (Article 396). Which plants are controlled is left to regulations that were not read. The code does not mention seeds.
pc2015
Transform Drug Policy Foundation — Cannabis social clubs in Spain: legalisation without commercialisation (16 November 2018) Business of Cannabis — Is cannabis legal in Spain? Clubs, courts and a market in limbo (2 October 2026) ICEERS — Cannabis clubs in Barcelona: a world benchmark at risk of extinction (12 July 2024) 5 unchanged
tolerated
Spain has the largest network of cannabis social clubs in Europe, built on court doctrine and not on any law: private use and growing for one's own use are not crimes, and 'shared consumption' among a closed group of adults has long been tolerated. The first club was founded in 2001. Estimates run from about 400 clubs in 2018 to a peak of 800 to 1,000 in the late 2010s, most of them in Catalonia and the Basque Country. A club registers as a non-profit association, admits adults on the word of an existing member, grows for its expected membership and hands out about 3 g a day for use on the premises, charging fees to cover costs. Clubs follow their own codes or those of regional federations. The Supreme Court convicted the organisers of three clubs in 2015, finding that what they did was not genuine private consumption, and police raids and investigations continue. Some Barcelona clubs grew to thousands of members by admitting tourists, and since 2024 the city has fined clubs and ordered some to close. In July 2026 the government approved a draft tobacco law that would remove the exemption for private smokers' clubs on which the clubs also rely.
tdpf18, boc26, ice24
The Local France — Leading French cannabis activist handed jail term (19 April 2013) Vice France — Des nouvelles de la ramasse (24 June 2013), on the Tours court's dissolution of the federation OFDT / MILDECA — Estimation de l'évolution des marchés des drogues illicites en France, communiqué (8 December 2025, PDF) 6 unchanged
All cannabis sold in France is illegal, and it is the largest drug market by volume. A study for the national drug observatory OFDT estimated that 397 tonnes were consumed in mainland France in 2023, worth about 2.7 billion euros. The volume has been almost flat since 2017 (up 2.3%), in line with use. In value, cocaine overtook cannabis for the first time, at 3.1 billion euros; together the two make up nearly 90% of a drug market estimated at 6.8 billion euros.
ofdt25
underground
Clubs have no legal footing and the one organised attempt was shut down by the courts. In March 2013 a federation of Cannabis Social Clubs registered itself with the prefecture in Tours as an act of open defiance, claiming about 150 clubs growing for their members' own use; only six declared themselves. Its spokesman was given a six- month suspended sentence and a 2,000 euro fine in April 2013 after 126 plants were found at his home, and in June 2013 the Tours court dissolved the federation as unlawful and contrary to good morals and barred its members from meeting. Other branches faced the same dissolution proceedings. No source read describes clubs operating openly since.
tl13, vice13
grey
unclear
illegal
banned
Article 208 of the Penal Code, as amended in 1993, punishes growing, holding, selling or buying plants classed as narcotics without authorisation with two to ten years in prison and a fine of 250,000 to 50 million francs. It does not mention seeds. The copy read predates the 2019 Penal Code.
penal
Volteface — "We have cannabis clubs in the UK?" (23 June 2016), republished by the Transnational Institute Wikipedia — Teesside Cannabis Club 5 unchanged
tolerated
No law provides for clubs and cannabis remains a class B drug, but clubs have operated openly since about 2011. By 2016 there were more than 80 across the country, most with 100 to 150 members, each registered as a non-profit and loosely overseen by a national committee, UK Cannabis Social Clubs. They are mainly places to meet and campaign rather than growing collectives. The best known, Teesside Cannabis Club in Stockton-on-Tees, has had its own premises since 2017 and about 850 members in 2024; it sells no cannabis, members bring their own, and it has worked with the support of local police, whose commissioner in Durham had decided not to pursue users and small growers. That tolerance is local police discretion, not national policy.
volteface16, wikitcc
Law of Georgia on Narcotic Drugs, Psychotropic Substances and Precursors and Narcological Assistance, English consolidated text (Legislative Herald of Georgia) 6 unchanged
grey
open
illegal
banned
The narcotics law defines cannabis as the flowering and fruiting tops of the hemp plant, leaving out seeds and leaves that come without tops. It defines sowing as putting a seed into soil or planting a seedling, and sowing, growing and cultivating plants that contain controlled substances is among the acts the law forbids. The text read is an English consolidated version on the official gazette site, before the new law passed in 2025.
nlaw
Graphic Online — GH¢50 million worth of cannabis destroyed (9 February 2023) 4 unchanged
All cannabis on sale is illegal. The Narcotics Control Commission burned about 50 tonnes of seized drugs, mostly cannabis, in February 2023, valued at 50 million cedis; it had been intercepted between 2021 and 2023, mainly in the Volta and Eastern regions, and the commission called it one of its biggest hauls in recent years. It warned drivers and vehicle owners against being used as couriers.
gr23
Nouveau Code pénal de la République de Guinée, février 2016 (Police and Human Rights Resources copy) 2 unchanged
grey
unclear
illegal
banned
The 2016 Penal Code punishes growing drug plants unlawfully. Under Article 832, growing a small amount for one's own use carries one to three years in prison and a fine of three times the value seized. The drug articles speak of plants and substances and do not mention seeds.
cp16
Transnational Institute — About drug law reform in Honduras 3 unchanged
illegal
banned
illegal
banned
According to the Transnational Institute's summary of the 1989 drug law (Decree 126-89), Article 7 prohibits producing, planting, cultivating and gathering plants or seeds that contain narcotic or controlled ingredients. The statute itself was not read.
tnihn
grey
unclear
illegal
banned
Article 10 of the 2001 drug law bans growing the cannabis plant, apart from medical or scientific permits under Article 43, and obliges landholders to destroy such plantations. The law mentions seeds only for the opium poppy.
loi2001
grey
unclear
illegal
banned
The 2020 Penal Code punishes the illicit production of narcotics with ten to twenty years (Article 206) and does not name cultivation or seeds separately. Whether growing a plant counts as production under the code was not confirmed.
pc2020
الجريدة — زراعة النباتات «المخدرة» بترخيص من «الصحة» 6 unchanged
licensed
regulated
licensed
regulated
Under the 2025 drug law, prohibited plants may be grown only by authorised government bodies, universities and licensed research centres. A Health Ministry decision limits this to medical and scientific research: the licence names the type and quantity of seed, a licensed pharmacist must answer for the seeds and plants, and leftover seed is destroyed before a committee when the licence ends. The law's own wording on seeds was not read.
kwt, jar
The 1998 narcotics law lists the cannabis plant as any plant of the genus containing THC, … ing and growing hemp counts as cultivating a narcotic plant and is a controlled activity. Since 2025 licensed companies may grow listed hemp varieties with up to 0.3% THC, and only from original or elite registered seed; individuals may not.
tn25, l279
Tengrinews — Требования к выращиванию конопли утвердили в Казахстане: чего делать нельзя (30 September 2025) 4 unchanged
illegal
banned
illegal
banned
Article 30 of Law 7 of 1990 bans growing, importing, owning, holding, buying or selling the listed plants at any stage of growth, as well as their seeds, except for medical or scientific purposes under licence. Only cannabis seeds roasted so they cannot germinate, and stalk fibre, are exempt. Growing or holding the plants or their seeds for personal use is punished with prison and a fine of 1,000 to 5,000 dinars under Article 37.
law7
Cultivation isUnlawful growing carries five to ten years in prison and a crimefine under Article 2 of Law 890 of 1970.
l890, wiki
Loi n° 890 du 1er juillet 1970 sur les stupéfiants, consolidated (Legimonaco) 3 unchanged
grey
unclear
illegal
banned
Article 2 of Law 890 of 1970 punishes unlawfully growing, holding, buying or selling narcotics with five to ten years in prison. The law does not mention seeds.
l890
Loi n° 93-37 du 20 juillet 1993 relative à la répression de la production, du trafic et de l'usage illicite des stupéfiants et substances psychotropes (UNODC SHERLOC copy) 3 unchanged
grey
unclear
illegal
banned
Law 93-37 punishes growing a high-risk drug with 15 to 30 years in prison, and with death for a repeat offence (Article 3). Which plants count is set by ministerial order, which was not read. The law speaks of substances and plants and does not mention seeds.
l9337
ARUC — Associations holding an operating permit The Malta Independent — Parliament unanimously passes new cannabis legislation amendments (12 May 2025) 5 unchanged
regulated
Non-profit Cannabis Harm Reduction Associations are the only legal source besides home growing. They are licensed by the cannabis authority ARUC, and the first began handing out cannabis in January 2024; in October 2026 the authority listed 24 with an operating permit, spread over towns on Malta and one site on Gozo. An association may have at most 500 members, who must be residents with a Maltese identity or residence card and may belong to only one; tourists cannot join. Members may receive up to 7 g a day and 50 g a month. Nothing may be consumed on the premises, advertising is banned, and since 2025 the premises must stand 250 metres from schools, sports facilities and youth centres.
chras, aruc, cap628, mi25
Without paperwork, the only protection is the long-standing rule that up to 5 g is not pro … ws possession of up to 28 g. Without a permit, anything over 5 g can still be prosecuted. The Supreme Court has twice protected individuals held with more, in May 2022 and February 2026, telling judges to weigh whether the cannabis was for personal use, but in 2026 it fell one vote short of removing the limit for everyone.
cms, tms, zeta22, imp26
No legal sales at all. Congress has never passed the regulatory law the Court called for, so there are no licensed dispensaries. Unlicensed outlets work around this: members-only shops that hand over cannabis as a gift with a paid membership or item, private clubs that ask for a court injunction or a prescription, and unlicensed shops operate at their own riskselling to tourists in resort towns. Police raids are reported to be rare but the model depends on local tolerance.
cms, giq, yuc
Low-THC and CBD products are sold; a handful of medical products are registered. No legal THC retail products. The health regulator has contested the CBD trade: in July 2023 it seized 1,811 products from a national shop chain and said such sales fell outside health rules.
cms, zeta23
After the Supreme Court's 2021 ruling an adult can apply to the health regulator COFEPRIS … ermit does not allow importing, selling or distributing, so there is no legal seed trade. The health regulator still treats cannabis seeds as narcotics under the health law. In practice, street stands on some major avenues sell plants and seeds openly.
tele, zeta23, yuc
Milenio — Clubes de marihuana en CdMx: del toque en Reforma al lujo en Polanco (30 March 2025) El CEO — Los clubes cannábicos, negocios que buscan florecer en México (26 January 2019) Yucatán Magazine — Cannabis in Mexico faces a cultural turning point (22 August 2025) Zeta Tijuana — SCJN declara inconstitucional penalizar posesión de más de 5 gramos de cannabis (May 2022) Zeta Tijuana — Cofepris clausura Paradise, empresa de cannabis ligada a Vicente Fox (24 July 2023) El Imparcial — Suprema Corte falló en contra de cambiar el límite de 5 gramos por un solo voto (February 2026) 5 unchanged
tolerated
No law provides for cannabis clubs, and Congress has never passed the regulation the Supreme Court called for, so clubs work in a grey zone across the country. They lean on the constitutional freedom of association and on members' personal permits or court injunctions, which one drug-policy researcher calls a semi-legal backing that does not cover any sale. The first, Club Xochipilli, opened in Mexico City in December 2015 after the court's first ruling. Because clubs are private, barely advertise and are unregistered, nobody can count them, but specialists describe a boom, with chains now present in Mexico City, the State of Mexico, Baja California and Quintana Roo, and clubs in Guadalajara. Typically a member registers with official ID and pays a daily, monthly or yearly fee; cannabis is handed over as a gift or against a 'donation' converted into points, and some clubs ask for an injunction or a prescription. Authorities mostly tolerate them, which researchers call selective inaction; clubs are short-lived, supply often comes from the illegal market, and raids, though reported as rare, still happen.
mil25, ceo19, yuc
News Agency of Nigeria — NGO demands urgent action on drug cultivation, use (15 April 2025) 4 unchanged
The whole market is illegal and largely home-grown. A 2019 UN survey documented 8,900 hectares of cannabis across six southern states; cultivation is concentrated in Ekiti, Ondo, Ogun, Oyo and Osun in the South-West and Delta and Edo in the South-South, while use is rising among young people in northern states such as Kano, Kaduna and Borno. The drug agency NDLEA destroyed 847 hectares of plantations in 2013 and more than 4,500 in 2014, and has destroyed many farms since 2021, yet growing continues. In 2023 the agency set up an alternative development unit to move cannabis farmers to food crops and livestock.
nan25
Omroep Brabant — Weer minder wietkwekerijen ontdekt (8 May 2025), reporting the National Police's 2024 figures Expertisecentrum Openbare Orde & Veiligheid (University of Groningen) — Cannabis Clubs (19 May 2016) CNNBS.nl — Cannabis Social Clubs in Nederland: de stand van zaken (4 October 2016) CNNBS.nl — Cannabisclubs Utrecht verboden (16 May 2014) Rechtbank Overijssel — Geen straf voor verkoop illegale wietolie (18 November 2021) 4 unchanged
underground
The toleration policy covers coffeeshops only; growing remains illegal, so a members' growing club has no footing. A handful were founded from 2014. The prosecution service told Amsterdam's Tree of Life in 2015 that its growing and sharing among members was punishable, though it chose not to prosecute actively. Utrecht backed two clubs and asked for an Opium Act exemption, which the health ministry refused in 2014 because the clubs served no scientific or medical purpose; by 2016 nothing had been planted there. The largest, the Frisian foundation Suver Nuver, posted home-made cannabis oil to thousands of members; in November 2021 the Overijssel court found it and its director guilty under the Opium Act and the Medicines Act but imposed no penalty, saying the choice was for lawmakers. No later national picture of clubs was found.
coov, cnn16, cnn14, rb21
Coffeeshop sales are tolerated but, outside the supply experiment, the cannabis they sell is grown illegally. Police dismantled 895 growing sites in 2024, the first year below a thousand in a long time; in 2017 it was more than 4,600. The National Police put the fall down mainly to lack of capacity and lower priority rather than less growing, and note that drug networks increasingly bring plants in from abroad, for instance from Spain.
ob25
National Drugs Control Act 2008, Supplement to the Sierra Leone Gazette, 7 August 2008 (Sierra Leone Web, PDF) 3 unchanged
illegal
banned
illegal
banned
The National Drugs Control Act 2008 defines cannabis as any part of the cannabis plant, including the seeds and leaves, so seeds are a prohibited drug like the rest of the plant. Its definition of cultivating covers planting, sowing, scattering seed, growing and tending.
ndca
Code des drogues, loi n° 18/97 adoptée le 11 novembre 1997 (UNODC SHERLOC copy) 2 unchanged
grey
unclear
illegal
banned
Article 8 of the 1997 Drug Code bans growing the cannabis plant anywhere in the country, and landholders must destroy such plantations or report them. The articles read do not mention seeds. The copy read stops at Article 108 and predates the 2007 amendments.
cdd97
Cultivation is a crimeArticle 2 of the 1998 Narcotics Act forbids growing Cannabis sativa.
wvm, wiki
Wet verdovende middelen, S.B. 1998 no. 14, as amended by S.B. 2002 no. 68 (De Nationale Assemblée, PDF) 2 unchanged
grey
unclear
illegal
banned
Article 2 of the 1998 Narcotics Act bans growing Cannabis sativa. Its schedule defines hemp as any part of the plant of the genus Cannabis from which the resin has not been extracted, without the express exception for seeds found in the Dutch law it resembles. The text read is the version as amended in 2002.
wvm
قانون المخدرات السوري رقم 2 لعام 1993 — text as reproduced by Lawyer Egypt 3 unchanged
illegal
banned
illegal
banned
Article 30 of Law 2 of 1993 bans growing, importing, owning, holding or buying the listed plants at any stage of growth, as well as their seeds; plant parts named in a separate schedule are exempt. Only state institutions and recognised research centres can be licensed to grow them or import seeds. Article 39 sets the death penalty for unlicensed growing. The text read is a copy on a law firm's site.
l2
The Drug Control and Enforcement Act defines cannabis as any part of the plant "excluding … defined to include planting and scattering the seed, and growing the plant is an offence. A bill presented to Parliament on 1 September 2026 would make holding or distributing seeds of prohibited plants an offence whatever their intended use; it had not been reported as passed.
dcea, dn26
States issue commercial licences, some uncapped (Oklahoma, Oregon), many tightly capped and very expensive. On 2228 April 2026 thea DEA rule moved FDA-approved and state-licensed medical cannabis to federal Schedule III. Adult-use … g all cannabis to Schedule III ran from 29 June to 15 July 2026 and a decision is pending.
Secretaría Nacional de Drogas / IRCCA — Regulación del mercado de cannabis en Uruguay: segunda evaluación de la ley 19.172 (April 2026, PDF) IRCCA — Resumen de datos del cannabis de uso adulto (al 31/12/2025) Infobae — Marihuana legal en Uruguay: clubes canábicos venden más que las farmacias y presionan para habilitar el acceso a turistas (24 March 2026) 4 unchanged
regulated
Membership clubs are one of three legal routes, alongside pharmacies and home growing, and a resident may register for only one. A club is a non-profit association of at most 45 members growing up to 99 flowering plants. At the end of 2025 there were 557 clubs, in every department. By March 2026 some 575 clubs had about 20,200 members; with under a quarter of registered users they say they supply more than half of the legal cannabis, about 4.75 tonnes against 4.25 tonnes sold by pharmacies. The clubs are pressing to be allowed to serve tourists.
ircca25, inf26
Eleven years after legalisation, the government's second evaluation of the law found that 46% of current users were registered for one of the legal routes. Classic trafficking of pressed cannabis, the main source in 2014, supplied only 6.7% of users in 2024. What remains is mostly a grey market of about 30%: cannabis from unregistered home growers and informal swaps, which the report calls distinct from traditional trafficking and less criminal, and one of the main unsolved problems. Pharmacies had 74,583 registered buyers at the end of 2024, but only 20 to 40% of them bought in any month, and the report says supply and distribution problems keep the legal market at about half its potential.
snd26
grey
unclear
illegal
banned
The Supreme Court's guidance on drug cases says the local narcotics law bans cultivating hemp for illegal use, and that sowing means sowing seed or planting seedlings without permission on any land. The offence is complete at the moment of sowing, whether or not anything comes up. The guidance does not treat holding seeds as an offence in itself.
sc
SD Law — Cannabis grow clubs declared illegal (14 September 2022) 5 unchanged
underground
After the 2018 ruling allowed private growing, many 'grow clubs' appeared: members supply seed, the club grows the plants and hands back the harvest, charging for the gardening and not for the cannabis. Police raided one, The Haze Club, in October 2020. In 2022 the Cape Town High Court refused to declare the model lawful, holding that the right to grow for private use cannot be outsourced and that an unregulated club could in practice legalise dealing. Until the law provides for them, clubs risk prosecution under the Drug Trafficking Act.
sdl22
docs/DATA-MAINTENANCE.md +41 @@ -26,5 +26,6 @@ Flower prices are a fourth, independent data set. They provide market context bu the freedom score. -Seed and clone law is an optional, unscored section of the profile (see "Seeds and clones"). +Seed and clone law is an optional, unscored section of the profile (see "Seeds and clones"), +as are cannabis clubs and the black market (see "Cannabis clubs and the black market"). ## Where things are @@ -138,4 +139,41 @@ by the ECB, cite the alternative exchange-rate source in the observation note. map modes. +## Cannabis clubs and the black market + +Two more optional, unscored sections under `context`. Each is a plain cited text, like +`production` or `medical`; leave a section out until something reliable is found. + +```yaml +context: + clubs: + status: tolerated # regulated | tolerated | underground | none + text: >- + Around 1,000 to 1,500 private associations grow for their members in a legal grey area … + sources: + - key + black_market: + text: >- + Most cannabis is still bought illegally; a gram costs about … + sources: + - key +``` + +- **`clubs`**: do cannabis clubs (social clubs, associations, members-only lounges) exist, and on + what footing? The interest is in unregulated or lightly regulated ones: how they work around the + law, how many there are, how membership works, how often they are raided. Licensed shops and + pharmacies belong in the access segment, not here. Where none are known, say so only if a source + says so. +- **`clubs.status`** is required with the section and colours the "Cannabis clubs" map option: + `regulated` (a law provides for clubs: licensed or registered, within set limits), `tolerated` + (no law, but they operate fairly openly and are mostly left alone), `underground` (they exist + but are treated as illegal: raided, organisers prosecuted), `none` (none known, or the law + leaves no room; use only when a source supports it). Judge the country as a whole, not one city. +- **`black_market`**: the state of the illegal market: how large it is next to any legal supply, + where the cannabis comes from, typical prices if sourced, how openly it is sold, and how + violent or organised it is. Prefer surveys, police and UN figures over anecdotes. +- Neither section moves a score. If research for them shows the law or practice differs from a + scored segment, fix the segment and add a history entry as usual. +- `node scripts/data-status.cjs clubs` and `… black_market` list what is done and what is left. + ## Seeds and clones @@ -229,4 +267,6 @@ all read from there, together with `PLANTING_TRADE`. - **`estimate: true`** (with an optional `window: [from, to]`) marks a first restriction whose date is unknown. Replace these with a dated law whenever one is found. +- **`country`** may also be a disputed territory (`xa`, `xn`, `xs`, `xt`). Its entries show on its + page and in the history list, but its scored changes never move the world average. - **`checked: true`** means: the date and the event were compared with a source that was read. A search-result summary is not a read source (see "What went wrong").
scripts/data-status.cjs +9 @@ -1,3 +1,3 @@ -// Where the data stands: node scripts/data-status.cjs [unchecked|estimates|thin|weak|planting|chain [id]] +// Where the data stands: node scripts/data-status.cjs [unchecked|estimates|thin|weak|planting|clubs|black_market|chain [id]] // Run from the project root. Needs js-yaml (already in node_modules). const fs = require("fs"), y = require("js-yaml"); @@ -14,4 +14,12 @@ console.log(`history: ${changes.length} entries | checked ${changes.filter((e) = if (what === "unchecked") for (const e of unchecked) console.log([e.country || "--", e.date, e.id, e.sources[0].url].join(" | ")); if (what === "estimates") console.log(estimates.map((e) => `${e.country}${e.window ? `[${e.window}]` : ""}`).join(" ")); +if (what === "clubs" || what === "black_market") { + // An optional context section: which profiles have it, and which are still to do, most people first. + const pop = (id) => { const p = country[id].population; return p[Object.keys(p).sort().at(-1)]; }; + const has = ids.filter((id) => country[id].context[what]); + console.log(`${what}: ${has.length} of ${ids.length} profiles`); + console.log("done:", has.map((id) => (what === "clubs" ? `${id}:${country[id].context.clubs.status}` : id)).join(" ")); + console.log("to do:", ids.filter((id) => !country[id].context[what]).sort((a, b) => pop(b) - pop(a)).map((id) => `${id}:${country[id].name}`).join(", ")); +} if (what === "planting") { // Seeds and clones: which profiles have the section, and which are still to do, best score first.
src/components/CountryArticle.astro +9 @@ -8,5 +8,5 @@ import CountryChart from "./CountryChart.astro"; import PricePanel from "./PricePanel.astro"; import { SHOW_PRICES } from "../lib/config.js"; -import { PLANTING_STATUS, PLANTING_TRADE } from "../lib/segment-ids.js"; +import { PLANTING_STATUS, CLUBS_STATUS, PLANTING_TRADE } from "../lib/segment-ids.js"; import { answersFor } from "../lib/answers.js"; @@ -122,5 +122,6 @@ const circ = 2 * Math.PI * r; )} {SHOW_PRICES && !sub && <PricePanel id={c.id} />} - {scored.length > 0 && <CountryChart history={scored} />} + {/* in the drawer the chart stays in this column; on the full page it moves under the breakdown */} + {scored.length > 0 && <div class="chart-in-side"><CountryChart history={scored} /></div>} </div> @@ -182,4 +183,5 @@ const circ = 2 * Math.PI * r; ); })} + {scored.length > 0 && <div class="chart-in-main"><CountryChart history={scored} /></div>} {group && ( @@ -213,4 +215,9 @@ const circ = 2 * Math.PI * r; <div class="segrow"> <div class="segrow-top"><span>{x.name === "Death penalty" ? "☠️ " : ""}{x.name}</span></div> + {x.clubs && ( + <div class="d-chips"> + <span class={`chip s-${CLUBS_STATUS[x.clubs].tone}`} title={CLUBS_STATUS[x.clubs].help}>Clubs: {CLUBS_STATUS[x.clubs].label}</span> + </div> + )} {x.seeds && ( <div class="d-chips">
src/components/HistoryList.astro +53 @@ -4,5 +4,5 @@ // and the kind filters above the list lead to the other pages. import ScoreChange from "./ScoreChange.astro"; -import { changes, historyPeriods, formatDate, CHANGE_CATEGORIES } from "../lib/countries.js"; +import { changes, places, historyPeriods, formatDate, CHANGE_CATEGORIES } from "../lib/countries.js"; import { tierColor } from "../lib/tiers.js"; @@ -20,4 +20,22 @@ const kindCount = (key) => dated.filter((ch) => ch.category === key).length; const years = [...new Set(items.map((ch) => ch.date.slice(0, 4)))] .map((year) => ({ year, items: items.filter((ch) => ch.date.startsWith(year)) })); +// The whole history in numbers, for the side panel. The same on every page of the list. +const num = (n) => n.toLocaleString("en-GB"); +const perPlace = new Map(); +for (const ch of changes) if (ch.country) perPlace.set(ch.country, (perPlace.get(ch.country) ?? 0) + 1); +const stats = { + total: changes.length, + scored: dated.filter((ch) => ch.score).length, + context: dated.filter((ch) => !ch.score).length, + estimated: changes.length - dated.length, + checked: dated.filter((ch) => ch.checked).length, + international: dated.filter((ch) => !ch.country).length, + places: perPlace.size, + oldest: dated.at(-1), + newest: dated[0], + top: [...perPlace].sort((x, y) => y[1] - x[1] || x[0].name.localeCompare(y[0].name)).slice(0, 8), + // every place, also those with no entry at all + fewest: places.map((c) => [c, perPlace.get(c) ?? 0]).sort((x, y) => x[1] - y[1] || x[0].name.localeCompare(y[0].name)).slice(0, 8), +}; --- <nav class="periods" aria-label="Periods"> @@ -36,4 +54,5 @@ const years = [...new Set(items.map((ch) => ch.date.slice(0, 4)))] </nav> +<div class="change-layout hist-layout"> <div class="chlog"> {years.map(({ year, items }) => ( @@ -59,4 +78,37 @@ const years = [...new Set(items.map((ch) => ch.date.slice(0, 4)))] </div> +<aside class="change-aside hist-aside" aria-label="History in numbers"> + <div class="card ca-card"> + <h2 class="hs-title">History in numbers</h2> + <dl class="hs-grid"> + <div><dt>Entries in total</dt><dd>{num(stats.total)}</dd></div> + <div><dt>{kind ? "Of this kind" : "In this period"}</dt><dd>{num(items.length)}</dd></div> + <div><dt>Moved a score</dt><dd>{num(stats.scored)}</dd></div> + <div><dt>Context only</dt><dd>{num(stats.context)}</dd></div> + <div><dt>Countries and territories</dt><dd>{num(stats.places)}</dd></div> + <div><dt>Tied to no country</dt><dd>{num(stats.international)}</dd></div> + </dl> + <p class="ca-note">{num(stats.checked)} of the {num(dated.length)} dated entries ({Math.round((stats.checked / dated.length) * 100)}%) were checked against a source that was read. Another {num(stats.estimated)} are estimated first bans with no known date; they are not listed here.</p> + <p class="ca-note">The oldest entry is from <a href={`${base}history/${stats.oldest.id}/`}>{stats.oldest.date.slice(0, 4)}</a>, the newest from <a href={`${base}history/${stats.newest.id}/`}>{formatDate(stats.newest.date)}</a>.</p> + </div> + <div class="ca-more hs-top"> + <h2>Most entries</h2> + <ol> + {stats.top.map(([c, n]) => ( + <li><a href={`${base}country/${c.slug}/`}><b>{c.flag} {c.name}</b><time>{n} entries</time></a></li> + ))} + </ol> + </div> + <div class="ca-more hs-top"> + <h2>Fewest entries</h2> + <ol> + {stats.fewest.map(([c, n]) => ( + <li><a href={`${base}country/${c.slug}/`}><b>{c.flag} {c.name}</b><time>{n === 1 ? "1 entry" : `${n} entries`}</time></a></li> + ))} + </ol> + </div> +</aside> +</div> + {period && <nav class="pager chl-pager" aria-label="Other periods"> {newer
src/content.config.ts +7 @@ -4,5 +4,5 @@ import { defineCollection } from "astro:content"; import { glob, file } from "astro/loaders"; import { z } from "astro/zod"; -import { SEGMENT_IDS, CONTEXT_IDS, CHANGE_CATEGORIES, PLANTING_STATUS, PLANTING_TRADE } from "./lib/segment-ids.js"; +import { SEGMENT_IDS, CONTEXT_IDS, CHANGE_CATEGORIES, PLANTING_STATUS, PLANTING_TRADE, CLUBS_STATUS } from "./lib/segment-ids.js"; const score = z.number().int().min(0).max(10); @@ -16,4 +16,6 @@ const plantingStatus = z.enum(Object.keys(PLANTING_STATUS) as [string, ...string const plantingTrade = z.enum(Object.keys(PLANTING_TRADE) as [string, ...string[]]); const planting = z.object({ seeds: plantingStatus, seeds_trade: plantingTrade, clones: plantingStatus, clones_trade: plantingTrade, text: z.string().min(1), sources: z.array(z.string()) }).strict(); +// cannabis clubs: the cited text plus the footing they stand on (CLUBS_STATUS) +const clubs = z.object({ status: z.enum(Object.keys(CLUBS_STATUS) as [string, ...string[]]), text: z.string().min(1), sources: z.array(z.string()) }).strict(); const status = { // "death": illegal, and the law allows the death penalty for cannabis offences, in practice trafficking. @@ -57,7 +59,8 @@ const countries = defineCollection({ scores: perSegment(score), segments: perSegment(cited), - // the four standard sections, plus `death_penalty` where the law provides for one and - // `planting` (seeds and clones) where it has been researched - context: z.object({ death_penalty: cited.optional(), planting: planting.optional(), ...Object.fromEntries(CONTEXT_IDS.map((id) => [id, cited])) }).strict(), + // the four standard sections, plus `death_penalty` where the law provides for one and, + // where they have been researched, `planting` (seeds and clones), `clubs` (cannabis clubs, + // above all unregulated or lightly regulated ones) and `black_market` (the illegal market) + context: z.object({ death_penalty: cited.optional(), planting: planting.optional(), clubs: clubs.optional(), black_market: cited.optional(), ...Object.fromEntries(CONTEXT_IDS.map((id) => [id, cited])) }).strict(), sources: z.record(z.string(), source), subunits: z
src/lib/countries.js +5 @@ -20,5 +20,5 @@ export const MED_LABEL = { yes: "Legal", limited: "Limited", no: "None" }; // "Death penalty" comes first and exists only for countries whose law provides for it; // "Seeds and clones" exists only where it has been researched and carries a status for each. -const CONTEXT = [["death_penalty", "Death penalty"], ["production", "Commercial production"], ["products", "Products"], ["planting", "Seeds and clones"], ["medical", "Medical use"], ["consequences", "Other consequences"]]; +const CONTEXT = [["death_penalty", "Death penalty"], ["production", "Commercial production"], ["products", "Products"], ["planting", "Seeds and clones"], ["medical", "Medical use"], ["clubs", "Cannabis clubs"], ["black_market", "Black market"], ["consequences", "Other consequences"]]; export const CONTEXT_NAMES = CONTEXT.map(([, name]) => name); @@ -71,8 +71,9 @@ const everyPlace = (await getCollection("countries")) reviewed: isoDate(data.updated), segments: SEGMENTS.map((seg) => cited(data.segments[seg.id])), // in SEGMENTS order - context: CONTEXT.map(([key, name]) => (data.context[key] ? { name, ...cited(data.context[key]), ...(key === "planting" && plantingOf(data.context[key])) } : null)), // null: no such section + context: CONTEXT.map(([key, name]) => (data.context[key] ? { name, ...cited(data.context[key]), ...(key === "planting" && plantingOf(data.context[key])), ...(key === "clubs" && { clubs: data.context[key].status }) } : null)), // null: no such section sources: Object.values(data.sources).map(pair), }, planting: data.context.planting ? plantingOf(data.context.planting) : null, + clubs: data.context.clubs?.status ?? null, // a key of CLUBS_STATUS, or null until researched subunitMeta: data.subunits ?? null, }; @@ -154,5 +155,5 @@ const changeEntries = (await getCollection("changes")).map(({ data }) => data).s export const changes = changeEntries .map((data, i) => { - const country = data.country ? countries.find((c) => c.slug === data.country) : null; + const country = data.country ? places.find((c) => c.slug === data.country) : null; // a country or a disputed territory if (data.country && !country) throw new Error(`changes.yaml: "${data.id}" refers to unknown country "${data.country}"`); // context only (a treaty, report or ruling): part of the history, but it moves no score @@ -226,5 +227,5 @@ function averageOverTime(weight) { return sum / total; }; - const events = changes.filter((ch) => ch.score && !ch.estimate).map((ch) => ({ change: ch, date: ch.date, t: dateTime(ch.date) })); + const events = changes.filter((ch) => ch.score && !ch.estimate && !ch.country.disputed).map((ch) => ({ change: ch, date: ch.date, t: dateTime(ch.date) })); for (let year = CHART_FROM; year <= +DATA_SNAPSHOT; year++) events.push({ change: null, date: `${year}-01-01`, t: Date.UTC(year, 0, 1) }); events.sort((a, b) => a.t - b.t);
src/lib/exports.js +1 @@ -23,4 +23,5 @@ export const scoreRows = places.map((c) => ({ clones: c.planting?.clones ?? "", clones_trade: c.planting?.clones_trade ?? "", + clubs: c.clubs ?? "", // empty until researched; see CLUBS_STATUS population: c.population, users: c.use.users,
src/lib/segment-ids.js +8 @@ -19,4 +19,12 @@ export const PLANTING_TRADE = { banned: { label: "Banned", tone: "illegal", help: ["Selling or handing them on is an offence", "Selling or handing them on is an offence"] }, }; +// Cannabis clubs: an optional context section with one status, the footing clubs stand on. +// Not part of the score. Licensed shops and pharmacies are not clubs. +export const CLUBS_STATUS = { + regulated: { label: "Regulated by law", tone: "legal", help: "A law provides for clubs: they are licensed or registered and work within set limits" }, + tolerated: { label: "Tolerated", tone: "decrim", help: "No law provides for clubs, but they operate fairly openly and are mostly left alone" }, + underground: { label: "Underground", tone: "partial", help: "Clubs exist but are treated as illegal: they are raided or their organisers prosecuted" }, + none: { label: "None known", tone: "illegal", help: "No clubs are known, or the law leaves no room for them" }, +}; // What kind of event a History entry is, in the order the legend lists them. export const CHANGE_CATEGORIES = {
src/pages/api/[id].json.js +1 @@ -11,5 +11,5 @@ export function getStaticPaths() { export function GET({ props }) { const { c } = props; - const entry = ({ t, src, seeds, seeds_trade, clones, clones_trade }) => ({ t, src, ...(seeds && { seeds, seeds_trade, clones, clones_trade }) }); + const entry = ({ t, src, seeds, seeds_trade, clones, clones_trade, clubs }) => ({ t, src, ...(seeds && { seeds, seeds_trade, clones, clones_trade }), ...(clubs && { clubs }) }); const body = { id: c.id,
src/pages/compare.astro +4 @@ -4,5 +4,5 @@ import { SEGMENTS, CONTEXT_NAMES, REC_LABEL, MED_LABEL, countries, places } from import { flatLatestPricesFor } from "../lib/prices.js"; import { SHOW_PRICES } from "../lib/config.js"; -import { PLANTING_STATUS, PLANTING_TRADE } from "../lib/segment-ids.js"; +import { PLANTING_STATUS, CLUBS_STATUS, PLANTING_TRADE } from "../lib/segment-ids.js"; const light = places.map(({ id, slug, name, flag, region, raw, score, rank, s, rec, med }) => @@ -13,4 +13,5 @@ const meta = { planting: PLANTING_STATUS, plantingTrade: PLANTING_TRADE, + clubs: CLUBS_STATUS, rec: REC_LABEL, med: MED_LABEL, @@ -116,5 +117,6 @@ const presets = [ const plantingChip = (what, set, key, i) => `<span class="chip s-${set[key].tone}" title="${esc(set[key].help[i])}">${what}: ${set[key].label}</span>`; const statusChips = (entry) => entry.seeds ? `<div class="d-chips">${plantingChip("Seeds", META.planting, entry.seeds, 0)}${plantingChip("Seed sales", META.plantingTrade, entry.seeds_trade, 0)}${plantingChip("Clones", META.planting, entry.clones, 1)}${plantingChip("Clone sales", META.plantingTrade, entry.clones_trade, 1)}</div>` : ""; - const prose = (entry) => entry ? `${statusChips(entry)}<p class="cmp-text">${esc(entry.t)}</p>${sources(entry)}` : ""; + const clubsChip = (entry) => entry.clubs ? `<div class="d-chips"><span class="chip s-${META.clubs[entry.clubs].tone}" title="${esc(META.clubs[entry.clubs].help)}">Clubs: ${META.clubs[entry.clubs].label}</span></div>` : ""; + const prose = (entry) => entry ? `${statusChips(entry)}${clubsChip(entry)}<p class="cmp-text">${esc(entry.t)}</p>${sources(entry)}` : ""; const priceCell = (p) => p ? `<div class="cmp-val"><b>$${p.usd.toFixed(2)}</b><span>/g</span></div><div class="cmp-sub">${p.price} ${p.currency}/g · ${p.date}</div>`
src/pages/index.astro +150 @@ -5,5 +5,5 @@ import Changes from "../components/Changes.astro"; import { SEGMENTS, DATA_SNAPSHOT, REC_LABEL, REC_ORDER, REC_HELP, MED_LABEL, countries, regions, worldAvg, lastUpdated, formatDate, subunitsOf, places } from "../lib/countries.js"; import { TIERS, tierColor } from "../lib/tiers.js"; -import { PLANTING_STATUS, PLANTING_TRADE } from "../lib/segment-ids.js"; +import { PLANTING_STATUS, PLANTING_TRADE, CLUBS_STATUS } from "../lib/segment-ids.js"; import { PRICE_MARKETS, PRICE_MARKET_LABEL, flatLatestPricesFor, latestPricesFor } from "../lib/prices.js"; import { SHOW_PRICES } from "../lib/config.js"; @@ -30,6 +30,6 @@ const MED_ORDER = ["yes", "limited", "no"]; // `ratio` is the share of people who use cannabis divided by the index score: how far use runs // ahead of freedom. A score below 1 counts as 1, so that a zero does not divide. -const mapData = places.map(({ id, num, name, flag, raw, score, rank, s, use, planting }) => - ({ id, num, name, flag, raw, score, rank, s, planting, prices: SHOW_PRICES ? flatLatestPricesFor(id) : {}, use: { ...use, ratio: +(use.prevalence / Math.max(raw, 1)).toFixed(2) } })); +const mapData = places.map(({ id, num, name, flag, raw, score, rank, s, use, planting, clubs }) => + ({ id, num, name, flag, raw, score, rank, s, planting, clubs, prices: SHOW_PRICES ? flatLatestPricesFor(id) : {}, use: { ...use, ratio: +(use.prevalence / Math.max(raw, 1)).toFixed(2) } })); // States, provinces and territories of the federations, for the map's "show states" switch. // Keyed like the shapes in src/data/subunits.geo.json: "us/ca". @@ -58,5 +58,34 @@ const PLANTING_COLOR = { legal: 5, licensed: 4, grey: 3, illegal: 0, open: 5, re const plantingScale = (statuses) => Object.fromEntries(Object.entries(statuses).map(([k, v]) => [k, { label: v.label, color: `var(--t${PLANTING_COLOR[k]})` }])); const PLANTING_SCALES = { status: plantingScale(PLANTING_STATUS), trade: plantingScale(PLANTING_TRADE) }; +// Cannabis clubs on the map: one option, coloured by the footing clubs stand on. +const CLUBS_COLOR = { regulated: 5, tolerated: 4, underground: 2, none: 0 }; +const CLUBS_SCALE = Object.fromEntries(Object.entries(CLUBS_STATUS).map(([k, v]) => [k, { label: v.label, help: v.help, color: `var(--t${CLUBS_COLOR[k]})` }])); const streetFlower = (c) => latestPricesFor(c.id).street; + +// Rankings columns the reader can switch on and off. `on` marks the ones shown by default. +// Extra columns sort on a number kept on the row: higher means freer (or bigger), -1 means no data. +const order = (keys) => Object.fromEntries(keys.map((k, i) => [k, keys.length - i])); +const STATUS_RANK = { ...order(Object.keys(PLANTING_STATUS)), ...order(["open", "regulated", "gift", "unclear", "banned"]) }; +const CLUBS_RANK = order(Object.keys(CLUBS_STATUS)); +const statusCol = (key, label, vocab, helpIndex) => ({ + key, label, group: "Context", + sort: (c) => STATUS_RANK[c.planting?.[key]] ?? -1, + chip: (c) => c.planting && { ...vocab[c.planting[key]], help: vocab[c.planting[key]].help[helpIndex] }, +}); +const BASE_COLUMNS = [ + { key: "region", label: "Region" }, { key: "users", label: "Users" }, { key: "share", label: "Share of adults" }, + ...(SHOW_PRICES ? [{ key: "price", label: "Price per gram" }] : []), + { key: "rec", label: "Recreational" }, { key: "med", label: "Medical" }, +]; +const EXTRA_COLUMNS = [ + ...SEGMENTS.map((s, i) => ({ key: `s${i}`, label: s.name, group: "Score segments", sort: (c) => c.s[i], score: (c) => c.s[i] })), + { key: "pop", label: "Population", group: "People", sort: (c) => c.population, text: (c) => compact(c.population) }, + statusCol("seeds", "Seeds", PLANTING_STATUS, 0), + statusCol("seeds_trade", "Seed sales", PLANTING_TRADE, 0), + statusCol("clones", "Clones", PLANTING_STATUS, 1), + statusCol("clones_trade", "Clone sales", PLANTING_TRADE, 1), + { key: "clubs", label: "Cannabis clubs", group: "Context", sort: (c) => CLUBS_RANK[c.clubs] ?? -1, chip: (c) => c.clubs && CLUBS_STATUS[c.clubs] }, +]; +const columnGroups = [["Shown by default", BASE_COLUMNS], ...["Score segments", "People", "Context"].map((g) => [g, EXTRA_COLUMNS.filter((x) => x.group === g)])]; --- <Base description={`A score from 0 to 100 for how free cannabis is in every country, with a map, rankings${SHOW_PRICES ? ", retail prices" : ""} and legal history.`}> @@ -76,19 +105,31 @@ const streetFlower = (c) => latestPricesFor(c.id).street; <div> <h2>World map</h2> - <p class="sub" id="mapSub" data-score="Red means prison risk, green means free. Select a country for its full breakdown." data-use="The stronger the blue, the more people use cannabis. Select a country for its full breakdown." data-ratio="The stronger the blue, the further cannabis use runs ahead of the freedom to use it. Select a country for its full breakdown." data-price="Pale yellow means cheaper, violet means more expensive. Countries without a sourced price are hatched." data-planting="Green means legal or sold freely, red means illegal or banned. Hatched countries have not been researched yet. Not part of the score.">Red means prison risk, green means free. Select a country for its full breakdown.</p> </div> - <label class="field"> - <span>Colour by</span> - <select id="metric"> - <option value="index">Overall index</option> - {SEGMENTS.map((s, i) => <option value={i}>{s.name}</option>)} - <option value="users">Cannabis users (number)</option> - <option value="prevalence">Cannabis users (share of adults)</option> - <option value="ratio">Share of users ÷ index</option> - {SHOW_PRICES && PRICE_MARKETS.map((market) => <option value={`price:${market}`}>{PRICE_MARKET_LABEL[market]} price</option>)} - {Object.entries(PLANTING_FIELDS).map(([field, label]) => <option value={`plant:${field}`}>{label}</option>)} - </select> - </label> </div> + <div class="metric-bar" id="metric" role="group" aria-label="Colour the map by"> + <div class="metric-group"> + <span>Freedom</span> + <button type="button" data-metric="index" aria-pressed="true">Overall index</button> + {SEGMENTS.map((s, i) => <button type="button" data-metric={i} aria-pressed="false">{s.name}</button>)} + </div> + <div class="metric-group"> + <span>Use</span> + <button type="button" data-metric="users" aria-pressed="false">Number of users</button> + <button type="button" data-metric="prevalence" aria-pressed="false">Share of adults</button> + <button type="button" data-metric="ratio" aria-pressed="false">Share of users ÷ index</button> + </div> + {SHOW_PRICES && ( + <div class="metric-group"> + <span>Price</span> + {PRICE_MARKETS.map((market) => <button type="button" data-metric={`price:${market}`} aria-pressed="false">{PRICE_MARKET_LABEL[market]}</button>)} + </div> + )} + <div class="metric-group"> + <span>Context</span> + {Object.entries(PLANTING_FIELDS).map(([field, label]) => <button type="button" data-metric={`plant:${field}`} aria-pressed="false">{label}</button>)} + <button type="button" data-metric="clubs" aria-pressed="false">Cannabis clubs</button> + </div> + </div> + <p class="sub map-sub" id="mapSub" data-score="Red means prison risk, green means free. Select a country for its full breakdown." data-use="The stronger the blue, the more people use cannabis. Select a country for its full breakdown." data-ratio="The stronger the blue, the further cannabis use runs ahead of the freedom to use it. Select a country for its full breakdown." data-price="Pale yellow means cheaper, violet means more expensive. Countries without a sourced price are hatched." data-planting="Green means legal or sold freely, red means illegal or banned. Hatched countries have not been researched yet. Not part of the score." data-clubs="Green means cannabis clubs are regulated or tolerated, orange that they work underground, red that none are known. Hatched countries have not been researched yet. Not part of the score.">Red means prison risk, green means free. Select a country for its full breakdown.</p> <div class="card map-card"> <div class="map-wrap" id="mapWrap"> @@ -118,12 +159,17 @@ const streetFlower = (c) => latestPricesFor(c.id).street; <span class="legend-note">{PRICE_SCALE.note}</span> </div>} + <div class="legend" data-legend="clubs" hidden> + {Object.values(CLUBS_SCALE).map((v) => <span title={v.help}><i class="sw" style={`background:${v.color}`}></i>{v.label}</span>)} + <span><i class="sw sw-nodata"></i>Not researched yet</span> + <span class="legend-note">The footing cannabis clubs stand on, shown for context and not part of the score. Licensed shops and pharmacies are not counted as clubs.</span> + </div> {Object.entries(PLANTING_SCALES).map(([key, scale]) => ( <div class="legend" data-legend={`plant-${key}`} hidden> {Object.values(scale).map((v) => <span><i class="sw" style={`background:${v.color}`}></i>{v.label}</span>)} <span><i class="sw sw-nodata"></i>Not researched yet</span> - <span class="legend-note">Seed and clone rules are shown for context and are not part of the score. States and provinces take their country's status.</span> + <span class="legend-note">Seed and clone rules are shown for context and are not part of the score.</span> </div> ))} - <div class="map-under"> + <div class="map-under" id="mapUnder"> <label class="map-toggle" title="Show the United States, Canada and Australia state by state"> <input type="checkbox" id="showUnits"> States and provinces @@ -149,4 +195,16 @@ const streetFlower = (c) => latestPricesFor(c.id).street; {REC_ORDER.map((k) => <option value={k}>{REC_LABEL[k]}</option>)} </select> + <details class="col-menu" id="colMenu"> + <summary>Columns</summary> + <div class="col-panel"> + {columnGroups.map(([group, cols], g) => ( + <fieldset> + <legend>{group}</legend> + {cols.map((col) => <label><input type="checkbox" data-col={col.key} checked={g === 0}> {col.label}</label>)} + </fieldset> + ))} + <button type="button" class="btn" id="colReset">Reset</button> + </div> + </details> </div> </div> @@ -157,10 +215,11 @@ const streetFlower = (c) => latestPricesFor(c.id).street; <th data-sort="rank" class="num" aria-sort="ascending">#</th> <th data-sort="name">Country</th> - <th data-sort="region" class="hide-sm">Region</th> - <th data-sort="users" class="num-col hide-md" title="People who used cannabis in the past year">Users</th> - <th data-sort="share" class="num-col hide-md" title="Share of people aged 15–64 who used cannabis in the past year">Share</th> - {SHOW_PRICES && <th data-sort="price" class="num-col hide-md" title="Latest sourced street-flower price, normalised to US dollars per gram">Price/g</th>} - <th data-sort="rec" class="hide-sm">Recreational</th> - <th data-sort="med" class="hide-md">Medical</th> + <th data-sort="region" data-col="region" class="hide-sm">Region</th> + <th data-sort="users" data-col="users" class="num-col hide-md" title="People who used cannabis in the past year">Users</th> + <th data-sort="share" data-col="share" class="num-col hide-md" title="Share of people aged 15–64 who used cannabis in the past year">Share</th> + {SHOW_PRICES && <th data-sort="price" data-col="price" class="num-col hide-md" title="Latest sourced street-flower price, normalised to US dollars per gram">Price/g</th>} + <th data-sort="rec" data-col="rec" class="hide-sm">Recreational</th> + <th data-sort="med" data-col="med" class="hide-md">Medical</th> + {EXTRA_COLUMNS.map((col) => <th data-sort={col.key} data-col={col.key} class:list={["col-off", { "num-col": !col.chip }]}>{col.label}</th>)} <th data-sort="score" class="score-col">Index</th> </tr> @@ -169,13 +228,25 @@ const streetFlower = (c) => latestPricesFor(c.id).street; {places.map((c) => ( <tr data-id={c.id} data-rank={c.rank ?? c.worldRank - 0.5} data-name={c.name} data-region={c.region} - data-rec={REC_ORDER.indexOf(c.rec)} data-med={MED_ORDER.indexOf(c.med)} data-reckey={c.rec} data-users={c.use.users} data-share={c.use.prevalence} data-price={SHOW_PRICES ? streetFlower(c)?.usd ?? -1 : undefined}> + data-rec={REC_ORDER.indexOf(c.rec)} data-med={MED_ORDER.indexOf(c.med)} data-reckey={c.rec} data-users={c.use.users} data-share={c.use.prevalence} data-price={SHOW_PRICES ? streetFlower(c)?.usd ?? -1 : undefined} + {...Object.fromEntries(EXTRA_COLUMNS.map((col) => [`data-${col.key}`, col.sort(c)]))}> <td class="num">{c.rank ?? "–"}</td> <td><a class="cname" href={`${base}country/${c.slug}/`}><span class="flag">{c.flag}</span>{c.name}</a>{c.disputed && <span class="tag-disputed" title={c.disputed.note}>disputed</span>}</td> - <td class="hide-sm">{c.region}</td> … 134 more lines
src/styles/global.css +46 @@ -170,5 +170,5 @@ select:focus-visible, input:focus-visible, button:focus-visible, tr:focus-visibl /* ── Table ──────────────────────────────────────────── */ -.table-card { overflow: hidden; } +.table-card { overflow-x: auto; } table { width: 100%; border-collapse: collapse; } th { @@ -351,4 +351,9 @@ th.num-col { text-align: right; } .country-page .d-flag { font-size: 56px; } .country-page .c-main > .d-h:first-child { margin-top: 0; } +/* Score over time: beside the head in the drawer, under the score breakdown on the full page */ +.chart-in-main { display: none; } +.country-page .chart-in-side { display: none; } +.country-page .chart-in-main { display: block; margin-top: 26px; } +.country-page .chart-in-main .c-chart { margin-top: 0; } .country-page .why p, .country-page .ctx { font-size: 15px; max-width: 72ch; } .c-sources ol { margin: 6px 0 0; padding-left: 20px; color: var(--muted); font-size: 13.5px; } @@ -747,6 +752,45 @@ table.cani tr[hidden] { display: none; } .ca-all { margin: 10px 0 0; font-size: 13px; } .ca-all a { color: var(--text-2); text-decoration: underline; text-underline-offset: 3px; } +/* History list: the same two columns, with the history in numbers on the right */ +.hist-layout { margin-top: 28px; } +.hist-layout .chlog { margin-top: 0; } +/* taller than most screens: it scrolls on its own so the lists at the bottom stay reachable */ +.hist-aside { max-height: calc(100vh - 104px); overflow-y: auto; scrollbar-width: thin; padding-right: 8px; } +.hs-title { margin: 0; font-size: 17px; } +.hs-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 16px; margin: 0; } +.hs-grid dt { color: var(--text-2); font-size: 12.5px; line-height: 1.3; } +.hs-grid dd { margin: 0; font-family: "Space Grotesk", sans-serif; font-size: 26px; font-weight: 700; line-height: 1.15; font-variant-numeric: tabular-nums; } +.hist-aside .ca-note a { text-decoration: underline; text-underline-offset: 3px; } +.hs-top li a { grid-template-columns: 1fr auto; align-items: baseline; gap: 10px; } @media (max-width: 980px) { .change-layout { grid-template-columns: minmax(0, 1fr); gap: 32px; } .change-aside { position: static; } -} + .hist-aside { order: -1; max-height: none; overflow: visible; padding-right: 0; } + .hs-top { display: none; } +} + +/* Map: what to colour by, as buttons grouped by kind */ +.metric-bar { display: flex; flex-wrap: wrap; gap: 8px 22px; margin: 0 0 14px; } +.metric-group { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } +.metric-group > span { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; margin-right: 2px; } +.metric-group button { padding: 5px 11px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface); color: var(--text-2); font: inherit; font-size: 13px; cursor: pointer; } +.metric-group button:hover { border-color: var(--muted); color: var(--text); } +.metric-group button[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 600; } +.map-sub { margin: 0 0 14px; } + +/* Rankings: choose which columns to show */ +.col-off { display: none !important; } +.col-menu { position: relative; } +.col-menu summary { list-style: none; cursor: pointer; padding: 8px 12px; border: 1px solid var(--border); border-radius: 9px; background: var(--surface); color: var(--text); font-size: 14px; user-select: none; } +.col-menu summary::-webkit-details-marker { display: none; } +.col-menu summary::after { content: " ▾"; color: var(--muted); } +.col-menu[open] summary { border-color: var(--muted); } +.col-panel { position: absolute; right: 0; top: calc(100% + 6px); z-index: 30; width: min(560px, calc(100vw - 32px)); padding: 14px 16px; display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px 18px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: 0 12px 32px rgb(0 0 0 / .28); } +.col-panel fieldset { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; min-width: 0; } +.col-panel legend { padding: 0; margin-bottom: 4px; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; } +.col-panel label { display: flex; align-items: center; gap: 7px; font-size: 13.5px; color: var(--text-2); cursor: pointer; } +.col-panel .btn { justify-self: start; align-self: end; } +#table td .pill { display: inline-block; min-width: 30px; font-size: 13.5px; padding: 1px 8px; } +#table td .chip { white-space: nowrap; } +@media (max-width: 860px) { .col-panel { right: auto; left: 0; } } +#table td[data-col="region"] { white-space: nowrap; }