Zum Inhalt springen
UI Kit
Accent

Regionenkarte

Werte je Region auf einer gezeichneten Karte der erfundenen Insel Fernland, als getönte Regionen mit Rangliste, Veränderung in zwei Tönen, gleich große Sechsecke oder Kreise nach Größe. Für eine echte Karte eigene SVG-Formen übergeben.

Eine von 6 Components in KPI-, Live- und Karten-Charts

4 Variants

Variants

Getönte Regionen

Fünf Akzentstufen auf gleichen Klassen, eine Rangliste neben der Karte. Ein Zeiger auf einer Seite hebt dieselbe Region hervor.

Im Studio öffnen

Veränderung

Plus und Minus in zwei Tönen mit drei Stärken, ein graues Mittelfeld für etwa gleich, Vorzeichen und Pfeil in der Liste.

Im Studio öffnen

Sechsecke

Jede Region als ein Sechseck gleicher Größe, so zählt ein kleiner Bezirk so viel wie ein großer. Name und Wert stehen darin.

Im Studio öffnen

Kreise nach Größe

Kreise, deren Fläche der Wert ist, über einer ruhigen Karte, die größten zuerst gezeichnet, ein Schlüssel für die Größe.

Im Studio öffnen

Details

Was Regionenkarte mitbringt, aus Registry und Quellcode gelesen.

Bereich
Elements
Variants
4
Version
1.0.0 · seit 1.1.0
Datei
components/ui/region-map.tsx
Bringt mit
utilskiticonchartchart-figure
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 7 eigene Texte

Props

Die Schnittstelle RegionMapProps aus components/ui/region-map.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von Omit<ChartA11y, 'label'>, ChartFigureProps an.

  • labeloptional
    string

    Accessible name of the chart. Default: the title.

  • variantoptional
    RegionVariant

    regionsdiverginghex-tilessymbols

  • regionsPflicht
    RegionValue[]
  • shapesoptional
    Record<string, string>

    Own map: SVG path per id in a 400 by 300 box

  • centersoptional
    Record<string, [number, number]>

    Own map: label point per id

  • tilesoptional
    Record<string, [number, number]>

    hex-tiles: column and row of each id. Default: a layout for the built-in map.

  • formatoptional
    Intl.NumberFormatOptions
  • unitLabeloptional
    string

    Name of the value, e.g. "Members per 1,000"

Eingebaute Texte

Diese Texte schreibt Regionenkarte selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 7 Texte anzeigen
regionEnglisch RegionDeutsch Region
valueEnglisch ValueDeutsch Wert
changeEnglisch ChangeDeutsch Veränderung
gainEnglisch GainDeutsch Plus
lossEnglisch LossDeutsch Minus
flatEnglisch About the sameDeutsch Etwa gleich
sizeKeyEnglisch Circle area shows the valueDeutsch Die Kreisfläche zeigt den Wert

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Regionenkarte reagiert selbst auf die Tasten Escape, Pfeil hoch, Pfeil runter, Pfeil links und Pfeil rechts.
  • Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.

Bewegung

  • Ohne Animationen, sie steht sofort ruhig da.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 342 Zeilen
components/ui/region-map.tsx
'use client';

import { useState } from 'react';
import { cn, round, useTexts } from '@/lib/kit';
import { Icon } from '@/components/ui/icon';
import { ChartRoot, ChartSurface, ChartTable, numberFormatter, rampColor, textWidth, useEntrance, useIntlLocale, useWidth, type ChartA11y } from '@/components/ui/chart';
import { ChartFigure, HALO, type ChartFigureProps } from '@/components/ui/chart-figure';

/*
 * Values per region on a drawn map.
 *   regions   tinted regions in five accent steps, a ranked list beside
 *   diverging change per region, gains and losses in two tones, sign in the label
 *   hex-tiles every region as one hexagon of the same size, fair to small regions
 *   symbols   circles sized by the value over a quiet map
 *
 * The built-in map is invented: the island Fernland with nine districts,
 * drawn here as a Voronoi split of an outline, no real geography. Pass your
 * own `shapes` (SVG paths in a 400 by 300 box) and `centers` for a real map.
 */

export type RegionVariant = 'regions' | 'diverging' | 'hex-tiles' | 'symbols';

export interface RegionValue {
  /** Id of the shape, r1 to r9 for the built-in map */
  id: string;
  label: string;
  value: number;
  /** diverging: change as a fraction, 0.08 is +8 % */
  change?: number;
}

export interface RegionMapProps extends Omit<ChartA11y, 'label'>, ChartFigureProps {
  /** Accessible name of the chart. Default: the title. */
  label?: string;
  variant?: RegionVariant;
  regions: RegionValue[];
  /** Own map: SVG path per id in a 400 by 300 box */
  shapes?: Record<string, string>;
  /** Own map: label point per id */
  centers?: Record<string, [number, number]>;
  /** hex-tiles: column and row of each id. Default: a layout for the built-in map. */
  tiles?: Record<string, [number, number]>;
  format?: Intl.NumberFormatOptions;
  /** Name of the value, e.g. "Members per 1,000" */
  unitLabel?: string;
  className?: string;
}

const TEXTS = {
  en: { region: 'Region', value: 'Value', change: 'Change', gain: 'Gain', loss: 'Loss', flat: 'About the same', sizeKey: 'Circle area shows the value' },
  de: { region: 'Region', value: 'Wert', change: 'Veränderung', gain: 'Plus', loss: 'Minus', flat: 'Etwa gleich', sizeKey: 'Die Kreisfläche zeigt den Wert' },
};

/* ---------------- the invented map: Fernland ---------------- */

type Pt = [number, number];
const ISLAND: Pt[] = Array.from({ length: 56 }, (_, i) => {
  const a = (i / 56) * Math.PI * 2;
  const r = 1 + 0.11 * Math.sin(3 * a + 0.4) + 0.07 * Math.cos(5 * a + 1.2) + 0.04 * Math.sin(9 * a);
  return [200 + Math.cos(a) * 176 * r, 150 + Math.sin(a) * 118 * r] as Pt;
});
const SEEDS: Record<string, Pt> = { r1: [92, 110], r2: [170, 72], r3: [262, 70], r4: [330, 128], r5: [126, 188], r6: [206, 150], r7: [292, 206], r8: [196, 236], r9: [62, 170] };

/** Sutherland-Hodgman: keep the part of a polygon on the side of the bisector closer to a */
function clip(poly: Pt[], a: Pt, b: Pt): Pt[] {
  const mx = (a[0] + b[0]) / 2;
  const my = (a[1] + b[1]) / 2;
  const nx = b[0] - a[0];
  const ny = b[1] - a[1];
  const inside = (p: Pt) => (p[0] - mx) * nx + (p[1] - my) * ny <= 0;
  const out: Pt[] = [];
  for (let i = 0; i < poly.length; i++) {
    const p = poly[i];
    const q = poly[(i + 1) % poly.length];
    const pi = inside(p);
    const qi = inside(q);
    if (pi) out.push(p);
    if (pi !== qi) {
      const dp = (p[0] - mx) * nx + (p[1] - my) * ny;
      const dq = (q[0] - mx) * nx + (q[1] - my) * ny;
      const t = dp / (dp - dq);
      out.push([p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t]);
    }
  }
  return out;
}
const toPath = (poly: Pt[]) => poly.map(([x, y], i) => `${i ? 'L' : 'M'}${round(x, 1)} ${round(y, 1)}`).join('') + 'Z';
const centroid = (poly: Pt[]): Pt => {
  let a = 0;
  let cx = 0;
  let cy = 0;
  for (let i = 0; i < poly.length; i++) {
    const [x1, y1] = poly[i];
    const [x2, y2] = poly[(i + 1) % poly.length];
    const f = x1 * y2 - x2 * y1;
    a += f;
    cx += (x1 + x2) * f;
    cy += (y1 + y2) * f;
  }
  return a ? [round(cx / (3 * a), 1), round(cy / (3 * a), 1)] : poly[0];
};
const FERNLAND = (() => {
  const shapes: Record<string, string> = {};
  const centers: Record<string, Pt> = {};
  for (const [id, s] of Object.entries(SEEDS)) {
    let poly = ISLAND;
    for (const [other, o] of Object.entries(SEEDS)) if (other !== id) poly = clip(poly, s, o);
    shapes[id] = toPath(poly);
    centers[id] = centroid(poly);
  }
  return { shapes, centers, outline: toPath(ISLAND) };
})();
/** Hex tile layout of Fernland (column, row), odd rows shifted half a tile */
const TILES: Record<string, Pt> = { r9: [0, 1], r1: [1, 0], r2: [2, 0], r3: [3, 0], r4: [4, 1], r5: [1, 1], r6: [2, 1], r7: [3, 1], r8: [2, 2] };

const hexPath = (cx: number, cy: number, r: number) =>
  Array.from({ length: 6 }, (_, i) => {
    const a = (Math.PI / 3) * i - Math.PI / 6;
    return `${i ? 'L' : 'M'}${round(cx + r * Math.cos(a), 1)} ${round(cy + r * Math.sin(a), 1)}`;
  }).join('') + 'Z';

/**
 * Values on a map: tinted regions, change in two tones, equal hex tiles or sized circles.
 *   <RegionMap variant="regions" regions={[{ id: 'r1', label: 'Nordheide', value: 42 }]} unitLabel="Members per 1,000" />
 */
export function RegionMap(props: RegionMapProps) {
  const { variant = 'regions', regions, format, unitLabel = '', label, summary, labels, animate = true, title, value, delta, meta, source, framed, className } = props;
  const t = useTexts(TEXTS, undefined);
  const intl = useIntlLocale();
  const fmt = numberFormatter(intl, format);
  const pct = numberFormatter(intl, { style: 'percent', maximumFractionDigits: 1 });
  const signed = (n: number) => (n > 0 ? '+' : n < 0 ? '−' : '') + pct(Math.abs(n));
  const [wrapRef, width] = useWidth<HTMLDivElement>(720);
  const [rootRef, anim] = useEntrance<HTMLDivElement>(animate);
  const [active, setActive] = useState<number | null>(null);
  const shapes = props.shapes ?? FERNLAND.shapes;
  const centers = props.centers ?? FERNLAND.centers;
  const tiles = props.tiles ?? TILES;
  const name = label ?? title ?? unitLabel;
  const beside = width >= 720;
  // Map text in view box units so it renders at about 11 px whatever the map width
  const mapPx = beside ? ((width - 24) * 1.5) / 2.5 : width;
  const unit = 400 / Math.max(1, mapPx);
  const small = mapPx < 480;
  const d = (ms: number) => (anim ? ({ '--kitch-d': `${round(ms, 0)}ms` } as React.CSSProperties) : undefined);

  const vals = regions.map((r) => r.value);
  const lo = Math.min(...vals);
  const hi = Math.max(...vals);
  // Five equal bins between the lowest and the highest value
  const binOf = (v: number) => Math.min(4, Math.floor(((v - lo) / (hi - lo || 1)) * 5));
  const edges = Array.from({ length: 6 }, (_, i) => lo + ((hi - lo) * i) / 5);
  const toneOf = (r: RegionValue) => {
    if (variant === 'diverging') {
      const c = r.change ?? 0;
      if (Math.abs(c) < 0.02) return 'color-mix(in oklab, var(--muted-foreground) 22%, var(--card))';
      const k = Math.abs(c) < 0.06 ? 26 : Math.abs(c) < 0.12 ? 46 : 70;
      return `color-mix(in oklab, ${c > 0 ? 'var(--kitfg-up)' : 'var(--kitfg-down)'} ${k}%, var(--card))`;
    }
    if (variant === 'symbols') return 'color-mix(in oklab, var(--foreground) 6%, var(--card))';
    return rampColor(binOf(r.value));
  };
  // Text on a tinted region: dark text on light tints, light text on the two darkest accent steps
  // Measured per mode: light keeps dark text up to step 3, dark mode needs dark text from step 3
  const inkOf = (r: RegionValue) => {
    if (variant !== 'regions' && variant !== 'hex-tiles') return 'var(--foreground)';
    const b = binOf(r.value);
    return b === 4 ? 'var(--primary-foreground)' : b === 3 ? 'var(--rm-ink3)' : 'var(--foreground)';
  };

  const order = [...regions.keys()].sort((a, b) => (variant === 'diverging' ? (regions[b].change ?? 0) - (regions[a].change ?? 0) : regions[b].value - regions[a].value));
  const onKeyDown = (e: React.KeyboardEvent) => {
    const at = active === null ? -1 : order.indexOf(active);
    let next = at;
    if (e.key === 'ArrowDown' || e.key === 'ArrowRight') next = Math.min(order.length - 1, at + 1);
    else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') next = Math.max(0, at - 1);
    else if (e.key === 'Escape') return setActive(null);
    else return;
    e.preventDefault();
    setActive(order[next]);
  };
  const a = active !== null ? regions[active] : null;
  const live = a ? `${a.label}. ${fmt(a.value)}${a.change !== undefined && variant === 'diverging' ? `, ${t.change} ${signed(a.change)}` : ''}` : '';
  const table = <ChartTable caption={name} head={[t.region, unitLabel || t.value, ...(variant === 'diverging' ? [t.change] : [])]} rows={order.map((i) => [regions[i].label, fmt(regions[i].value), ...(variant === 'diverging' ? [signed(regions[i].change ?? 0)] : [])])} />;
  const handlers = (i: number) => ({
    onPointerEnter: (e: React.PointerEvent) => e.pointerType === 'mouse' && setActive(i),
    onPointerLeave: (e: React.PointerEvent) => e.pointerType === 'mouse' && setActive(null),
    onPointerDown: () => setActive(active === i ? null : i),
  });

  const mapW = 400;
  const mapH = 300;
  const sMax = Math.max(...vals, 1);
  const rMax = 30;

  const map = (
    <svg width="100%" viewBox={`0 0 ${mapW} ${mapH}`} aria-hidden className="block overflow-visible">
      {variant === 'hex-tiles' ? (
        (() => {
          const r = 38;
          const w = Math.sqrt(3) * r;
          const cols = Math.max(...Object.values(tiles).map((p) => p[0])) + 1.5;
          const rowsN = Math.max(...Object.values(tiles).map((p) => p[1])) + 1;
          const ox = (mapW - cols * w) / 2 + w / 2;
          const oy = (mapH - (rowsN - 1) * r * 1.5) / 2;
          return regions.map((reg, i) => {
            const p = tiles[reg.id];
            if (!p) return null;
            const x = ox + p[0] * w + (p[1] % 2 ? w / 2 : 0);
            const y = oy + p[1] * r * 1.5;
            const short = reg.label.length > 9 ? `${reg.label.slice(0, 8)}.` : reg.label;
            return (
              <g key={reg.id} {...handlers(i)} className={cn('cursor-default', anim && 'kitch-pop')} style={{ ...d(80 + i * 60), transformOrigin: `${x}px ${y}px`, transformBox: 'view-box' }}>
                <path d={hexPath(x, y, r - 2)} fill={toneOf(reg)} stroke={active === i ? 'var(--foreground)' : 'var(--kitch-surface)'} strokeWidth={active === i ? 2.5 : 2} strokeLinejoin="round" className="transition-[stroke] duration-200" />
                {!small && <text x={x} y={y - 6} textAnchor="middle" className="text-[11px] font-medium" fill={inkOf(reg)}>{short}</text>}
                <text x={x} y={small ? y : y + 11} dy={small ? '0.34em' : undefined} textAnchor="middle" className="font-semibold tabular-nums" fill={inkOf(reg)} style={{ fontSize: small ? round(13 * unit, 1) : 13 }}>{fmt(reg.value)}</text>
              </g>
            );
          });
        })()
      ) : (
        <>
          <path d={FERNLAND.outline} fill="none" stroke="var(--kitch-axis)" strokeWidth="1" transform="translate(0 4)" opacity="0.5" />
          {regions.map((reg, i) => (
            <path key={reg.id} {...handlers(i)} d={shapes[reg.id]} fill={toneOf(reg)} stroke={active === i ? 'var(--foreground)' : 'var(--kitch-surface)'} strokeWidth={active === i ? 2.5 : 2} strokeLinejoin="round" className={cn('cursor-default transition-[stroke,opacity] duration-200', anim && 'kitch-fade')} style={d(80 + i * 70)} opacity={active !== null && active !== i ? 0.75 : 1} />
          ))}
          {variant === 'symbols' &&
            [...regions.keys()]
              .sort((x, y) => regions[y].value - regions[x].value)
              .map((i) => {
                const reg = regions[i];
                const c = centers[reg.id];
                if (!c) return null;
                const r = round(Math.sqrt(reg.value / sMax) * rMax);
                return (
                  <circle key={reg.id} {...handlers(i)} cx={c[0]} cy={c[1]} r={Math.max(3, r)} fill="var(--primary)" fillOpacity={active === i ? 0.9 : 0.7} stroke="var(--kitch-surface)" strokeWidth="2" className={cn('cursor-default transition-[fill-opacity] duration-200', anim && 'kitch-pop')} style={{ ...d(400 + i * 80), transformOrigin: `${c[0]}px ${c[1]}px`, transformBox: 'view-box' }} />
                );
              })}
          {regions.map((reg, i) => {
            const c = centers[reg.id];
            if (!c) return null;
            if (small && active !== i) return null;
            const r = variant === 'symbols' ? Math.max(3, round(Math.sqrt(reg.value / sMax) * rMax)) : 0;
            const y = variant === 'symbols' ? c[1] + r + 12 : c[1];
            return (
              <text key={reg.id} x={c[0]} y={y} dy="0.34em" textAnchor="middle" className="pointer-events-none font-medium" fill={small || variant === 'symbols' ? 'var(--foreground)' : inkOf(reg)} style={{ fontSize: round(Math.max(10, 11 * unit), 1), ...(small || variant === 'symbols' ? HALO : {}) }}>
                {!small && textWidth(reg.label, 10) > 70 ? `${reg.label.slice(0, 9)}.` : reg.label}
              </text>
            );
          })}
        </>
      )}
    </svg>
  );

  const legend =
    variant === 'diverging' ? (
      <ul className="flex flex-wrap items-center gap-x-4 gap-y-1.5 text-[12px] text-muted-foreground">
        {[
          { k: t.loss, c: 'var(--kitfg-down)', icon: 'arrow-down-right' as const },
          { k: t.flat, c: 'var(--muted-foreground)', icon: null },
          { k: t.gain, c: 'var(--kitfg-up)', icon: 'arrow-up-right' as const },
        ].map((x) => (
          <li key={x.k} className="flex items-center gap-1.5">
            <span aria-hidden className="flex h-3 overflow-hidden rounded-[3px]">
              {(x.icon ? [70, 46, 26] : [22]).map((k) => (
                <span key={k} className="w-2.5" style={{ background: `color-mix(in oklab, ${x.c} ${k}%, var(--card))` }} />
              ))}
            </span>
            {x.icon && <Icon name={x.icon} size={13} strokeWidth={2} />}
            {x.k}
          </li>
        ))}
      </ul>
    ) : variant === 'symbols' ? (
      <p className="flex items-center gap-2 text-[12px] text-muted-foreground">
        <svg width="34" height="22" viewBox="0 0 34 22" aria-hidden>
          <circle cx="11" cy="11" r="10" fill="var(--primary)" fillOpacity="0.7" />
          <circle cx="27" cy="15" r="6" fill="var(--primary)" fillOpacity="0.7" />
        </svg>
        {t.sizeKey}
      </p>
    ) : (
      <div className="text-[12px] text-muted-foreground">
        <div className="flex items-center gap-1">
          {[0, 1, 2, 3, 4].map((s) => (
            <span key={s} aria-hidden className="h-2.5 flex-1 rounded-[2px] first:rounded-l-[4px] last:rounded-r-[4px]" style={{ background: rampColor(s), minWidth: 28 }} />
          ))}
        </div>
        <div className="mt-1 flex justify-between tabular-nums">
          <span>{fmt(edges[0])}</span>
          <span>{fmt(edges[5])}</span>
        </div>
      </div>
    );

  return (
    <ChartFigure title={title} value={value} delta={delta} meta={meta} source={source} framed={framed} className={className}>
      <ChartRoot anim={anim} rootRef={rootRef} className="[--rm-ink3:var(--foreground)] dark:[--rm-ink3:var(--primary-foreground)]">
        <div ref={wrapRef} className={cn('grid gap-6', beside ? 'grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)] items-start' : 'grid-cols-1')}>
          <div>
            <ChartSurface label={name} summary={summary ?? order.slice(0, 3).map((i) => `${regions[i].label} ${fmt(regions[i].value)}`).join(', ')} labels={labels} live={live} table={table} onKeyDown={onKeyDown} onFocus={(e) => e.target === e.currentTarget && active === null && e.currentTarget.matches(':focus-visible') && setActive(order[0])} onBlur={() => setActive(null)}>
              {map}
            </ChartSurface>
            <div className="mt-4 max-w-xs">{legend}</div>
          </div>
          <ol className="grid content-start gap-0.5">
            {order.map((i, k) => {
              const reg = regions[i];
              return (
                <li key={reg.id}>
                  <button
                    type="button"
                    onPointerEnter={(e) => e.pointerType === 'mouse' && setActive(i)}
                    onPointerLeave={(e) => e.pointerType === 'mouse' && setActive(null)}
                    onFocus={() => setActive(i)}
                    onBlur={() => setActive(null)}
                    onClick={() => setActive(active === i ? null : i)}
                    className={cn('flex min-h-11 w-full items-center gap-3 rounded-md px-2.5 text-left text-[13px] transition-colors duration-200 sm:min-h-10', active === i ? 'bg-accent' : 'hover:bg-accent/60', anim && 'kitch-rise')}
                    style={d(200 + k * 50)}
                  >
                    <span className="w-4 text-right text-[12px] tabular-nums text-muted-foreground">{k + 1}</span>
                    <span aria-hidden className="size-3 shrink-0 rounded-[3px]" style={{ background: variant === 'symbols' ? 'var(--primary)' : toneOf(reg) }} />
                    <span className="min-w-0 flex-1 truncate text-foreground">{reg.label}</span>
                    {variant === 'diverging' ? (
                      <span className={cn('inline-flex items-center gap-0.5 font-medium tabular-nums', (reg.change ?? 0) > 0.02 ? 'text-success-foreground' : (reg.change ?? 0) < -0.02 ? 'text-destructive' : 'text-muted-foreground')}>
                        {Math.abs(reg.change ?? 0) >= 0.02 && <Icon name={(reg.change ?? 0) > 0 ? 'arrow-up-right' : 'arrow-down-right'} size={13} strokeWidth={2} />}
                        {signed(reg.change ?? 0)}
                      </span>
                    ) : (
                      <span className="font-medium tabular-nums text-foreground">{fmt(reg.value)}</span>
                    )}
                  </button>
                </li>
              );
            })}
          </ol>
        </div>
      </ChartRoot>
    </ChartFigure>
  );
}

Häufige Fragen

Wie installiere ich Regionenkarte?

Mit aiw add @ai-website/region-map. Der Befehl legt die Datei components/ui/region-map.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon, chart und chart-figure. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Regionenkarte?

4 Varianten, Getönte Regionen, Veränderung, Sechsecke und Kreise nach Größe. Du wählst sie mit der Prop variant, zum Beispiel variant="diverging".

Kann ich die Texte von Regionenkarte übersetzen?

Ja. Regionenkarte bringt 7 eigene Texte auf Deutsch und Englisch mit, etwa „Region“, „Wert“ und „Veränderung“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent Regionenkarte einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Regionenkarte mit aiw_components und installieren sie mit aiw_add @ai-website/region-map. Nenn im Prompt die Variante, etwa variant="regions", dann schreibt der Assistent deine Inhalte direkt in die Props.

Kann ich Regionenkarte ohne Code anpassen?

Ja, im Studio. Alle 4 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden RegionMap-Code zum Kopieren.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert