polls-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev c06cea4889e9
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 334801de44cb0fe911d5a88b
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/Results.tsx
// Results for one poll: bars, a word cloud drawn on this device, rating averages, ranking order and answers.
// The same pieces render small on phones and large in present mode.
import { useEffect, useMemo, useState } from "react";
import { pct, rankedOptions, type Poll, type Summary } from "../functions/lib/polls";

export function ResultsView({ poll, summary, big = false }: { poll: Poll; summary: Summary; big?: boolean }) {
  const cls = `results${big ? " results-big" : ""}`;
  if (poll.type === "choice") {
    const counts = summary.counts || poll.options.map(() => 0);
    const top = Math.max(0, ...counts);
    return (
      <ul className={`${cls} bars${poll.options.length > 8 ? " many" : ""}`}>
        {poll.options.map((o, i) => (
          <li key={i} className={top > 0 && counts[i] === top ? "lead" : ""}>
            <div className="bar-line"><span className="bar-label">{o}</span><span className="bar-num">{pct(counts[i], summary.n)}%<small> · {counts[i]}</small></span></div>
            <div className="bar"><span style={{ width: `${pct(counts[i], summary.n)}%` }} /></div>
          </li>
        ))}
      </ul>
    );
  }
  if (poll.type === "rating") {
    const dist = summary.dist || Array.from({ length: poll.max }, () => 0);
    const most = Math.max(1, ...dist);
    return (
      <div className={`${cls} rating-results`}>
        <p className="avg"><strong>{summary.avg !== undefined ? summary.avg.toFixed(1) : "-"}</strong> <span>out of {poll.max}</span></p>
        <div className="columns" style={{ gridTemplateColumns: `repeat(${poll.max}, minmax(0, 1fr))` }}>
          {dist.map((c, i) => (
            <div key={i} className="column">
              <span className="column-num">{c}</span>
              <span className="column-bar"><span style={{ height: `${(c / most) * 100}%` }} /></span>
              <span className="column-key">{i + 1}</span>
            </div>
          ))}
        </div>
      </div>
    );
  }
  if (poll.type === "wordcloud") {
    const words = summary.words || [];
    if (!words.length) return <p className={`${cls} results-empty`}>No words yet.</p>;
    return <div className={cls}><WordCloud words={words} big={big} /></div>;
  }
  if (poll.type === "open") {
    const texts = summary.texts || [];
    if (!texts.length) return <p className={`${cls} results-empty`}>No answers yet.</p>;
    return <ul className={`${cls} texts`}>{texts.slice(0, big ? 24 : 50).map((t, i) => <li key={i}>{t}</li>)}</ul>;
  }
  const ranked = rankedOptions(poll, summary);
  const n = poll.options.length;
  return (
    <ol className={`${cls} ranking`}>
      {ranked.map((r, i) => (
        <li key={r.option}>
          <span className="rank-place">{i + 1}</span>
          <div className="rank-body">
            <div className="bar-line"><span className="bar-label">{r.option}</span>{summary.n > 0 && <span className="bar-num">average place {r.avg.toFixed(1)}</span>}</div>
            <div className="bar"><span style={{ width: summary.n ? `${((n + 1 - r.avg) / n) * 100}%` : "0%" }} /></div>
          </div>
        </li>
      ))}
    </ol>
  );
}

const CLOUD_COLORS = ["#0b0b0c", "#2e5ec4", "#4f7a5c", "#a8442c", "#1f4597", "#8a6a2c"];

interface Placed { word: string; size: number; x: number; y: number; color: string }

/** Places words on an Archimedean spiral, biggest first, shrinking a word that does not fit. */
function layoutCloud(words: [string, number][], W: number, H: number): Placed[] {
  const ctx = document.createElement("canvas").getContext("2d");
  if (!ctx) return [];
  const list = words.slice(0, 80);
  const most = list[0][1];
  const least = list[list.length - 1][1];
  const maxSize = Math.min(H / 4.2, W / Math.max(4, list[0][0].length * 0.62));
  const minSize = Math.max(13, maxSize / 5);
  const boxes: { x: number; y: number; w: number; h: number }[] = [];
  const placed: Placed[] = [];
  list.forEach(([word, count], i) => {
    const t = most === least ? (list.length === 1 ? 1 : 0.55) : Math.sqrt((count - least) / (most - least));
    let size = minSize + (maxSize - minSize) * t;
    for (let shrink = 0; shrink < 5; shrink++, size *= 0.82) {
      ctx.font = `600 ${size}px "Archivo Variable", system-ui, sans-serif`;
      const w = ctx.measureText(word).width + size * 0.3;
      const h = size * 1.05;
      if (w > W) continue;
      for (let a = 0; a < 900; a += 0.32) {
        const r = 3.2 * a;
        const x = W / 2 + r * Math.cos(a) * 1.5 - w / 2;
        const y = H / 2 + r * Math.sin(a) - h / 2;
        if (x < 0 || y < 0 || x + w > W || y + h > H) { if (r > W) break; continue; }
        if (boxes.some((b) => x < b.x + b.w && x + w > b.x && y < b.y + b.h && y + h > b.y)) continue;
        boxes.push({ x, y, w, h });
        placed.push({ word, size, x: x + w / 2, y: y + h / 2, color: CLOUD_COLORS[i === 0 ? 0 : 1 + (i % (CLOUD_COLORS.length - 1))] });
        return;
      }
    }
  });
  return placed;
}

export function WordCloud({ words, big }: { words: [string, number][]; big?: boolean }) {
  const W = big ? 1000 : 600;
  const H = big ? 520 : 360;
  // Measure again once the app's font has loaded, so words do not overlap.
  const [fontsReady, setFontsReady] = useState(false);
  useEffect(() => { document.fonts?.ready.then(() => setFontsReady(true)); }, []);
  const key = JSON.stringify(words.slice(0, 80));
  const placed = useMemo(() => layoutCloud(words, W, H), [key, W, H, fontsReady]); // eslint-disable-line react-hooks/exhaustive-deps
  return (
    <svg className="cloud" viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`Word cloud: ${words.slice(0, 10).map(([w, c]) => `${w} (${c})`).join(", ")}`}>
      {placed.map((p) => (
        <text key={p.word} x={p.x} y={p.y} fontSize={p.size} fill={p.color} textAnchor="middle" dominantBaseline="central">{p.word}</text>
      ))}
    </svg>
  );
}

Change history

Point OpenApps links to openapps.free
c06cea4889 · 17 hours ago
Bind agent address in policy constants
a7de116d52 · 19 hours ago
Use OpenApps service URLs and add contribution strip
e8ce0b0bf0 · 19 hours ago
Bounded source sync
dcdc4d56e6 · 1 day ago
Bounded source sync
7acdace1e1 · 1 day ago
Bounded source sync
2140a66ea0 · 1 day ago
Bounded source sync
0db2fe3ca0 · 1 day ago
Bounded source sync
996f633a3c · 1 day ago