Spaces:
Running
Running
Download index.html from derek-thomas/ADPC-stats: direct link, hf CLI and curl.
- Browser
- Download file 41.6 kB
-
https://huggingface.co/spaces/derek-thomas/ADPC-stats/resolve/main/index.html
- Command line
-
hf download hf://spaces/derek-thomas/ADPC-stats/index.html
-
curl -L -o index.html https://huggingface.co/spaces/derek-thomas/ADPC-stats/resolve/main/index.html
41.6 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Abu Dhabi needs pickleball courts: the case from 16 months of play</title> | |
| </head> | |
| <body> | |
| <nav class="sitenav"><a href="stats.html">Full group analysis →</a></nav> | |
| <style>.sitenav{max-width:980px;margin:0 auto 1.4rem;font:600 .78rem/1 system-ui,-apple-system,sans-serif; | |
| letter-spacing:.06em;text-transform:uppercase} | |
| .sitenav a{color:inherit;opacity:.62;text-decoration:none;border-bottom:1px solid currentColor;padding-bottom:2px} | |
| .sitenav a:hover,.sitenav a:focus-visible{opacity:1}</style> | |
| <style> | |
| :root{ | |
| --plane:#e9edee; --surface:#f7f8f8; --ink:#0e1a20; --ink-2:#4a5860; --muted:#7d8c93; | |
| --grid:#dde3e5; --axis:#bcc7cb; --ring:rgba(14,26,32,.12); | |
| --series:#2a78d6; --wash:rgba(42,120,214,.10); --band:rgba(14,26,32,.055); | |
| --mark:#e8d21a; | |
| --sp:clamp(1rem,3vw,1.7rem); | |
| } | |
| @media (prefers-color-scheme:dark){ :root:where(:not([data-theme="light"])){ | |
| --plane:#0b0e0f; --surface:#171a1c; --ink:#fff; --ink-2:#b9c4c8; --muted:#8b979c; | |
| --grid:#272c2f; --axis:#3a4144; --ring:rgba(255,255,255,.12); | |
| --series:#3987e5; --wash:rgba(57,135,229,.14); --band:rgba(255,255,255,.05); | |
| --mark:#b9a50c; | |
| }} | |
| :root[data-theme="dark"]{ | |
| --plane:#0b0e0f; --surface:#171a1c; --ink:#fff; --ink-2:#b9c4c8; --muted:#8b979c; | |
| --grid:#272c2f; --axis:#3a4144; --ring:rgba(255,255,255,.12); | |
| --series:#3987e5; --wash:rgba(57,135,229,.14); --band:rgba(255,255,255,.05); | |
| --mark:#b9a50c; | |
| } | |
| *{box-sizing:border-box} | |
| body{background:var(--plane);color:var(--ink); | |
| font:400 16px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif;margin:0;padding:var(--sp)} | |
| .wrap{max-width:980px;margin:0 auto;display:flex;flex-direction:column;gap:3rem} | |
| .eyebrow{font-size:.71rem;letter-spacing:.14em;text-transform:uppercase;color:var(--muted); | |
| font-weight:700;margin:0} | |
| h1{font-size:clamp(2rem,5.4vw,3.4rem);line-height:1.02;letter-spacing:-.035em;font-weight:700; | |
| margin:.7rem 0 0;text-wrap:balance;max-width:17ch} | |
| h2{font-size:clamp(1.35rem,3vw,1.9rem);line-height:1.12;letter-spacing:-.028em;font-weight:700; | |
| margin:.45rem 0 0;text-wrap:balance;max-width:24ch} | |
| h3{font-size:1rem;font-weight:700;letter-spacing:-.01em;margin:0} | |
| .mark{background:linear-gradient(transparent 62%,var(--mark) 62%,var(--mark) 94%,transparent 94%); | |
| padding:0 .06em} | |
| .lede{font-family:"Iowan Old Style",Georgia,serif;font-size:1.16rem;line-height:1.6; | |
| color:var(--ink-2);max-width:58ch;margin:1.3rem 0 0} | |
| .prose{font-family:"Iowan Old Style",Georgia,serif;line-height:1.68;color:var(--ink-2); | |
| max-width:62ch;margin:0} | |
| .prose b{color:var(--ink);font-weight:600} | |
| header{border-bottom:2px solid var(--ink);padding-bottom:2.2rem} | |
| .stats{display:flex;flex-wrap:wrap;gap:1px;background:var(--axis);border:1px solid var(--axis)} | |
| .stat{background:var(--surface);padding:1rem 1.05rem;display:flex;flex-direction:column;gap:.28rem; | |
| flex:1 1 140px;min-width:140px} | |
| .stat .k{font-size:.68rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-weight:700} | |
| .stat .v{font-size:1.7rem;font-weight:700;letter-spacing:-.035em;line-height:1} | |
| .stat .n{font-size:.79rem;color:var(--muted);line-height:1.4} | |
| .stat .n b{color:var(--ink);font-weight:700} | |
| .compare{display:flex;align-items:stretch;gap:1px;background:var(--axis); | |
| border:1px solid var(--axis);margin:.9rem 0;flex-wrap:wrap} | |
| .compare .cbox{background:var(--surface);flex:1 1 190px;min-width:170px; | |
| padding:.95rem 1.1rem;display:flex;flex-direction:column;gap:.3rem} | |
| .compare .cbox.now{background:var(--wash)} | |
| .compare .cn{font-size:.68rem;letter-spacing:.08em;text-transform:uppercase; | |
| color:var(--muted);font-weight:700} | |
| .compare .cv{font-size:1.7rem;font-weight:700;letter-spacing:-.035em;line-height:1} | |
| .compare .cv .cu{font-size:.85rem;font-weight:600;color:var(--muted);margin-left:.25rem} | |
| .compare .cr{font-size:.79rem;color:var(--ink-2);line-height:1.4} | |
| .compare .arrow{flex:0 0 auto;min-width:76px;display:flex;flex-direction:column; | |
| align-items:center;justify-content:center;text-align:center;background:var(--surface); | |
| padding:.5rem .6rem;font-size:.72rem;font-weight:700;color:var(--ink);letter-spacing:.01em; | |
| line-height:1.3} | |
| .compare .arrow .x{font-size:1.15rem;letter-spacing:-.02em} | |
| @media (max-width:520px){.compare{flex-direction:column}.compare .arrow{min-width:0}} | |
| .ev{display:flex;flex-direction:column;gap:1.15rem} | |
| .evhead{display:flex;flex-direction:column;gap:.1rem} | |
| .panel{background:var(--surface);border:1px solid var(--ring);padding:1.25rem var(--sp) 1rem; | |
| display:flex;flex-direction:column;gap:.3rem} | |
| .psub{font-size:.85rem;color:var(--muted);margin:0;max-width:60ch} | |
| .psub b{color:var(--ink);font-weight:700} | |
| .figbox{overflow-x:auto;margin-top:.75rem} | |
| svg{display:block;width:100%;height:auto;overflow:visible} | |
| .figbox.wide svg{min-width:560px} | |
| .tick{font-size:11px;fill:var(--muted);font-variant-numeric:tabular-nums} | |
| .tickb{font-size:11px;fill:var(--ink-2);font-weight:600;font-variant-numeric:tabular-nums} | |
| .dlabel{font-size:12.5px;font-weight:700;fill:var(--ink);font-variant-numeric:tabular-nums} | |
| .slabel{font-size:10.5px;font-weight:600;fill:var(--ink-2);font-variant-numeric:tabular-nums} | |
| .anno{font-size:11.5px;fill:var(--muted);font-style:italic} | |
| /* defined terms */ | |
| .terms{margin:0;padding:0;display:grid;gap:.7rem} | |
| .terms div{display:grid;grid-template-columns:minmax(9rem,11rem) 1fr;gap:.2rem .9rem} | |
| .terms dt{font-size:.82rem;font-weight:700;color:var(--ink);letter-spacing:-.01em} | |
| .terms dd{margin:0;font-size:.87rem;line-height:1.55;color:var(--ink-2)} | |
| .terms dd b{color:var(--ink);font-weight:600;font-variant-numeric:tabular-nums} | |
| @media (max-width:600px){.terms div{grid-template-columns:1fr}} | |
| /* numbered argument chain */ | |
| .chain{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:.9rem; | |
| counter-reset:step} | |
| .chain li{display:grid;grid-template-columns:1.55rem 1fr;gap:.8rem;align-items:baseline} | |
| .chain li::before{counter-increment:step;content:counter(step);font-size:.72rem;font-weight:700; | |
| color:var(--muted);letter-spacing:.08em;font-variant-numeric:tabular-nums; | |
| border-top:2px solid var(--axis);padding-top:.3rem} | |
| .chain p{margin:0;font-size:.95rem;line-height:1.6;color:var(--ink-2);max-width:58ch} | |
| .chain b{color:var(--ink);font-weight:600} | |
| .method{border-top:1px solid var(--grid);margin-top:.9rem;padding-top:.9rem; | |
| display:flex;flex-direction:column;gap:.55rem} | |
| .method .h{font-size:.67rem;letter-spacing:.11em;text-transform:uppercase;color:var(--muted); | |
| font-weight:700} | |
| .method p{margin:0;font-size:.83rem;line-height:1.6;color:var(--ink-2);max-width:62ch} | |
| .method b{color:var(--ink);font-weight:600;font-variant-numeric:tabular-nums} | |
| .flag{border-left:2px solid var(--axis);padding-left:.8rem} | |
| .ask{background:var(--surface);border:1px solid var(--ring);border-top:3px solid var(--ink); | |
| padding:1.6rem var(--sp);display:flex;flex-direction:column;gap:1rem} | |
| .asklist{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:.85rem} | |
| .asklist li{display:grid;grid-template-columns:auto 1fr;gap:.85rem;align-items:baseline} | |
| .asklist .n{font-size:.72rem;font-weight:700;color:var(--muted);letter-spacing:.08em; | |
| font-variant-numeric:tabular-nums} | |
| .asklist p{margin:0;font-size:.95rem;line-height:1.55;color:var(--ink-2);max-width:58ch} | |
| .asklist b{color:var(--ink);font-weight:600} | |
| .note{border-top:1px solid var(--axis);padding-top:1.3rem;display:flex;flex-direction:column;gap:.7rem} | |
| .note h3{font-size:.71rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);font-weight:700} | |
| .note p{margin:0;font-size:.9rem;line-height:1.62;color:var(--ink-2);max-width:64ch} | |
| .fnref{color:var(--ink-2);text-decoration:none;border-bottom:1px dotted var(--muted);white-space:nowrap} | |
| .fnref:hover,.fnref:focus-visible{color:var(--ink);border-bottom-style:solid} | |
| #fn-times{scroll-margin-top:1.4rem} | |
| #evidence-03{scroll-margin-top:1rem} | |
| #tip{position:fixed;pointer-events:none;opacity:0;transition:opacity .1s;z-index:9; | |
| background:var(--surface);border:1px solid var(--axis);padding:.5rem .7rem;font-size:.78rem; | |
| line-height:1.45;color:var(--ink);box-shadow:0 4px 18px rgba(0,0,0,.16); | |
| font-variant-numeric:tabular-nums;white-space:nowrap} | |
| #tip b{font-weight:700} | |
| #tip .r{display:flex;justify-content:space-between;gap:1rem;color:var(--ink-2)} | |
| #tip .r i{font-style:normal;color:var(--ink);font-weight:600} | |
| @media (prefers-reduced-motion:reduce){*{transition:none}} | |
| </style> | |
| <div class="wrap"> | |
| <header> | |
| <p class="eyebrow">Abu Dhabi Pickleball Club · a case for public courts</p> | |
| <h1>The players are here. Dedicated <span class="mark">courts</span> are not.</h1> | |
| <p class="lede">Pickleball is a paddle sport, usually played in doubles on a court | |
| about a quarter the size of a tennis court. It arrived in Abu Dhabi as one | |
| Tuesday morning pickup game in April 2025. Sixteen months later, the club has | |
| <b>505 players</b>, coordinating play across five borrowed venues, none of | |
| them built for the sport and none of them the club's own. The group's scheduling | |
| data shows the demand already here.</p> | |
| </header> | |
| <div class="stats"> | |
| <div class="stat"><span class="k">Players</span><span class="v">505</span><span class="n">up from <b>13</b> founder members</span></div> | |
| <div class="stat"><span class="k">Joining now</span><span class="v">48<span style="font-size:.9rem">/mo</span></span><span class="n">the last 200 members joined in about 4 months</span></div> | |
| <div class="stat"><span class="k">Sessions run</span><span class="v">253</span><span class="n">scheduled by the club</span></div> | |
| <div class="stat"><span class="k">Sign-ups</span><span class="v">3,199</span><span class="n">13 per session on average</span></div> | |
| <div class="stat"><span class="k">Venues used</span><span class="v">5</span><span class="n">none of them ours</span></div> | |
| </div> | |
| <section class="ev"> | |
| <div class="evhead"> | |
| <p class="eyebrow">Evidence 01 · Growth</p> | |
| <h2>Membership has grown every month, and growth is getting faster</h2> | |
| </div> | |
| <div class="panel"> | |
| <p class="psub">Members in the group, every day since it opened on | |
| 20 April 2025. Dots mark each hundredth member; the brackets underneath | |
| show how many days each hundred took to arrive.</p> | |
| <div class="figbox wide"><svg id="c1" viewBox="0 0 900 320" role="img" | |
| aria-label="Membership rises without interruption from 14 members in April 2025 to | |
| 505 in August 2026. The curve steepens from March 2026 onward."></svg></div> | |
| </div> | |
| <p class="prose">In sixteen months membership has never once shrunk on a | |
| month-over-month basis: people join, a few leave, but every month has ended | |
| higher than it started. Growth is getting faster. The next 200 people joined | |
| in about half the time:</p> | |
| <div class="compare"> | |
| <div class="cbox"> | |
| <span class="cn">Members 100 → 300</span> | |
| <span class="cv">24<span class="cu">/mo</span></span> | |
| <span class="cr">249 days, just over 8 months (117 then 132, shown above)</span> | |
| </div> | |
| <div class="arrow"><span class="x">2×</span>the pace</div> | |
| <div class="cbox now"> | |
| <span class="cn">Members 300 → 500 · now</span> | |
| <span class="cv">48<span class="cu">/mo</span></span> | |
| <span class="cr">126 days, about 4 months (55 then 71, shown above)</span> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <p class="psub">Net members added each month: <b>joined</b> minus | |
| <b>left</b>. A taller bar means more members were added.</p> | |
| <div class="figbox wide"><svg id="c2" viewBox="0 0 900 250" role="img" | |
| aria-label="Monthly net growth ran between 12 and 44 members through 2025, dipped | |
| to 11 in March 2026, then jumped to 43, 73, 45 and 42 in the four months after."></svg></div> | |
| </div> | |
| <p class="prose">The slower months, from September to December 2025 and in February | |
| and March 2026, were also the months with the fewest organised sessions. This points | |
| to a shortage of court time, rather than a lack of interest.</p> | |
| </section> | |
| <section class="ev"> | |
| <div class="evhead"> | |
| <p class="eyebrow">Evidence 02 · Utilisation</p> | |
| <h2>Sessions are increasing faster than membership, but places are limited</h2> | |
| </div> | |
| <div class="panel"> | |
| <p class="psub">Four terms are used below. Each one is a count of something | |
| in the message record, not an estimate.</p> | |
| <dl class="terms"> | |
| <div><dt>Organised session</dt> | |
| <dd>A scheduling poll posted in the group naming a day, a venue and usually a | |
| time. <b>253</b> were found. Games arranged by phone, in person, or in side chats | |
| are not counted, so 253 is a minimum, not the total.</dd></div> | |
| <div><dt>Sign-up</dt> | |
| <dd>One member voting on one of those polls. There were <b>3,199</b> votes, an | |
| average of <b>12.6</b> per session. The busiest session had <b>47</b>.</dd></div> | |
| <div><dt>Active member</dt> | |
| <dd>A member who posted at least once in a given month: <b>10</b> in April 2025, | |
| <b>131</b> in May 2026.</dd></div> | |
| <div><dt>At capacity</dt> | |
| <dd>A session slot with a player limit, usually “max 12 members,” where | |
| sign-ups reached or passed that limit.</dd></div> | |
| </dl> | |
| </div> | |
| <div class="panel"> | |
| <div class="figbox wide"><svg id="c3" viewBox="0 0 900 260" role="img" | |
| aria-label="Monthly organised sessions rose from 1 in June 2025 to a peak of 46 in | |
| May 2026, holding near 40 per month since."></svg></div> | |
| <div class="method"> | |
| <p class="h">Reading the dip</p> | |
| <p>Sessions fall away between November 2025 and March 2026 while the group itself | |
| stayed busy, with <b>561</b> to <b>1,192</b> messages a month in that period. Play | |
| was being arranged through recurring pinned posts rather than fresh polls, so the | |
| poll count misses that period rather than showing a real lull in play.</p> | |
| </div> | |
| </div> | |
| <ol class="chain"> | |
| <li><p><b>The schedule grew 40 times over.</b> One organised session in June 2025; | |
| <b>46</b> in May 2026; <b>39</b> in each of the three months since. That is better | |
| than one session a day, every day, on courts borrowed from other sports.</p></li> | |
| <li><p><b>It isn't just more members.</b> Measured against the size of the group, the | |
| schedule still grew six times over: <b>1.3</b> sessions per 100 members in June | |
| 2025, <b>7.9</b> per 100 in July 2026. Members joined fast; play per member rose | |
| faster.</p></li> | |
| <li><p><b>Each session is a crowd, not a casual hit.</b> The average session collects | |
| <b>12.6</b> sign-ups. A pickleball court holds four players. On average, that is | |
| enough people for three courts at the venues the group can borrow.</p></li> | |
| <li><p><b>The limit is already binding.</b> Of <b>129</b> session slots that stated a | |
| player cap, <b>26</b> filled it exactly and <b>15</b> went over it. In those cases, | |
| people signed up after the organiser had announced the limit. Waitlists are | |
| mentioned <b>116</b> times and published as numbered lists <b>68</b> times.</p></li> | |
| </ol> | |
| <p class="prose">Membership numbers alone do not show who wants to play. The group | |
| uses player caps, numbered sign-up sheets, and waitlists “in case someone drops out.” | |
| These are clear signs that the number of available courts limits play.</p> | |
| </section> | |
| <section class="ev" id="evidence-03"> | |
| <div class="evhead"> | |
| <p class="eyebrow">Evidence 03 · The limit</p> | |
| <h2>Play is squeezed into the hours the climate allows</h2> | |
| </div> | |
| <div class="panel"> | |
| <p class="psub"><b>368 session slots</b> with a stated start time, from <b>20 April | |
| 2025 – 8 August 2026</b>, were read from 253 polls. Some polls | |
| offer several slots at once. <a href="#fn-times" class="fnref">How | |
| that's counted ↓</a>.</p> | |
| <p class="psub" style="margin-top:.6rem">This counts every hour a session was actually | |
| running, not just the hour it started. For example, a 7:30–9:30pm slot counts toward | |
| both 7pm and 8pm. That shows how many sessions were happening at once, through the | |
| day. A slot with no stated end is assumed to run 2 hours.</p> | |
| <div class="figbox wide"><svg id="c4" viewBox="0 0 900 290" role="img" | |
| aria-label="Sessions running by hour of day, 20 April 2025 to 8 August 2026: activity | |
| climbs from a 6am cluster of 37 to a peak of 165 around 7pm, staying high from 5pm to | |
| 10pm before dropping off. Almost nothing runs between 10am and 4pm, and nothing | |
| between midnight and 5am."></svg></div> | |
| </div> | |
| <p class="prose">The community mainly plays in two short time periods. | |
| <b>320 of 368 slots (87%) start at 5pm or later</b>, and another | |
| <b>44</b> start between 5 and 9 in the morning. Just <b>4</b> fall anywhere | |
| between 10am and 4pm, and that is forced by the weather, not a preference: | |
| from May to September an uncovered court in Abu Dhabi is unplayable in the middle | |
| of the day. Both of the windows the group actually uses are windows in which an | |
| unlit, unshaded court is worth very little.</p> | |
| </section> | |
| <section class="ev"> | |
| <div class="evhead"> | |
| <p class="eyebrow">Evidence 04 · The venues</p> | |
| <h2>Every court is borrowed, and none were built for this</h2> | |
| </div> | |
| <div class="panel"> | |
| <p class="psub">Sessions naming each venue in its scheduling poll. A | |
| relocated session can name two venues, so these do not sum to 253.</p> | |
| <dl class="terms"> | |
| <div><dt>Sadim Park</dt><dd><b>165</b> sessions</dd></div> | |
| <div><dt>Al Masar Park</dt><dd><b>76</b> sessions</dd></div> | |
| <div><dt>Raheeq Park</dt><dd><b>21</b> sessions</dd></div> | |
| <div><dt>Al Zuwar</dt><dd><b>6</b> sessions</dd></div> | |
| <div><dt>Masdar Park</dt><dd><b>2</b> sessions</dd></div> | |
| </dl> | |
| </div> | |
| <ol class="chain"> | |
| <li><p><b>Two venues carry almost all of it, and neither was built for | |
| pickleball.</b> Sadim and Al Masar between them cover the large majority | |
| of named sessions; Al Masar’s court is a tennis court used for pickleball | |
| instead, and other courts have been found by searching out unused park courts | |
| and taping pickleball lines onto them.</p></li> | |
| <li><p><b>The lights aren’t the club’s to keep.</b> Sadim is | |
| floodlit, and games there have run past midnight under the lights. The | |
| floodlights belong to the park, not the club. When officials | |
| switched off lighting across Khalifa City’s parks for over a week | |
| (24 March–1 April 2026), evening sessions at Sadim | |
| and Al Masar went dark together regardless of what the poll said, and | |
| organisers spent the week routing players to whichever park still had | |
| power.</p></li> | |
| <li><p><b>Weather closes the same two venues just as fast.</b> Rain has | |
| taken out Sadim and Al Masar together too, damaging the court surface | |
| and forcing a same-day move between them. Neither closure is something | |
| the club has any say over.</p></li> | |
| <li><p><b>Nothing is permanent.</b> Nets and paddles are hand-carried in | |
| and packed up every session, and play gets relocated whenever the | |
| borrowed court is already occupied by other park users.</p></li> | |
| </ol> | |
| </section> | |
| <div class="ask"> | |
| <h3>What would change with dedicated courts</h3> | |
| <ul class="asklist"> | |
| <li><span class="n">01</span><p><b>Lighting the club controls.</b> 87% of session | |
| slots start after 5pm, and the single most common start time is 7pm, in the dark. | |
| Borrowed floodlights work until a park operator switches them off. This happened | |
| across Khalifa City for over a week in March 2026, and the club could not change it.</p></li> | |
| <li><span class="n">02</span><p><b>Shaded or covered play.</b> Four session slots out | |
| of 368 in the 10am–4pm window is not a preference, it is heat. Shading existing | |
| courts converts six dead hours a day into usable ones without building a new | |
| facility.</p></li> | |
| <li><span class="n">03</span><p><b>Enough courts to stop capping sessions.</b> | |
| Organisers are already turning players away at 12 per slot, with published | |
| waitlists. Play is spread across five borrowed venues; a single permanent | |
| home would bring together a community that has grown from 13 members at the | |
| start to 505 in sixteen months.</p></li> | |
| </ul> | |
| </div> | |
| <div class="note"> | |
| <h3>About this data</h3> | |
| <p>Everything here is drawn from the exported message history of a single | |
| community WhatsApp group, from 20 April 2025 to 8 August 2026. It contains 38,166 lines, | |
| 13,609 messages written by members, and 943 membership events. No message | |
| content and no member’s identity appears anywhere on this page. Every figure is a | |
| count, not a quote. Membership is | |
| built up from every add, invite, leave and removal event; this method comes out | |
| five above the <b>505</b> members WhatsApp itself reports, a difference in how | |
| the group’s first members are counted, and 505 is the figure used | |
| here.</p> | |
| <p>Sessions are counted as scheduling polls posted in the group, which misses play | |
| arranged by other means and misses more of it between November 2025 and March 2026. | |
| Sign-ups are votes cast on those polls; a vote is a stated plan to attend, not a | |
| count of who actually showed up. Session start times show when play was <i>planned</i>, | |
| which is the closest available measure of when it happened. See below for how times | |
| are read. Venues are counted when one of five known place names, the parks this | |
| group has used, appears in a session’s poll text. A session can | |
| name more than one when play was moved.</p> | |
| <p id="fn-times">How session start times are read: most polls give one time for the | |
| whole session. But 80 of the 253 polls, almost all from 30 April 2026, when | |
| evening polls began listing 5:30pm, 7:30pm and 9:30pm as separate choices in the same | |
| poll, give more than one time. Each choice is counted as its own slot with | |
| its own sign-ups, instead of the whole poll being counted once. Of the 368 slots with a | |
| stated time: 163 give a clock time with am or pm (“7–9:30pm”) and are | |
| used exactly as written; 205 give a plain range such as “7 to 9.30” together | |
| with the word <i>morning</i> or <i>night</i>, often in the poll’s own title | |
| (“SADIM NIGHT”) rather than in the choice itself. This shows which | |
| one it is. A further 17 polls name a time of day only (“Sunday morning”, | |
| “under the lights”) with no clock time, and 39 give no time at all; neither | |
| group appears on the chart. Numbers that are not times are taken out first: dates, | |
| scores, and player limits (“MAX 12 MEMBERS”). An earlier version of | |
| this count included those numbers and wrongly showed games starting at 3 and 4 in the | |
| morning. No session slot in the record actually starts later than 9pm or before 5am; | |
| the 9pm cluster is real and repeats often, not a mistake. The chart above counts a | |
| slot in every hour it runs, not just the hour it starts: when a poll states an end | |
| time that's used directly (“7–9:30pm” runs through the 7 and 8 | |
| o’clock hours); when only a start is given, the slot is assumed to run 2 hours, | |
| matching all but a handful of the sessions that do state both ends. | |
| <a href="#evidence-03" class="fnref">Back to the chart ↑</a></p> | |
| <p>This is one group, not a full count of everyone who plays the sport in Abu Dhabi. | |
| Other clubs and informal play are not included, so these figures should be | |
| read as the least demand could be, not the whole picture. August 2026 covers eight | |
| days and is drawn | |
| dashed. Every figure on this page is produced by <code>analysis/extract.py</code> | |
| in the Space repository, run against the export.</p> | |
| </div> | |
| </div> | |
| <div id="tip" role="status" aria-live="polite"></div> | |
| <script> | |
| const DAILY = [14, 15, 19, 19, 19, 22, 23, 25, 25, 26, 26, 27, 27, 27, 27, 27, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 31, 31, 31, 31, 31, 34, 39, 42, 42, 42, 43, 43, 43, 50, 52, 52, 52, 52, 52, 53, 54, 60, 61, 62, 63, 63, 63, 63, 66, 66, 66, 66, 67, 68, 68, 68, 71, 73, 74, 74, 74, 76, 76, 76, 76, 78, 78, 79, 84, 85, 85, 84, 85, 86, 86, 86, 87, 87, 87, 88, 87, 87, 88, 88, 88, 88, 88, 90, 90, 91, 93, 106, 115, 120, 120, 120, 121, 122, 122, 122, 122, 122, 123, 124, 127, 127, 127, 127, 128, 128, 129, 130, 133, 134, 138, 139, 141, 141, 141, 142, 142, 142, 146, 146, 148, 150, 152, 154, 154, 153, 153, 153, 153, 154, 154, 154, 154, 154, 154, 154, 157, 157, 157, 156, 155, 155, 156, 159, 159, 159, 161, 161, 162, 164, 167, 167, 167, 168, 169, 171, 171, 172, 172, 172, 173, 173, 171, 175, 175, 177, 177, 177, 176, 177, 177, 178, 179, 178, 178, 179, 179, 179, 177, 177, 177, 180, 182, 181, 184, 185, 188, 188, 189, 190, 191, 191, 192, 192, 193, 192, 192, 195, 196, 196, 196, 198, 198, 198, 202, 206, 209, 211, 211, 211, 214, 214, 213, 211, 212, 215, 215, 215, 216, 216, 221, 221, 222, 222, 222, 222, 222, 223, 222, 224, 224, 223, 222, 221, 220, 220, 221, 221, 222, 223, 223, 223, 225, 225, 225, 225, 228, 228, 228, 229, 229, 233, 233, 234, 238, 244, 242, 242, 244, 246, 245, 246, 249, 253, 254, 256, 259, 259, 257, 259, 263, 262, 262, 263, 265, 269, 269, 269, 269, 270, 271, 272, 272, 272, 273, 272, 272, 276, 277, 275, 274, 276, 276, 276, 276, 275, 276, 276, 275, 275, 274, 277, 277, 277, 277, 276, 276, 276, 275, 274, 274, 276, 276, 276, 277, 278, 279, 280, 280, 282, 283, 283, 284, 285, 285, 284, 284, 285, 283, 285, 284, 283, 284, 288, 289, 294, 300, 302, 301, 302, 302, 302, 303, 304, 311, 313, 313, 316, 321, 322, 323, 325, 327, 329, 331, 330, 332, 329, 330, 331, 330, 329, 329, 331, 334, 337, 335, 340, 341, 343, 352, 356, 357, 357, 359, 361, 360, 362, 365, 372, 375, 382, 384, 384, 390, 390, 392, 393, 396, 396, 399, 400, 399, 399, 404, 408, 413, 416, 418, 420, 421, 424, 425, 426, 426, 430, 431, 435, 439, 440, 439, 439, 439, 442, 442, 444, 442, 446, 446, 447, 447, 447, 448, 447, 449, 450, 450, 450, 447, 450, 451, 448, 446, 445, 445, 445, 460, 464, 468, 473, 474, 477, 477, 480, 480, 481, 482, 482, 482, 483, 485, 484, 491, 490, 490, 491, 492, 491, 493, 494, 497, 499, 503, 505]; | |
| const M = [{"month":"2025-04","members":26,"joined":13,"left":0,"active":10,"msgs":63,"pct":38.5},{"month":"2025-05","members":50,"joined":24,"left":0,"active":21,"msgs":127,"pct":42.0},{"month":"2025-06","members":76,"joined":26,"left":0,"active":33,"msgs":562,"pct":43.4},{"month":"2025-07","members":120,"joined":48,"left":4,"active":60,"msgs":981,"pct":50.0},{"month":"2025-08","members":150,"joined":37,"left":7,"active":75,"msgs":1363,"pct":50.0},{"month":"2025-09","members":167,"joined":25,"left":8,"active":74,"msgs":1001,"pct":44.3},{"month":"2025-10","members":182,"joined":25,"left":10,"active":61,"msgs":433,"pct":33.5},{"month":"2025-11","members":213,"joined":45,"left":14,"active":63,"msgs":919,"pct":29.6},{"month":"2025-12","members":225,"joined":28,"left":16,"active":67,"msgs":687,"pct":29.8},{"month":"2026-01","members":265,"joined":58,"left":18,"active":98,"msgs":1028,"pct":37.0},{"month":"2026-02","members":277,"joined":26,"left":14,"active":79,"msgs":561,"pct":28.5},{"month":"2026-03","members":288,"joined":24,"left":13,"active":74,"msgs":962,"pct":25.7},{"month":"2026-04","members":331,"joined":68,"left":25,"active":122,"msgs":1924,"pct":36.9},{"month":"2026-05","members":404,"joined":94,"left":21,"active":131,"msgs":1480,"pct":32.4},{"month":"2026-06","members":449,"joined":81,"left":36,"active":123,"msgs":870,"pct":27.4},{"month":"2026-07","members":491,"joined":64,"left":22,"active":89,"msgs":561,"pct":18.1},{"month":"2026-08","members":505,"joined":17,"left":3,"active":19,"msgs":55,"pct":3.8}]; | |
| const POLLS = [{"month":"2025-04","n":0},{"month":"2025-05","n":0},{"month":"2025-06","n":1},{"month":"2025-07","n":0},{"month":"2025-08","n":2},{"month":"2025-09","n":12},{"month":"2025-10","n":26},{"month":"2025-11","n":11},{"month":"2025-12","n":3},{"month":"2026-01","n":9},{"month":"2026-02","n":7},{"month":"2026-03","n":11},{"month":"2026-04","n":37},{"month":"2026-05","n":46},{"month":"2026-06","n":39},{"month":"2026-07","n":39},{"month":"2026-08","n":10}]; | |
| const MS = [{"milestone":100,"date":"2025-07-28","days":99},{"milestone":200,"date":"2025-11-22","days":117},{"milestone":300,"date":"2026-04-03","days":132},{"milestone":400,"date":"2026-05-28","days":55},{"milestone":500,"date":"2026-08-07","days":71}]; | |
| const RUNNING = [0,0,0,0,0,1,37,42,29,21,10,1,4,4,4,4,5,101,112,165,162,112,105,18]; | |
| const NS = "http://www.w3.org/2000/svg"; | |
| const MN = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; | |
| const START = Date.UTC(2025, 3, 20); | |
| const PARTIAL = POLLS.length - 1; // Aug 2026 = 8 days only | |
| const el = (t, a) => { const n = document.createElementNS(NS, t); | |
| for (const k in a) n.setAttribute(k, a[k]); return n; }; | |
| const fmtM = s => { const [y, m] = s.split("-"); return MN[+m - 1] + " " + y.slice(2); }; | |
| const fmtD = s => { const d = new Date(s + "T00:00:00Z"); | |
| return d.getUTCDate() + " " + MN[d.getUTCMonth()] + " " + d.getUTCFullYear(); }; | |
| const dayLabel = i => { const d = new Date(START + i * 864e5); | |
| return d.getUTCDate() + " " + MN[d.getUTCMonth()] + " " + d.getUTCFullYear(); }; | |
| const hLab = h => (h % 12 === 0 ? 12 : h % 12) + (h < 12 ? "am" : "pm"); | |
| function bar(x, w, base, val, r) { | |
| const h = Math.abs(val - base); if (h < .5) return ""; | |
| const rr = Math.min(r, h, w / 2); | |
| return "M" + x + "," + base + " V" + (val + rr) + " q0," + (-rr) + " " + rr + "," + (-rr) + | |
| " h" + (w - 2 * rr) + " q" + rr + ",0 " + rr + "," + rr + " V" + base + " Z"; | |
| } | |
| const tip = document.getElementById("tip"); | |
| function showTip(e, html) { | |
| tip.innerHTML = html; tip.style.opacity = 1; | |
| const r = tip.getBoundingClientRect(); | |
| let x = e.clientX + 14, y = e.clientY - r.height - 12; | |
| if (x + r.width > innerWidth - 8) x = e.clientX - r.width - 14; | |
| if (y < 8) y = e.clientY + 18; | |
| tip.style.left = x + "px"; tip.style.top = y + "px"; | |
| } | |
| const hideTip = () => tip.style.opacity = 0; | |
| /* --- 1. membership, day by day, with milestone brackets --- */ | |
| (function () { | |
| const svg = document.getElementById("c1"); | |
| const W = 900, H = 320, L = 46, R = 54, T = 14, B = 78; | |
| const pw = W - L - R, ph = H - T - B, max = 550; | |
| const X = i => L + i * pw / (DAILY.length - 1); | |
| const Y = v => T + ph - v / max * ph; | |
| for (let v = 0; v <= 500; v += 100) { | |
| svg.append(el("line", { x1: L, x2: L + pw, y1: Y(v), y2: Y(v), | |
| stroke: "var(--grid)", "stroke-width": 1 })); | |
| const t = el("text", { x: L - 8, y: Y(v) + 4, "text-anchor": "end", class: "tick" }); | |
| t.textContent = v; svg.append(t); | |
| } | |
| svg.append(el("line", { x1: L, x2: L + pw, y1: Y(0), y2: Y(0), | |
| stroke: "var(--axis)", "stroke-width": 1 })); | |
| const pts = DAILY.map((v, i) => X(i) + "," + Y(v)).join(" "); | |
| svg.append(el("path", { d: "M" + L + "," + Y(0) + " L" + pts.replace(/ /g, " L") + | |
| " L" + X(DAILY.length - 1) + "," + Y(0) + " Z", fill: "var(--wash)" })); | |
| svg.append(el("polyline", { points: pts, fill: "none", stroke: "var(--series)", | |
| "stroke-width": 2.2, "stroke-linejoin": "round" })); | |
| /* month ticks, skipped when they would collide */ | |
| let lastX = -99; | |
| for (let i = 0; i < POLLS.length; i++) { | |
| const [y, mo] = POLLS[i].month.split("-"); | |
| const idx = Math.max(0, Math.round((Date.UTC(+y, +mo - 1, 1) - START) / 864e5)); | |
| if (idx > DAILY.length - 1) continue; | |
| if (X(idx) - lastX < 26) continue; | |
| lastX = X(idx); | |
| const t = el("text", { x: X(idx), y: T + ph + 16, "text-anchor": "middle", class: "tick" }); | |
| t.textContent = MN[+mo - 1]; | |
| svg.append(t); | |
| if (mo === "01" || i === 0) { | |
| const y2 = el("text", { x: X(idx), y: T + ph + 28, "text-anchor": "middle", class: "tick" }); | |
| y2.textContent = "20" + y.slice(2); svg.append(y2); | |
| } | |
| } | |
| /* milestone dots + brackets showing how long each hundred took */ | |
| const by = T + ph + 46; | |
| let prev = 0; | |
| MS.forEach(m => { | |
| const idx = Math.round((Date.UTC(+m.date.slice(0, 4), +m.date.slice(5, 7) - 1, | |
| +m.date.slice(8, 10)) - START) / 864e5); | |
| svg.append(el("circle", { cx: X(idx), cy: Y(m.milestone), r: 4.2, fill: "var(--series)", | |
| stroke: "var(--surface)", "stroke-width": 1.6 })); | |
| const lab = el("text", { x: X(idx) - 6, y: Y(m.milestone) - 9, "text-anchor": "end", | |
| class: "slabel" }); | |
| lab.textContent = m.milestone; svg.append(lab); | |
| svg.append(el("line", { x1: X(prev), x2: X(idx), y1: by, y2: by, | |
| stroke: "var(--axis)", "stroke-width": 1 })); | |
| [prev, idx].forEach(i => svg.append(el("line", { x1: X(i), x2: X(i), y1: by - 4, y2: by + 4, | |
| stroke: "var(--axis)", "stroke-width": 1 }))); | |
| const d = el("text", { x: (X(prev) + X(idx)) / 2, y: by + 16, "text-anchor": "middle", | |
| class: m.days < 100 ? "tickb" : "tick" }); | |
| d.textContent = m.days + " days"; svg.append(d); | |
| prev = idx; | |
| }); | |
| const last = DAILY.length - 1; | |
| const end = el("text", { x: X(last) + 9, y: Y(DAILY[last]) + 4, class: "dlabel" }); | |
| end.textContent = "505"; svg.append(end); | |
| const cross = el("line", { y1: T, y2: T + ph, stroke: "var(--axis)", "stroke-width": 1, opacity: 0 }); | |
| const dot = el("circle", { r: 4.5, fill: "var(--series)", stroke: "var(--surface)", | |
| "stroke-width": 1.6, opacity: 0 }); | |
| svg.append(cross, dot); | |
| const hit = el("rect", { x: L, y: T, width: pw, height: ph, fill: "transparent" }); | |
| hit.addEventListener("pointermove", e => { | |
| const b = svg.getBoundingClientRect(); | |
| const i = Math.max(0, Math.min(DAILY.length - 1, | |
| Math.round(((e.clientX - b.left) / b.width * 900 - L) / pw * (DAILY.length - 1)))); | |
| cross.setAttribute("x1", X(i)); cross.setAttribute("x2", X(i)); cross.setAttribute("opacity", 1); | |
| dot.setAttribute("cx", X(i)); dot.setAttribute("cy", Y(DAILY[i])); dot.setAttribute("opacity", 1); | |
| showTip(e, "<b>" + dayLabel(i) + "</b><div class='r'><span>Members</span><i>" + | |
| DAILY[i] + "</i></div>"); | |
| }); | |
| hit.addEventListener("pointerleave", () => { hideTip(); | |
| cross.setAttribute("opacity", 0); dot.setAttribute("opacity", 0); }); | |
| svg.append(hit); | |
| })(); | |
| /* --- 2. net members added each month --- */ | |
| (function () { | |
| const svg = document.getElementById("c2"); | |
| const W = 900, H = 250, L = 42, R = 16, T = 26, B = 40; | |
| const pw = W - L - R, ph = H - T - B, max = 80; | |
| const Y = v => T + ph - v / max * ph; | |
| const band = pw / M.length, bw = Math.min(26, band - 8); | |
| for (let v = 0; v <= max; v += 20) { | |
| svg.append(el("line", { x1: L, x2: L + pw, y1: Y(v), y2: Y(v), | |
| stroke: "var(--grid)", "stroke-width": 1 })); | |
| const t = el("text", { x: L - 8, y: Y(v) + 4, "text-anchor": "end", class: "tick" }); | |
| t.textContent = v; svg.append(t); | |
| } | |
| svg.append(el("line", { x1: L, x2: L + pw, y1: Y(0), y2: Y(0), | |
| stroke: "var(--axis)", "stroke-width": 1 })); | |
| M.forEach((m, i) => { | |
| const net = m.joined - m.left, part = i === PARTIAL; | |
| const x = L + i * band + (band - bw) / 2; | |
| const d = bar(x, bw, Y(0), Y(net), 4); | |
| if (d) svg.append(el("path", { d, fill: part ? "none" : "var(--series)", | |
| stroke: part ? "var(--series)" : "none", "stroke-width": part ? 1.5 : 0, | |
| "stroke-dasharray": part ? "3 2" : "" })); | |
| const v = el("text", { x: x + bw / 2, y: Y(net) - 7, "text-anchor": "middle", | |
| class: net >= 40 ? "dlabel" : "slabel" }); | |
| v.textContent = "+" + net; v.setAttribute("opacity", part ? .55 : 1); svg.append(v); | |
| const t = el("text", { x: x + bw / 2, y: H - 18, "text-anchor": "middle", class: "tick" }); | |
| t.textContent = fmtM(m.month).split(" ")[0]; | |
| t.setAttribute("opacity", part ? .55 : 1); svg.append(t); | |
| if (i === 0 || m.month.endsWith("-01")) { | |
| const y2 = el("text", { x: x + bw / 2, y: H - 5, "text-anchor": "middle", class: "tick" }); | |
| y2.textContent = "20" + m.month.slice(2, 4); svg.append(y2); | |
| } | |
| const hit = el("rect", { x: L + i * band, y: T, width: band, height: ph, fill: "transparent" }); | |
| hit.addEventListener("pointermove", e => showTip(e, "<b>" + fmtM(m.month) + | |
| (part ? " (8 days)" : "") + "</b><div class='r'><span>Joined</span><i>+" + m.joined + | |
| "</i></div><div class='r'><span>Left</span><i>−" + m.left + | |
| "</i></div><div class='r'><span>Net</span><i>+" + net + | |
| "</i></div><div class='r'><span>Roster</span><i>" + m.members + "</i></div>")); | |
| hit.addEventListener("pointerleave", hideTip); svg.append(hit); | |
| }); | |
| })(); | |
| /* --- 3. organised sessions per month --- */ | |
| (function () { | |
| const svg = document.getElementById("c3"); | |
| const W = 900, H = 260, L = 42, R = 16, T = 22, B = 40; | |
| const pw = W - L - R, ph = H - T - B, max = 50; | |
| const Y = v => T + ph - v / max * ph; | |
| const band = pw / POLLS.length, bw = Math.min(24, band - 8); | |
| for (let v = 0; v <= max; v += 10) { | |
| svg.append(el("line", { x1: L, x2: L + pw, y1: Y(v), y2: Y(v), | |
| stroke: "var(--grid)", "stroke-width": 1 })); | |
| const t = el("text", { x: L - 8, y: Y(v) + 4, "text-anchor": "end", class: "tick" }); | |
| t.textContent = v; svg.append(t); | |
| } | |
| svg.append(el("line", { x1: L, x2: L + pw, y1: Y(0), y2: Y(0), | |
| stroke: "var(--axis)", "stroke-width": 1 })); | |
| POLLS.forEach((p, i) => { | |
| const x = L + i * band + (band - bw) / 2, part = i === PARTIAL; | |
| const d = bar(x, bw, Y(0), Y(p.n), 4); | |
| if (d) svg.append(el("path", { d, fill: part ? "none" : "var(--series)", | |
| stroke: part ? "var(--series)" : "none", "stroke-width": part ? 1.5 : 0, | |
| "stroke-dasharray": part ? "3 2" : "" })); | |
| const t = el("text", { x: x + bw / 2, y: H - 18, "text-anchor": "middle", class: "tick" }); | |
| t.textContent = fmtM(p.month).split(" ")[0]; | |
| t.setAttribute("opacity", part ? .55 : 1); svg.append(t); | |
| if (i === 0 || p.month.endsWith("-01")) { | |
| const y2 = el("text", { x: x + bw / 2, y: H - 5, "text-anchor": "middle", class: "tick" }); | |
| y2.textContent = "20" + p.month.slice(2, 4); svg.append(y2); | |
| } | |
| if (p.n === 46) { | |
| const v = el("text", { x: x + bw / 2, y: Y(p.n) - 9, "text-anchor": "middle", class: "dlabel" }); | |
| v.textContent = "46"; svg.append(v); | |
| } | |
| const hit = el("rect", { x: L + i * band, y: T, width: band, height: ph, fill: "transparent" }); | |
| hit.addEventListener("pointermove", e => showTip(e, "<b>" + fmtM(p.month) + | |
| (part ? " (8 days)" : "") + "</b><div class='r'><span>Sessions</span><i>" + p.n + | |
| "</i></div><div class='r'><span>Members</span><i>" + M[i].members + | |
| "</i></div><div class='r'><span>Per 100 members</span><i>" + | |
| (100 * p.n / M[i].members).toFixed(1) + "</i></div>")); | |
| hit.addEventListener("pointerleave", hideTip); svg.append(hit); | |
| }); | |
| })(); | |
| /* --- 4. session slots by start hour: sign-ups (bars) + slot count (line) --- */ | |
| (function () { | |
| const svg = document.getElementById("c4"); | |
| const W = 900, H = 290, L = 42, R = 16, T = 40, B = 44; | |
| const pw = W - L - R, ph = H - T - B, max = 180, step = 30; | |
| const Y = v => T + ph - v / max * ph; | |
| const X = h => L + (h + 0.5) * pw / 24; | |
| const total = RUNNING.reduce((a, b) => a + b, 0); | |
| svg.append(el("rect", { x: L + 10 * pw / 24, y: T, width: 7 * pw / 24, height: ph, fill: "var(--band)" })); | |
| const bandN = RUNNING.slice(10, 17).reduce((a, b) => a + b, 0); | |
| const bl = el("text", { x: L + 13.5 * pw / 24, y: T - 20, "text-anchor": "middle", class: "anno" }); | |
| bl.textContent = "under way 10am to 4pm: " + bandN + " of " + total.toLocaleString() + " hours"; svg.append(bl); | |
| for (let v = 0; v <= max; v += step) { | |
| svg.append(el("line", { x1: L, x2: L + pw, y1: Y(v), y2: Y(v), | |
| stroke: "var(--grid)", "stroke-width": 1 })); | |
| const t = el("text", { x: L - 8, y: Y(v) + 4, "text-anchor": "end", class: "tick" }); | |
| t.textContent = v; svg.append(t); | |
| } | |
| svg.append(el("line", { x1: L, x2: L + pw, y1: Y(0), y2: Y(0), | |
| stroke: "var(--axis)", "stroke-width": 1 })); | |
| const pts = RUNNING.map((n, h) => X(h) + "," + Y(n)).join(" "); | |
| svg.append(el("path", { d: "M" + X(0) + "," + Y(0) + " L" + pts.replace(/ /g, " L") + | |
| " L" + X(23) + "," + Y(0) + " Z", fill: "var(--wash)" })); | |
| svg.append(el("polyline", { points: pts, fill: "none", stroke: "var(--series)", | |
| "stroke-width": 2.2, "stroke-linejoin": "round" })); | |
| RUNNING.forEach((n, h) => { | |
| if (h % 3 === 0) { | |
| const t = el("text", { x: X(h), y: H - 22, "text-anchor": "middle", class: "tick" }); | |
| t.textContent = hLab(h); svg.append(t); | |
| } | |
| const bw = pw / 24; | |
| const hit = el("rect", { x: L + h * bw, y: T, width: bw, height: ph, fill: "transparent" }); | |
| hit.addEventListener("pointermove", e => showTip(e, "<b>" + hLab(h) + "</b>" + | |
| "<div class='r'><span>Sessions under way</span><i>" + n + "</i></div>" + | |
| "<div class='r'><span>Share of the day’s court hours</span><i>" + | |
| (100 * n / total).toFixed(1) + "%</i></div>")); | |
| hit.addEventListener("pointerleave", hideTip); svg.append(hit); | |
| }); | |
| [6, 19].forEach(h => { | |
| const t = el("text", { x: X(h), y: Y(RUNNING[h]) - 10, | |
| "text-anchor": "middle", class: "dlabel" }); | |
| t.textContent = hLab(h) + ": " + RUNNING[h]; svg.append(t); | |
| }); | |
| const ax = el("text", { x: L + pw / 2, y: H - 5, "text-anchor": "middle", class: "tick" }); | |
| ax.textContent = "hour of the day"; svg.append(ax); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |