Spaces:
Running
Running
thibaud frere
commited on
Commit
·
2da6ea7
1
Parent(s):
8d0d788
update charts and colors
Browse files- README.md +39 -0
- app/scripts/find_duplicated_spaces.py +196 -0
- app/src/components/ColorPicker.astro +98 -0
- app/src/components/HtmlEmbed.astro +1 -5
- app/src/components/Palettes.astro +169 -0
- app/src/content/chapters/introduction.mdx +2 -1
- app/src/content/chapters/vibe-coding-charts.mdx +2 -2
- app/src/content/chapters/writing-your-content.mdx +4 -2
- app/src/content/embeds/d3-bar.html +0 -11
- app/src/content/embeds/d3-benchmark.html +1 -11
- app/src/content/embeds/d3-line-quad.html +35 -12
- app/src/content/embeds/d3-line.html +0 -11
- app/src/content/embeds/d3-neural-network.html +1 -1
- app/src/content/embeds/d3-pie.html +5 -1
- app/src/content/embeds/d3-scatter.html +9 -5
- app/src/content/embeds/vibe-code-d3-embeds-directives.md +16 -18
- app/src/scripts/color-palettes.js +1 -0
README.md
CHANGED
|
@@ -9,3 +9,42 @@ header: mini
|
|
| 9 |
app_port: 8080
|
| 10 |
thumbnail: https://huggingface.co/spaces/tfrere/research-paper-template/thumb.jpg
|
| 11 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
app_port: 8080
|
| 10 |
thumbnail: https://huggingface.co/spaces/tfrere/research-paper-template/thumb.jpg
|
| 11 |
---
|
| 12 |
+
|
| 13 |
+
## Find recent duplicated Spaces
|
| 14 |
+
|
| 15 |
+
This repository includes a small utility to list public Spaces created in the last N days that were duplicated from a given source Space.
|
| 16 |
+
|
| 17 |
+
Prerequisites:
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
pip install huggingface_hub requests
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
Usage:
|
| 24 |
+
|
| 25 |
+
```bash
|
| 26 |
+
python app/scripts/find_duplicated_spaces.py --source owner/space-name --days 14
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
Options:
|
| 30 |
+
|
| 31 |
+
- `--source`: required. The source Space in the form `owner/space-name`.
|
| 32 |
+
- `--days`: optional. Time window in days (default: 14).
|
| 33 |
+
- `--token`: optional. Your HF token. Defaults to `HF_TOKEN` env var if set.
|
| 34 |
+
- `--no-deep`: optional. Disable README/frontmatter fallback detection.
|
| 35 |
+
|
| 36 |
+
Examples:
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
# Using env var for the token (optional)
|
| 40 |
+
export HF_TOKEN=hf_xxx
|
| 41 |
+
|
| 42 |
+
# Find Spaces duplicated from tfrere/my-space in the last 14 days
|
| 43 |
+
python app/scripts/find_duplicated_spaces.py --source tfrere/my-space
|
| 44 |
+
|
| 45 |
+
# Use a 7-day window and explicit token
|
| 46 |
+
python app/scripts/find_duplicated_spaces.py --source tfrere/my-space --days 7 --token $HF_TOKEN
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
The script first checks card metadata (e.g., `duplicated_from`) and optionally falls back to parsing the README frontmatter for robustness.
|
| 50 |
+
|
app/scripts/find_duplicated_spaces.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Find Spaces created in the last N days that were duplicated from a given source Space.
|
| 4 |
+
|
| 5 |
+
This script uses the public Hugging Face Hub APIs via `huggingface_hub` and optionally
|
| 6 |
+
falls back to reading README frontmatter for robustness.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
python app/scripts/find_duplicated_spaces.py --source owner/space-name [--days 14] [--token <hf_token>] [--no-deep]
|
| 10 |
+
|
| 11 |
+
Notes:
|
| 12 |
+
- Comments are in English as requested.
|
| 13 |
+
- Chat responses remain in French.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import argparse
|
| 19 |
+
import os
|
| 20 |
+
from datetime import datetime, timedelta, timezone
|
| 21 |
+
from typing import Iterable, List, Optional
|
| 22 |
+
|
| 23 |
+
import requests
|
| 24 |
+
from huggingface_hub import HfApi
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def parse_args() -> argparse.Namespace:
|
| 28 |
+
parser = argparse.ArgumentParser(
|
| 29 |
+
description="List recent Spaces duplicated from a given Space"
|
| 30 |
+
)
|
| 31 |
+
parser.add_argument(
|
| 32 |
+
"--source",
|
| 33 |
+
required=True,
|
| 34 |
+
help="Source Space in the form 'owner/space-name'",
|
| 35 |
+
)
|
| 36 |
+
parser.add_argument(
|
| 37 |
+
"--days",
|
| 38 |
+
type=int,
|
| 39 |
+
default=14,
|
| 40 |
+
help="Time window in days (default: 14)",
|
| 41 |
+
)
|
| 42 |
+
parser.add_argument(
|
| 43 |
+
"--token",
|
| 44 |
+
default=os.environ.get("HF_TOKEN"),
|
| 45 |
+
help="Hugging Face token (optional). Defaults to HF_TOKEN env var if set.",
|
| 46 |
+
)
|
| 47 |
+
parser.add_argument(
|
| 48 |
+
"--no-deep",
|
| 49 |
+
action="store_true",
|
| 50 |
+
help=(
|
| 51 |
+
"Disable deep detection (README/frontmatter fetch) when card metadata is missing."
|
| 52 |
+
),
|
| 53 |
+
)
|
| 54 |
+
return parser.parse_args()
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def iso_to_datetime(value: str) -> datetime:
|
| 58 |
+
"""Parse ISO 8601 timestamps returned by the Hub to aware datetime in UTC."""
|
| 59 |
+
# Examples: "2024-09-01T12:34:56.789Z" or without microseconds
|
| 60 |
+
try:
|
| 61 |
+
dt = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ")
|
| 62 |
+
except ValueError:
|
| 63 |
+
dt = datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
|
| 64 |
+
return dt.replace(tzinfo=timezone.utc)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def readme_frontmatter_duplicated_from(space_id: str) -> Optional[str]:
|
| 68 |
+
"""Fetch README raw and try to extract duplicated_from from YAML frontmatter."""
|
| 69 |
+
# Raw README for Spaces is accessible at /spaces/{id}/raw/README.md
|
| 70 |
+
url = f"https://huggingface.co/spaces/{space_id}/raw/README.md"
|
| 71 |
+
try:
|
| 72 |
+
resp = requests.get(url, timeout=10)
|
| 73 |
+
if resp.status_code != 200:
|
| 74 |
+
return None
|
| 75 |
+
text = resp.text
|
| 76 |
+
except requests.RequestException:
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
# Very light-weight frontmatter scan to find a line like: duplicated_from: owner/space
|
| 80 |
+
# Do not parse full YAML to avoid extra deps.
|
| 81 |
+
lines = text.splitlines()
|
| 82 |
+
in_frontmatter = False
|
| 83 |
+
for line in lines:
|
| 84 |
+
if line.strip() == "---":
|
| 85 |
+
in_frontmatter = not in_frontmatter
|
| 86 |
+
# Stop if we closed the frontmatter without finding the key.
|
| 87 |
+
if not in_frontmatter:
|
| 88 |
+
break
|
| 89 |
+
continue
|
| 90 |
+
if in_frontmatter and line.strip().startswith("duplicated_from:"):
|
| 91 |
+
# Extract value after colon, trim quotes/spaces
|
| 92 |
+
value = line.split(":", 1)[1].strip().strip("'\"")
|
| 93 |
+
return value or None
|
| 94 |
+
return None
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def get_recent_spaces(api: HfApi, days: int) -> Iterable:
|
| 98 |
+
"""Yield Spaces created within the last `days` days, iterating newest first.
|
| 99 |
+
|
| 100 |
+
Tries to sort by creation date descending; falls back gracefully if not supported.
|
| 101 |
+
"""
|
| 102 |
+
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
| 103 |
+
|
| 104 |
+
# Primary attempt: request spaces sorted by creation date (newest first)
|
| 105 |
+
try:
|
| 106 |
+
spaces_iter = api.list_spaces(full=True, sort="created", direction=-1)
|
| 107 |
+
except TypeError:
|
| 108 |
+
# Fallback: no sort support in current huggingface_hub; get a reasonably large list
|
| 109 |
+
# Note: This may include items older than the cutoff; we'll filter below.
|
| 110 |
+
spaces_iter = api.list_spaces(full=True)
|
| 111 |
+
|
| 112 |
+
for space in spaces_iter:
|
| 113 |
+
created_at_raw = getattr(space, "created_at", None) or getattr(space, "createdAt", None)
|
| 114 |
+
if not created_at_raw:
|
| 115 |
+
# If missing, include conservatively
|
| 116 |
+
yield space
|
| 117 |
+
continue
|
| 118 |
+
created_at = (
|
| 119 |
+
created_at_raw if isinstance(created_at_raw, datetime) else iso_to_datetime(str(created_at_raw))
|
| 120 |
+
)
|
| 121 |
+
if created_at >= cutoff:
|
| 122 |
+
yield space
|
| 123 |
+
else:
|
| 124 |
+
# If we know the iteration is sorted by creation desc, we can break early
|
| 125 |
+
# Only do that when we explicitly asked for sort="created"
|
| 126 |
+
if "spaces_iter" in locals():
|
| 127 |
+
try:
|
| 128 |
+
# If we reached here under the sorted branch, short-circuit
|
| 129 |
+
# by checking if the generator came from the sorted call
|
| 130 |
+
_ = api # keep linter calm
|
| 131 |
+
except Exception:
|
| 132 |
+
pass
|
| 133 |
+
# We can't be certain the iterator is sorted in fallback; just continue
|
| 134 |
+
# without breaking to avoid missing any items.
|
| 135 |
+
continue
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def find_duplicated_spaces(
|
| 139 |
+
api: HfApi, source: str, days: int, deep_detection: bool
|
| 140 |
+
) -> List[str]:
|
| 141 |
+
"""Return list of Space IDs that were duplicated from `source` within `days`."""
|
| 142 |
+
source = source.strip().strip("/ ")
|
| 143 |
+
results: List[str] = []
|
| 144 |
+
for space in get_recent_spaces(api, days=days):
|
| 145 |
+
space_id = getattr(space, "id", None) or getattr(space, "repo_id", None)
|
| 146 |
+
if not space_id:
|
| 147 |
+
continue
|
| 148 |
+
|
| 149 |
+
# Check card metadata first
|
| 150 |
+
card = getattr(space, "cardData", None) or getattr(space, "card_data", None)
|
| 151 |
+
duplicated_from_value: Optional[str] = None
|
| 152 |
+
if isinstance(card, dict):
|
| 153 |
+
for key in ("duplicated_from", "duplicatedFrom", "duplicated-from"):
|
| 154 |
+
if key in card and isinstance(card[key], str):
|
| 155 |
+
duplicated_from_value = card[key].strip().strip("/ ")
|
| 156 |
+
break
|
| 157 |
+
|
| 158 |
+
# Optional deep detection via README frontmatter
|
| 159 |
+
if not duplicated_from_value and deep_detection:
|
| 160 |
+
duplicated_from_value = readme_frontmatter_duplicated_from(space_id)
|
| 161 |
+
if duplicated_from_value:
|
| 162 |
+
duplicated_from_value = duplicated_from_value.strip().strip("/ ")
|
| 163 |
+
|
| 164 |
+
if duplicated_from_value and duplicated_from_value.lower() == source.lower():
|
| 165 |
+
results.append(space_id)
|
| 166 |
+
|
| 167 |
+
return results
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def main() -> None:
|
| 171 |
+
args = parse_args()
|
| 172 |
+
api = HfApi(token=args.token)
|
| 173 |
+
|
| 174 |
+
duplicated = find_duplicated_spaces(
|
| 175 |
+
api=api,
|
| 176 |
+
source=args.source,
|
| 177 |
+
days=args.days,
|
| 178 |
+
deep_detection=not args.no_deep,
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
if duplicated:
|
| 182 |
+
print(
|
| 183 |
+
f"Found {len(duplicated)} Space(s) duplicated from {args.source} in the last {args.days} days:\n"
|
| 184 |
+
)
|
| 185 |
+
for sid in duplicated:
|
| 186 |
+
print(f"https://huggingface.co/spaces/{sid}")
|
| 187 |
+
else:
|
| 188 |
+
print(
|
| 189 |
+
f"No public Spaces duplicated from {args.source} in the last {args.days} days."
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
if __name__ == "__main__":
|
| 194 |
+
main()
|
| 195 |
+
|
| 196 |
+
|
app/src/components/ColorPicker.astro
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
---
|
| 3 |
+
<div class="color-picker" style="width:100%; margin: 10px 0;">
|
| 4 |
+
<style>
|
| 5 |
+
.color-picker .picker__stack { display:flex; flex-direction:column; gap:12px; }
|
| 6 |
+
.color-picker .current-card { display:grid; grid-template-columns: 30% 70%; align-items: center; gap:14px; padding:14px 32px 14px 16px; border:1px solid var(--border-color); background: var(--surface-bg); border-radius: 12px; }
|
| 7 |
+
.color-picker .current-left { display:flex; flex-direction: column; gap:8px; min-width: 0; }
|
| 8 |
+
.color-picker .current-right { display:flex; flex-direction: column; gap:8px; padding-left: 14px; border-left: 1px solid var(--border-color); }
|
| 9 |
+
.color-picker .current-main { display:flex; align-items:center; gap:12px; min-width: 0; }
|
| 10 |
+
.color-picker .current-swatch { width: 64px; height: 64px; border-radius: 8px; border: 1px solid var(--border-color); }
|
| 11 |
+
.color-picker .current-text { display:flex; flex-direction: column; line-height: 1.2; min-width: 0; }
|
| 12 |
+
.color-picker .current-name { font-size: 14px; font-weight: 800; color: var(--text-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: clamp(140px, 28vw, 260px); }
|
| 13 |
+
.color-picker .current-hex, .color-picker .current-extra { font-size: 11px; color: var(--muted-color); letter-spacing: .02em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: clamp(140px, 28vw, 260px); }
|
| 14 |
+
.color-picker .picker__label { font-weight:700; font-size: 12px; color: var(--muted-color); text-transform: uppercase; letter-spacing: .02em; }
|
| 15 |
+
.color-picker .hue-slider { position:relative; height:16px; border-radius:10px; border:1px solid var(--border-color); background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); cursor: ew-resize; touch-action: none; flex: 1 1 auto; min-width: 200px; }
|
| 16 |
+
.color-picker .hue-knob { position:absolute; top:50%; left:93.6%; width:14px; height:14px; border-radius:50%; border:2px solid #fff; transform:translate(-50%, -50%); background: var(--surface-bg); z-index: 2; box-shadow: 0 0 0 1px rgba(0,0,0,.05); }
|
| 17 |
+
.color-picker .hue-slider:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; }
|
| 18 |
+
.color-picker .hue-value { font-variant-numeric: tabular-nums; color: var(--muted-color); font-size: 12px; }
|
| 19 |
+
@media (max-width: 720px) { .color-picker .current-card { grid-template-columns: 1fr; } .color-picker .current-right { padding-left: 0; border-left: none; } }
|
| 20 |
+
</style>
|
| 21 |
+
<div class="picker__stack">
|
| 22 |
+
<div class="current-card">
|
| 23 |
+
<div class="current-left">
|
| 24 |
+
<div class="current-main">
|
| 25 |
+
<div class="current-swatch" aria-label="Current color" title="Current color"></div>
|
| 26 |
+
<div class="current-text">
|
| 27 |
+
<div class="current-name">—</div>
|
| 28 |
+
<div class="current-hex">—</div>
|
| 29 |
+
<div class="current-extra current-lch">—</div>
|
| 30 |
+
<div class="current-extra current-rgb">—</div>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
</div>
|
| 34 |
+
<div class="current-right">
|
| 35 |
+
<div class="picker__label">Hue</div>
|
| 36 |
+
<div class="hue-slider" role="slider" aria-label="Hue" aria-valuemin="0" aria-valuemax="360" aria-valuenow="337" tabindex="0">
|
| 37 |
+
<div class="hue-knob"></div>
|
| 38 |
+
</div>
|
| 39 |
+
<div class="hue-value">337°</div>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
<script>
|
| 45 |
+
(() => {
|
| 46 |
+
const COLOR_NAMES = [{"name":"Candy Apple Red","hex":"#ff0800"},{"name":"Boiling Magma","hex":"#ff3300"},{"name":"Aerospace Orange","hex":"#ff4f00"},{"name":"Burtuqali Orange","hex":"#ff6700"},{"name":"American Orange","hex":"#ff8b00"},{"name":"Cheese","hex":"#ffa600"},{"name":"Amber","hex":"#ffbf00"},{"name":"Demonic Yellow","hex":"#ffe700"},{"name":"Bat-Signal","hex":"#feff00"},{"name":"Bitter Lime","hex":"#cfff00"},{"name":"Electric Lime","hex":"#ccff00"},{"name":"Bright Yellow Green","hex":"#9dff00"},{"name":"Lasting Lime","hex":"#88ff00"},{"name":"Bright Green","hex":"#66ff00"},{"name":"Chlorophyll Green","hex":"#4aff00"},{"name":"Green Screen","hex":"#22ff00"},{"name":"Electric Pickle","hex":"#00ff04"},{"name":"Acid","hex":"#00ff22"},{"name":"Lucent Lime","hex":"#00ff33"},{"name":"Cathode Green","hex":"#00ff55"},{"name":"Booger Buster","hex":"#00ff77"},{"name":"Green Gas","hex":"#00ff99"},{"name":"Enthusiasm","hex":"#00ffaa"},{"name":"Ice Ice Baby","hex":"#00ffdd"},{"name":"Master Sword Blue","hex":"#00ffee"},{"name":"Agressive Aqua","hex":"#00fbff"},{"name":"Vivid Sky Blue","hex":"#00ccff"},{"name":"Capri","hex":"#00bfff"},{"name":"Sky of Magritte","hex":"#0099ff"},{"name":"Azure","hex":"#007fff"},{"name":"Blue Ribbon","hex":"#0066ff"},{"name":"Blinking Blue","hex":"#0033ff"},{"name":"Icelandic Water","hex":"#0011ff"},{"name":"Blue","hex":"#0000ff"},{"name":"Blue Pencil","hex":"#2200ff"},{"name":"Electric Ultramarine","hex":"#3f00ff"},{"name":"Aladdin's Feather","hex":"#5500ff"},{"name":"Purple Climax","hex":"#8800ff"},{"name":"Amethyst Ganzstar","hex":"#8f00ff"},{"name":"Electric Purple","hex":"#bf00ff"},{"name":"Phlox","hex":"#df00ff"},{"name":"Brusque Pink","hex":"#ee00ff"},{"name":"Bright Magenta","hex":"#ff08e8"},{"name":"Big bang Pink","hex":"#ff00bb"},{"name":"Mean Girls Lipstick","hex":"#ff00ae"},{"name":"Pink","hex":"#ff0099"},{"name":"Hot Flamingoes","hex":"#ff005d"},{"name":"Blazing Dragonfruit","hex":"#ff0054"},{"name":"Carmine Red","hex":"#ff0038"},{"name":"Bright Red","hex":"#ff000d"}];
|
| 47 |
+
if (!window.__colorNames) window.__colorNames = COLOR_NAMES;
|
| 48 |
+
|
| 49 |
+
if (!window.__colorPickerBus) {
|
| 50 |
+
window.__colorPickerBus = (() => {
|
| 51 |
+
let hue = 337; let adjusting=false; const listeners = new Set();
|
| 52 |
+
return { get: () => ({ hue, adjusting }), publish: (sourceId, nextHue, isAdj) => { hue=((nextHue%360)+360)%360; adjusting=!!isAdj; listeners.forEach(fn => { try { fn({ sourceId, hue, adjusting }); } catch{} }); }, subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); } };
|
| 53 |
+
})();
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
const bootstrap = () => {
|
| 57 |
+
const root = document.querySelector('.color-picker'); if (!root || root.dataset.mounted) return; root.dataset.mounted='true';
|
| 58 |
+
const slider = root.querySelector('.hue-slider'); const knob = root.querySelector('.hue-knob'); const hueValue = root.querySelector('.hue-value'); const currentSwatch = root.querySelector('.current-swatch'); const currentName = root.querySelector('.current-name'); const currentHex = root.querySelector('.current-hex'); const currentLch = root.querySelector('.current-lch'); const currentRgb = root.querySelector('.current-rgb');
|
| 59 |
+
const bus = window.__colorPickerBus; const instanceId = Math.random().toString(36).slice(2);
|
| 60 |
+
const getKnobRadius = () => { try { const w = knob ? knob.getBoundingClientRect().width : 0; return w ? w/2 : 8; } catch { return 8; } };
|
| 61 |
+
const hexToHsl = (H) => {
|
| 62 |
+
const s = H.replace('#','');
|
| 63 |
+
const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s;
|
| 64 |
+
const bigint = parseInt(v, 16);
|
| 65 |
+
let r = (bigint >> 16) & 255, g = (bigint >> 8) & 255, b = bigint & 255;
|
| 66 |
+
r /= 255; g /= 255; b /= 255;
|
| 67 |
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
| 68 |
+
let h = 0, s2 = 0, l = (max + min) / 2;
|
| 69 |
+
if (max !== min) {
|
| 70 |
+
const d = max - min;
|
| 71 |
+
s2 = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
| 72 |
+
switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; default: h = (r - g) / d + 4; }
|
| 73 |
+
h /= 6;
|
| 74 |
+
}
|
| 75 |
+
return { h: Math.round(h*360), s: Math.round(s2*100), l: Math.round(l*100) };
|
| 76 |
+
};
|
| 77 |
+
const hslToHex = (h, s, l) => {
|
| 78 |
+
s /= 100; l /= 100;
|
| 79 |
+
const k = n => (n + h/30) % 12;
|
| 80 |
+
const a = s * Math.min(l, 1 - l);
|
| 81 |
+
const f = n => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
| 82 |
+
const toHex = x => Math.round(255 * x).toString(16).padStart(2, '0');
|
| 83 |
+
return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`.toUpperCase();
|
| 84 |
+
};
|
| 85 |
+
const getName = (hex) => { const h = hexToHsl(hex).h || 0; const labels=['Red','Orange','Yellow','Lime','Green','Cyan','Blue','Indigo','Violet','Magenta']; const idx=Math.round(((h%360)/360)*(labels.length-1)); return labels[idx]; };
|
| 86 |
+
const updateUI = (h, adjusting) => { const rect = slider.getBoundingClientRect(); const r=Math.min(getKnobRadius(), Math.max(0, rect.width/2 - 1)); const t=Math.max(0, Math.min(1, (h/360))); const leftPx = r + t * Math.max(0, (rect.width - 2*r)); if (knob) knob.style.left = (leftPx/rect.width*100) + '%'; if (hueValue) hueValue.textContent=`${Math.round(h)}°`; if (slider) slider.setAttribute('aria-valuenow', String(Math.round(h))); const L=62, S=72; const baseHex=hslToHex(h,S,L); if (currentSwatch) currentSwatch.style.background=baseHex; if (currentName) currentName.textContent=getName(baseHex); if (currentHex) currentHex.textContent=baseHex; if (currentLch) currentLch.textContent = `HSL ${L}, ${S}, ${Math.round(h)}°`; if (currentRgb){ const hex=baseHex.replace('#',''); const R=parseInt(hex.slice(0,2),16), G=parseInt(hex.slice(2,4),16), B=parseInt(hex.slice(4,6),16); currentRgb.textContent=`RGB ${R}, ${G}, ${B}`; } const hoverHex=hslToHex(h, Math.max(0,S-10), Math.max(0, L-8)); const rootEl=document.documentElement; rootEl.style.setProperty('--primary-color', baseHex); rootEl.style.setProperty('--primary-color-hover', hoverHex); };
|
| 87 |
+
const getHueFromEvent = (ev) => { const rect=slider.getBoundingClientRect(); const clientX=ev.touches ? ev.touches[0].clientX : ev.clientX; const x = clientX - rect.left; const r=Math.min(getKnobRadius(), Math.max(0, rect.width/2 - 1)); const effX=Math.max(r, Math.min(rect.width - r, x)); const denom=Math.max(1, rect.width - 2*r); const t=(effX - r) / denom; return t*360; };
|
| 88 |
+
const unsubscribe = bus.subscribe(({ sourceId, hue, adjusting }) => { if (sourceId === instanceId) return; updateUI(hue, adjusting); });
|
| 89 |
+
try { let initH=337; if (window.ColorPalettes && typeof window.ColorPalettes.getPrimary==='function'){ const hex=window.ColorPalettes.getPrimary(); initH = hexToHsl(hex).h || initH; } else { const cssPrimary=getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim(); if (cssPrimary) { initH = hexToHsl(cssPrimary).h || initH; } } const { hue: sharedHue } = bus.get(); updateUI(initH ?? sharedHue, false); bus.publish(instanceId, initH ?? sharedHue, false); } catch { const { hue: sharedHue } = bus.get(); updateUI(sharedHue, false); }
|
| 90 |
+
const onDown = (ev) => { ev.preventDefault(); const h=getHueFromEvent(ev); updateUI(h, true); bus.publish(instanceId, h, true); const move=(e)=>{ e.preventDefault && e.preventDefault(); const hh=getHueFromEvent(e); updateUI(hh, true); bus.publish(instanceId, hh, true); }; const up=()=>{ bus.publish(instanceId, getHueFromEvent(ev), false); window.removeEventListener('mousemove', move); window.removeEventListener('touchmove', move); window.removeEventListener('mouseup', up); window.removeEventListener('touchend', up); }; window.addEventListener('mousemove', move, { passive:false }); window.addEventListener('touchmove', move, { passive:false }); window.addEventListener('mouseup', up, { once:true }); window.addEventListener('touchend', up, { once:true }); };
|
| 91 |
+
if (slider){ slider.addEventListener('mousedown', onDown); slider.addEventListener('touchstart', onDown, { passive:false }); slider.addEventListener('keydown', (e)=>{ const step=e.shiftKey?10:2; if (e.key==='ArrowLeft'){ e.preventDefault(); const { hue } = bus.get(); const h=hue-step; updateUI(h, true); bus.publish(instanceId, h, true); bus.publish(instanceId, h, false); } if (e.key==='ArrowRight'){ e.preventDefault(); const { hue } = bus.get(); const h=hue+step; updateUI(h, true); bus.publish(instanceId, h, true); bus.publish(instanceId, h, false); } }); }
|
| 92 |
+
const ro=new MutationObserver(()=>{ if (!document.body.contains(root)){ unsubscribe && unsubscribe(); ro.disconnect(); } }); ro.observe(document.body, { childList:true, subtree:true });
|
| 93 |
+
};
|
| 94 |
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap, { once:true }); else bootstrap();
|
| 95 |
+
})();
|
| 96 |
+
</script>
|
| 97 |
+
|
| 98 |
+
|
app/src/components/HtmlEmbed.astro
CHANGED
|
@@ -33,11 +33,7 @@ const configAttr = typeof config === 'string' ? config : (config != null ? JSON.
|
|
| 33 |
<div><!-- Fragment not found: {src} --></div>
|
| 34 |
) }
|
| 35 |
|
| 36 |
-
|
| 37 |
-
// Ensure global color palettes generator is loaded once per page
|
| 38 |
-
import '../scripts/color-palettes.js';
|
| 39 |
-
export {};
|
| 40 |
-
</script>
|
| 41 |
|
| 42 |
<script>
|
| 43 |
// Re-execute <script> tags inside the injected fragment (innerHTML doesn't run scripts)
|
|
|
|
| 33 |
<div><!-- Fragment not found: {src} --></div>
|
| 34 |
) }
|
| 35 |
|
| 36 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
<script>
|
| 39 |
// Re-execute <script> tags inside the injected fragment (innerHTML doesn't run scripts)
|
app/src/components/Palettes.astro
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
const rootId = `palettes-${Math.random().toString(36).slice(2)}`;
|
| 3 |
+
---
|
| 4 |
+
<div class="palettes" id={rootId} style="width:100%; margin: 10px 0;">
|
| 5 |
+
<style is:global>
|
| 6 |
+
.palettes { box-sizing: border-box; }
|
| 7 |
+
.palettes .palettes__grid { display: grid; grid-template-columns: 1fr; gap: 12px; max-width: 100%; }
|
| 8 |
+
.palettes .palette-card { position: relative; display: grid; grid-template-columns: 1fr minmax(0, 220px); align-items: stretch; gap: 12px; border: 1px solid var(--border-color); border-radius: 10px; background: var(--surface-bg); padding: 12px; transition: box-shadow .18s ease, transform .18s ease, border-color .18s ease; min-height: 60px; }
|
| 9 |
+
.palettes .palette-card__preview { width: 48px; height: 48px; border-radius: 999px; flex: 0 0 auto; background-size: cover; background-position: center; }
|
| 10 |
+
.palettes .palette-card__copy { position: absolute; top: 50%; left: 100%; transform: translateY(-50%); z-index: 3; border-left: none; border-top-left-radius: 0; border-bottom-left-radius: 0; }
|
| 11 |
+
.palettes .palette-card__copy svg { width: 18px; height: 18px; fill: currentColor; display: block; color: inherit; }
|
| 12 |
+
.palettes .palette-card__swatches { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); grid-auto-rows: 1fr; gap: 2px; margin: 0; min-height: 60px; }
|
| 13 |
+
.palettes .palette-card__swatches .sw { width: 100%; min-width: 0; height: auto; border-radius: 0; border: 1px solid var(--border-color); }
|
| 14 |
+
.palettes .palette-card__swatches .sw:first-child { border-top-left-radius: 8px; border-bottom-left-radius: 8px; }
|
| 15 |
+
.palettes .palette-card__swatches .sw:last-child { border-top-right-radius: 8px; border-bottom-right-radius: 8px; }
|
| 16 |
+
.palettes .palette-card__content { display: flex; flex-direction: row; align-items: center; justify-content: flex-start; gap: 12px; min-width: 0; padding-right: 12px; }
|
| 17 |
+
.palettes .palette-card__preview { width: 48px; height: 48px; border-radius: 999px; position: relative; flex: 0 0 auto; overflow: hidden; }
|
| 18 |
+
.palettes .palette-card__preview .dot { position: absolute; width: 4px; height: 4px; background: #fff; border-radius: 999px; box-shadow: 0 0 0 1px var(--border-color); }
|
| 19 |
+
.palettes .palette-card__preview .donut-hole { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 24px; height: 24px; border-radius: 999px; background: var(--surface-bg); box-shadow: 0 0 0 1px var(--border-color) inset; }
|
| 20 |
+
|
| 21 |
+
.palettes .palette-card__content__info { display: flex; flex-direction: column; }
|
| 22 |
+
.palettes .palette-card__title { text-align: left; font-weight: 800; font-size: 15px; }
|
| 23 |
+
.palettes .palette-card__desc { text-align: left; color: var(--muted-color); line-height: 1.5; font-size: 12px; }
|
| 24 |
+
|
| 25 |
+
.palettes .palettes__select { width: 100%; max-width: 100%; border: 1px solid var(--border-color); background: var(--surface-bg); color: var(--text-color); padding: 8px 10px; border-radius: 8px; }
|
| 26 |
+
.palettes .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 1px, 1px); white-space: nowrap; border: 0; }
|
| 27 |
+
.palettes .palettes__controls { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; margin: 8px 0 14px; }
|
| 28 |
+
.palettes .palettes__field { display: flex; flex-direction: column; gap: 6px; min-width: 0; flex: 1 1 280px; max-width: 100%; }
|
| 29 |
+
.palettes .palettes__label { font-size: 12px; color: var(--muted-color); font-weight: 800; }
|
| 30 |
+
.palettes .palettes__label-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
| 31 |
+
.palettes .ghost-badge { font-size: 11px; padding: 1px 6px; border-radius: 999px; border: 1px solid var(--border-color); color: var(--muted-color); background: transparent; font-variant-numeric: tabular-nums; }
|
| 32 |
+
.palettes .palettes__count { display: flex; align-items: center; gap: 8px; max-width: 100%; }
|
| 33 |
+
.palettes .palettes__count input[type="range"] { width: 100%; }
|
| 34 |
+
.palettes .palettes__count output { min-width: 28px; text-align: center; font-variant-numeric: tabular-nums; font-size: 12px; color: var(--muted-color); }
|
| 35 |
+
.palettes input[type="range"] { -webkit-appearance: none; appearance: none; height: 24px; background: transparent; cursor: pointer; accent-color: var(--primary-color); }
|
| 36 |
+
.palettes input[type="range"]:focus { outline: none; }
|
| 37 |
+
.palettes input[type="range"]::-webkit-slider-runnable-track { height: 6px; background: var(--border-color); border-radius: 999px; }
|
| 38 |
+
.palettes input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; margin-top: -6px; width: 18px; height: 18px; background: var(--primary-color); border: 2px solid var(--surface-bg); border-radius: 50%; }
|
| 39 |
+
.palettes input[type="range"]::-moz-range-track { height: 6px; background: var(--border-color); border: none; border-radius: 999px; }
|
| 40 |
+
.palettes input[type="range"]::-moz-range-progress { height: 6px; background: var(--primary-color); border-radius: 999px; }
|
| 41 |
+
.palettes input[type="range"]::-moz-range-thumb { width: 18px; height: 18px; background: var(--primary-color); border: 2px solid var(--surface-bg); border-radius: 50%; }
|
| 42 |
+
html.cb-grayscale, body.cb-grayscale { filter: grayscale(1) !important; }
|
| 43 |
+
html.cb-protanopia, body.cb-protanopia { filter: url(#cb-protanopia) !important; }
|
| 44 |
+
html.cb-deuteranopia, body.cb-deuteranopia { filter: url(#cb-deuteranopia) !important; }
|
| 45 |
+
html.cb-tritanopia, body.cb-tritanopia { filter: url(#cb-tritanopia) !important; }
|
| 46 |
+
html.cb-achromatopsia, body.cb-achromatopsia { filter: url(#cb-achromatopsia) !important; }
|
| 47 |
+
@media (max-width: 1100px) { .palettes .palette-card { grid-template-columns: 1fr; align-items: stretch; gap: 10px; } .palettes .palette-card__swatches { grid-template-columns: repeat(6, minmax(0, 1fr)); } .palettes .palette-card__content { border-right: none; padding-right: 0; } .palettes .palette-card__copy { display: none; } }
|
| 48 |
+
</style>
|
| 49 |
+
<div class="palettes__controls">
|
| 50 |
+
<div class="palettes__field">
|
| 51 |
+
<label class="palettes__label" for="cb-select">Color vision simulation</label>
|
| 52 |
+
<select id="cb-select" class="palettes__select">
|
| 53 |
+
<option value="none">Normal color vision — typical for most people</option>
|
| 54 |
+
<option value="achromatopsia">Achromatopsia — no color at all</option>
|
| 55 |
+
<option value="protanopia">Protanopia — reduced/absent reds</option>
|
| 56 |
+
<option value="deuteranopia">Deuteranopia — reduced/absent greens</option>
|
| 57 |
+
<option value="tritanopia">Tritanopia — reduced/absent blues</option>
|
| 58 |
+
</select>
|
| 59 |
+
</div>
|
| 60 |
+
<div class="palettes__field">
|
| 61 |
+
<div class="palettes__label-row">
|
| 62 |
+
<label class="palettes__label" for="color-count">Number of colors</label>
|
| 63 |
+
<output id="color-count-out" for="color-count" class="ghost-badge">8</output>
|
| 64 |
+
</div>
|
| 65 |
+
<div class="palettes__count">
|
| 66 |
+
<input id="color-count" type="range" min="6" max="10" step="1" value="8" aria-label="Number of colors" />
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
<div class="palettes__grid"></div>
|
| 71 |
+
<div class="palettes__simu" role="group" aria-labelledby="cb-sim-title">
|
| 72 |
+
<svg aria-hidden="true" focusable="false" width="0" height="0" style="position:absolute; left:-9999px; overflow:hidden;">
|
| 73 |
+
<defs>
|
| 74 |
+
<filter id="cb-protanopia"><feColorMatrix type="matrix" values="0.567 0.433 0 0 0 0.558 0.442 0 0 0 0 0.242 0.758 0 0 0 0 0 1 0"/></filter>
|
| 75 |
+
<filter id="cb-deuteranopia"><feColorMatrix type="matrix" values="0.625 0.375 0 0 0 0.7 0.3 0 0 0 0 0.3 0.7 0 0 0 0 0 1 0"/></filter>
|
| 76 |
+
<filter id="cb-tritanopia"><feColorMatrix type="matrix" values="0.95 0.05 0 0 0 0 0.433 0.567 0 0 0 0.475 0.525 0 0 0 0 0 1 0"/></filter>
|
| 77 |
+
<filter id="cb-achromatopsia"><feColorMatrix type="matrix" values="0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0.299 0.587 0.114 0 0 0 0 0 1 0"/></filter>
|
| 78 |
+
</defs>
|
| 79 |
+
</svg>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
<script type="module" is:inline>
|
| 83 |
+
import '/src/scripts/color-palettes.js';
|
| 84 |
+
const ROOT_ID = "{rootId}";
|
| 85 |
+
(() => {
|
| 86 |
+
const cards = [
|
| 87 |
+
{ key: 'categorical', title: 'Categorical', desc: 'For <strong>non‑numeric categories</strong>; <strong>visually distinct</strong> colors.' },
|
| 88 |
+
{ key: 'sequential', title: 'Sequential', desc: 'For <strong>numeric scales</strong>; gradient from <strong>dark to light</strong>. Ideal for <strong>heatmaps</strong>.' },
|
| 89 |
+
{ key: 'diverging', title: 'Diverging', desc: 'For numeric scales with negative and positive; Opposing extremes with smooth contrast around a neutral midpoint.' }
|
| 90 |
+
];
|
| 91 |
+
const getPaletteColors = (key, count) => {
|
| 92 |
+
const total=Number(count)||6;
|
| 93 |
+
if (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function') {
|
| 94 |
+
return window.ColorPalettes.getColors(key,total) || [];
|
| 95 |
+
}
|
| 96 |
+
return [];
|
| 97 |
+
};
|
| 98 |
+
const render = () => {
|
| 99 |
+
const root = document.getElementById(ROOT_ID) || document.querySelector('.palettes');
|
| 100 |
+
if (!root) return;
|
| 101 |
+
const grid=root.querySelector('.palettes__grid'); if (!grid) return;
|
| 102 |
+
const input=document.getElementById('color-count'); const total=input ? Number(input.value)||6 : 6;
|
| 103 |
+
const html = cards.map(c => {
|
| 104 |
+
const colors=getPaletteColors(c.key,total);
|
| 105 |
+
const swatches=colors.map(col=>`<div class=\"sw\" style=\"background:${col}\"></div>`).join('');
|
| 106 |
+
const baseHex = (window.ColorPalettes && typeof window.ColorPalettes.getPrimary==='function') ? window.ColorPalettes.getPrimary() : (colors[0] || '#FF0000');
|
| 107 |
+
const hueDeg = (()=>{ try { const s=baseHex.replace('#',''); const v=s.length===3?s.split('').map(ch=>ch+ch).join(''):s; const r=parseInt(v.slice(0,2),16)/255, g=parseInt(v.slice(2,4),16)/255, b=parseInt(v.slice(4,6),16)/255; const M=Math.max(r,g,b), m=Math.min(r,g,b), d=M-m; if (d===0) return 0; let h=0; if (M===r) h=((g-b)/d)%6; else if (M===g) h=(b-r)/d+2; else h=(r-g)/d+4; h*=60; if (h<0) h+=360; return h; } catch { return 0; } })();
|
| 108 |
+
const gradient = c.key==='categorical'
|
| 109 |
+
? (() => {
|
| 110 |
+
const steps = 60; // smooth hue wheel (fixed orientation)
|
| 111 |
+
const wheel = Array.from({ length: steps }, (_, i) => `hsl(${Math.round((i/steps)*360)}, 100%, 50%)`).join(', ');
|
| 112 |
+
return `conic-gradient(${wheel})`;
|
| 113 |
+
})()
|
| 114 |
+
: (colors.length ? `linear-gradient(90deg, ${colors.join(', ')})` : `linear-gradient(90deg, var(--border-color), var(--border-color))`);
|
| 115 |
+
const previewInner = (()=>{
|
| 116 |
+
if (c.key !== 'categorical' || !colors.length) return '';
|
| 117 |
+
const ring = 18; const cx = 24; const cy = 24; const offset = (hueDeg/360) * 2 * Math.PI;
|
| 118 |
+
return colors.map((col,i)=>{
|
| 119 |
+
const angle = offset + (i/colors.length) * 2 * Math.PI;
|
| 120 |
+
const x = cx + ring * Math.cos(angle);
|
| 121 |
+
const y = cy + ring * Math.sin(angle);
|
| 122 |
+
return `<span class=\"dot\" style=\"left:${x-2}px; top:${y-2}px\"></span>`;
|
| 123 |
+
}).join('');
|
| 124 |
+
})();
|
| 125 |
+
const donutHole = (c.key === 'categorical') ? '<span class=\"donut-hole\"></span>' : '';
|
| 126 |
+
return `
|
| 127 |
+
<div class="palette-card" data-colors="${colors.join(',')}">
|
| 128 |
+
<div class="palette-card__content">
|
| 129 |
+
<div class=\"palette-card__preview\" aria-hidden=\"true\" style=\"background:${gradient}\">${previewInner}${donutHole}</div>
|
| 130 |
+
<div class="palette-card__content__info">
|
| 131 |
+
<div class="palette-card__title">${c.title}</div>
|
| 132 |
+
<div class="palette-card__desc">${c.desc}</div>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
<div class="palette-card__swatches" style="grid-template-columns: repeat(${colors.length}, minmax(0, 1fr));">${swatches}</div>
|
| 136 |
+
<button class="palette-card__copy button--ghost" type="button" aria-label="Copy palette">
|
| 137 |
+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
|
| 138 |
+
</button>
|
| 139 |
+
</div>`;
|
| 140 |
+
}).join('');
|
| 141 |
+
grid.innerHTML=html;
|
| 142 |
+
};
|
| 143 |
+
const MODE_TO_CLASS = { protanopia:'cb-protanopia', deuteranopia:'cb-deuteranopia', tritanopia:'cb-tritanopia', achromatopsia:'cb-achromatopsia' };
|
| 144 |
+
const CLEAR_CLASSES = Object.values(MODE_TO_CLASS);
|
| 145 |
+
const clearCbClasses = () => { const rootEl=document.documentElement; CLEAR_CLASSES.forEach(cls=>rootEl.classList.remove(cls)); };
|
| 146 |
+
const applyCbClass = (mode) => { clearCbClasses(); const cls=MODE_TO_CLASS[mode]; if (cls) document.documentElement.classList.add(cls); };
|
| 147 |
+
const currentCbMode = () => { const rootEl=document.documentElement; for (const [mode, cls] of Object.entries(MODE_TO_CLASS)) { if (rootEl.classList.contains(cls)) return mode; } return 'none'; };
|
| 148 |
+
const setupCbSim = () => { const select=document.getElementById('cb-select'); if (!select) return; try { select.value=currentCbMode(); } catch{} select.addEventListener('change', () => applyCbClass(select.value)); };
|
| 149 |
+
const setupCountControl = () => { const input=document.getElementById('color-count'); const out=document.getElementById('color-count-out'); if (!input) return; const clamp=(n,min,max)=>Math.max(min,Math.min(max,n)); const read=()=>clamp(Number(input.value)||6,6,10); const syncOut=()=>{ if (out) out.textContent=String(read()); }; const onChange=()=>{ syncOut(); render(); }; syncOut(); input.addEventListener('input', onChange); document.addEventListener('palettes:updated', () => { syncOut(); render(); }); };
|
| 150 |
+
let copyDelegationSetup=false; const setupCopyDelegation = () => { if (copyDelegationSetup) return; const grid=document.querySelector('.palettes .palettes__grid'); if (!grid) return; grid.addEventListener('click', async (e) => { const btn = e.target.closest ? e.target.closest('.palette-card__copy') : null; if (!btn) return; const card = btn.closest('.palette-card'); if (!card) return; const colors=(card.dataset.colors||'').split(',').filter(Boolean); const json=JSON.stringify(colors,null,2); try { await navigator.clipboard.writeText(json); const old=btn.innerHTML; btn.innerHTML='<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 16.2l-3.5-3.5-1.4 1.4L9 19 20.3 7.7l-1.4-1.4z"/></svg>'; setTimeout(()=> btn.innerHTML=old, 900); } catch { window.prompt('Copy palette', json); } }); copyDelegationSetup=true; };
|
| 151 |
+
const bootstrap = () => {
|
| 152 |
+
setupCbSim();
|
| 153 |
+
setupCountControl();
|
| 154 |
+
setupCopyDelegation();
|
| 155 |
+
// Render immediately
|
| 156 |
+
render();
|
| 157 |
+
// Re-render on palette updates
|
| 158 |
+
document.addEventListener('palettes:updated', render);
|
| 159 |
+
// Force an immediate notify after listeners are attached (ensures initial render)
|
| 160 |
+
try {
|
| 161 |
+
if (window.ColorPalettes && typeof window.ColorPalettes.notify === 'function') window.ColorPalettes.notify();
|
| 162 |
+
else if (window.ColorPalettes && typeof window.ColorPalettes.refresh === 'function') window.ColorPalettes.refresh();
|
| 163 |
+
} catch {}
|
| 164 |
+
};
|
| 165 |
+
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', bootstrap, { once: true }); } else { bootstrap(); }
|
| 166 |
+
})();
|
| 167 |
+
</script>
|
| 168 |
+
|
| 169 |
+
|
app/src/content/chapters/introduction.mdx
CHANGED
|
@@ -17,7 +17,8 @@ import Sidenote from "../../components/Sidenote.astro";
|
|
| 17 |
<span className="tag">Markdown-based</span>
|
| 18 |
<span className="tag">KaTeX math</span>
|
| 19 |
<span className="tag">Syntax highlighting</span>
|
| 20 |
-
<span className="tag">Citations
|
|
|
|
| 21 |
<span className="tag">Table of contents</span>
|
| 22 |
<span className="tag">Mermaid diagrams</span>
|
| 23 |
<span className="tag">Plotly ready</span>
|
|
|
|
| 17 |
<span className="tag">Markdown-based</span>
|
| 18 |
<span className="tag">KaTeX math</span>
|
| 19 |
<span className="tag">Syntax highlighting</span>
|
| 20 |
+
<span className="tag">Citations in all flavors</span>
|
| 21 |
+
<span className="tag">Footnotes</span>
|
| 22 |
<span className="tag">Table of contents</span>
|
| 23 |
<span className="tag">Mermaid diagrams</span>
|
| 24 |
<span className="tag">Plotly ready</span>
|
app/src/content/chapters/vibe-coding-charts.mdx
CHANGED
|
@@ -27,8 +27,8 @@ I want you to code a d3 chart that visualizes the data.
|
|
| 27 |
|
| 28 |
They can be found in the `app/src/content/embeds` folder and you can also use them as a starting point or examples to vibe code with.
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
| 32 |
src="d3-benchmark.html" desc={`Figure 1: Grouped bar chart comparing model scores across benchmarks (MMLU, GSM8K, HellaSwag, TruthfulQA, ARC‑C). Each group is a benchmark; colors encode models; values are accuracy/score (higher is better).`} />
|
| 33 |
---
|
| 34 |
<HtmlEmbed
|
|
|
|
| 27 |
|
| 28 |
They can be found in the `app/src/content/embeds` folder and you can also use them as a starting point or examples to vibe code with.
|
| 29 |
|
| 30 |
+
<HtmlEmbed
|
| 31 |
+
title="d3-benchmark: LLM Benchmark"
|
| 32 |
src="d3-benchmark.html" desc={`Figure 1: Grouped bar chart comparing model scores across benchmarks (MMLU, GSM8K, HellaSwag, TruthfulQA, ARC‑C). Each group is a benchmark; colors encode models; values are accuracy/score (higher is better).`} />
|
| 33 |
---
|
| 34 |
<HtmlEmbed
|
app/src/content/chapters/writing-your-content.mdx
CHANGED
|
@@ -6,6 +6,8 @@ import Wide from '../../components/Wide.astro';
|
|
| 6 |
import Note from '../../components/Note.astro';
|
| 7 |
import FullWidth from '../../components/FullWidth.astro';
|
| 8 |
import HtmlEmbed from '../../components/HtmlEmbed.astro';
|
|
|
|
|
|
|
| 9 |
import audioDemo from '../assets/audio/audio-example.wav';
|
| 10 |
import Accordion from '../../components/Accordion.astro';
|
| 11 |
|
|
@@ -130,7 +132,7 @@ Use the **color picker** below to see how the primary color affects the theme.
|
|
| 130 |
#### Brand color
|
| 131 |
|
| 132 |
<Sidenote>
|
| 133 |
-
<
|
| 134 |
<Fragment slot="aside">
|
| 135 |
You can use the color picker to select the right color.
|
| 136 |
|
|
@@ -143,7 +145,7 @@ Use the **color picker** below to see how the primary color affects the theme.
|
|
| 143 |
|
| 144 |
Here is a suggestion of **color palettes** for your **data visualizations** that align with your **brand identity**. These palettes are generated from your `--primary-color`.
|
| 145 |
|
| 146 |
-
<
|
| 147 |
<br/>
|
| 148 |
**Use color with care.**
|
| 149 |
Color should rarely be the only channel of meaning.
|
|
|
|
| 6 |
import Note from '../../components/Note.astro';
|
| 7 |
import FullWidth from '../../components/FullWidth.astro';
|
| 8 |
import HtmlEmbed from '../../components/HtmlEmbed.astro';
|
| 9 |
+
import ColorPicker from '../../components/ColorPicker.astro';
|
| 10 |
+
import Palettes from '../../components/Palettes.astro';
|
| 11 |
import audioDemo from '../assets/audio/audio-example.wav';
|
| 12 |
import Accordion from '../../components/Accordion.astro';
|
| 13 |
|
|
|
|
| 132 |
#### Brand color
|
| 133 |
|
| 134 |
<Sidenote>
|
| 135 |
+
<ColorPicker />
|
| 136 |
<Fragment slot="aside">
|
| 137 |
You can use the color picker to select the right color.
|
| 138 |
|
|
|
|
| 145 |
|
| 146 |
Here is a suggestion of **color palettes** for your **data visualizations** that align with your **brand identity**. These palettes are generated from your `--primary-color`.
|
| 147 |
|
| 148 |
+
<Palettes />
|
| 149 |
<br/>
|
| 150 |
**Use color with care.**
|
| 151 |
Color should rarely be the only channel of meaning.
|
app/src/content/embeds/d3-bar.html
CHANGED
|
@@ -1,16 +1,5 @@
|
|
| 1 |
<div class="d3-bar" ></div>
|
| 2 |
<style>
|
| 3 |
-
/* Theme-driven axis/tick/grid variables */
|
| 4 |
-
.d3-bar {
|
| 5 |
-
--axis-color: rgba(0,0,0,0.25);
|
| 6 |
-
--tick-color: rgba(0,0,0,0.55);
|
| 7 |
-
--grid-color: rgba(0,0,0,0.05);
|
| 8 |
-
}
|
| 9 |
-
[data-theme="dark"] .d3-bar {
|
| 10 |
-
--axis-color: rgba(255,255,255,0.25);
|
| 11 |
-
--tick-color: rgba(255,255,255,0.70);
|
| 12 |
-
--grid-color: rgba(255,255,255,0.08);
|
| 13 |
-
}
|
| 14 |
.d3-bar .controls { margin-top: 0; display: flex; gap: 16px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
|
| 15 |
.d3-bar .controls .control-group { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
|
| 16 |
.d3-bar .controls label { font-size: 12px; color: var(--text-color); font-weight: 700; }
|
|
|
|
| 1 |
<div class="d3-bar" ></div>
|
| 2 |
<style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
.d3-bar .controls { margin-top: 0; display: flex; gap: 16px; align-items: center; justify-content: flex-end; flex-wrap: wrap; }
|
| 4 |
.d3-bar .controls .control-group { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
|
| 5 |
.d3-bar .controls label { font-size: 12px; color: var(--text-color); font-weight: 700; }
|
app/src/content/embeds/d3-benchmark.html
CHANGED
|
@@ -1,16 +1,6 @@
|
|
| 1 |
<div class="d3-benchmark"></div>
|
| 2 |
<style>
|
| 3 |
-
.d3-benchmark {
|
| 4 |
-
position: relative;
|
| 5 |
-
--axis-color: rgba(0,0,0,0.25);
|
| 6 |
-
--tick-color: rgba(0,0,0,0.55);
|
| 7 |
-
--grid-color: rgba(0,0,0,0.05);
|
| 8 |
-
}
|
| 9 |
-
[data-theme="dark"] .d3-benchmark {
|
| 10 |
-
--axis-color: rgba(255,255,255,0.25);
|
| 11 |
-
--tick-color: rgba(255,255,255,0.70);
|
| 12 |
-
--grid-color: rgba(255,255,255,0.08);
|
| 13 |
-
}
|
| 14 |
.d3-benchmark .controls {
|
| 15 |
display: flex;
|
| 16 |
align-items: center;
|
|
|
|
| 1 |
<div class="d3-benchmark"></div>
|
| 2 |
<style>
|
| 3 |
+
.d3-benchmark { position: relative; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
.d3-benchmark .controls {
|
| 5 |
display: flex;
|
| 6 |
align-items: center;
|
app/src/content/embeds/d3-line-quad.html
CHANGED
|
@@ -184,6 +184,8 @@
|
|
| 184 |
<script>
|
| 185 |
(() => {
|
| 186 |
const THIS_SCRIPT = document.currentScript;
|
|
|
|
|
|
|
| 187 |
// Pretty label mapping for metric keys
|
| 188 |
const prettyMetricLabel = (key) => {
|
| 189 |
if (!key) return '';
|
|
@@ -270,8 +272,12 @@
|
|
| 270 |
let axisLabelY = 'Value';
|
| 271 |
|
| 272 |
// Colors and markers (match original embeds)
|
| 273 |
-
const
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
// Shapes supprimés: on n'utilise que la couleur
|
| 276 |
// Ready signal for async load completion
|
| 277 |
let readyResolve = null;
|
|
@@ -371,9 +377,15 @@
|
|
| 371 |
}
|
| 372 |
|
| 373 |
axisLabelY = isRankStrict ? 'Rank' : prettyMetricLabel(metricKey);
|
|
|
|
| 374 |
const { innerWidth, innerHeight } = updateScales();
|
| 375 |
|
| 376 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
|
| 378 |
// zones ± stderr (métriques non rank)
|
| 379 |
gAreas.selectAll('*').remove();
|
|
@@ -407,10 +419,10 @@
|
|
| 407 |
.attr('opacity',0)
|
| 408 |
.attr('stroke', d=>d.color)
|
| 409 |
.attr('d', d=>lineGen(d.values))
|
| 410 |
-
.transition()
|
| 411 |
.attr('opacity',0.9);
|
| 412 |
paths
|
| 413 |
-
.transition()
|
| 414 |
.attr('stroke', d=>d.color)
|
| 415 |
.attr('opacity',0.9)
|
| 416 |
.attr('d', d=>lineGen(d.values));
|
|
@@ -421,14 +433,16 @@
|
|
| 421 |
const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d=> `${d.run}-${d.step}`);
|
| 422 |
ptsSel.enter().append('circle').attr('class','pt')
|
| 423 |
.attr('data-run', d=>d.run)
|
| 424 |
-
.attr('r',
|
| 425 |
.attr('fill', d=>d.color)
|
| 426 |
.attr('fill-opacity', 0.6)
|
| 427 |
.attr('stroke', 'none')
|
| 428 |
.attr('cx', d=>xScale(d.step))
|
| 429 |
.attr('cy', d=>yScale(d.value))
|
| 430 |
.merge(ptsSel)
|
| 431 |
-
.
|
|
|
|
|
|
|
| 432 |
.attr('cx', d=>xScale(d.step))
|
| 433 |
.attr('cy', d=>yScale(d.value));
|
| 434 |
ptsSel.exit().remove();
|
|
@@ -579,13 +593,22 @@
|
|
| 579 |
if (r.ok && window.d3 && window.d3.csvParse) {
|
| 580 |
const txt = await r.text();
|
| 581 |
const rows = window.d3.csvParse(txt);
|
| 582 |
-
const runList = Array.from(new Set(rows.map(row => String(row.run||'').trim()).filter(Boolean)));
|
| 583 |
-
const
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`;
|
| 588 |
}).join('');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 589 |
// Legend hover ghosting across all cells
|
| 590 |
legendItemsHost.querySelectorAll('.item').forEach(el => {
|
| 591 |
el.addEventListener('mouseenter', () => {
|
|
|
|
| 184 |
<script>
|
| 185 |
(() => {
|
| 186 |
const THIS_SCRIPT = document.currentScript;
|
| 187 |
+
// Shared run->color mapping to keep legend and series perfectly in sync
|
| 188 |
+
let SHARED_RUN_COLOR = null;
|
| 189 |
// Pretty label mapping for metric keys
|
| 190 |
const prettyMetricLabel = (key) => {
|
| 191 |
if (!key) return '';
|
|
|
|
| 272 |
let axisLabelY = 'Value';
|
| 273 |
|
| 274 |
// Colors and markers (match original embeds)
|
| 275 |
+
const getRunColors = (n) => {
|
| 276 |
+
try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch(_) {}
|
| 277 |
+
const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';
|
| 278 |
+
return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])].slice(0, n);
|
| 279 |
+
};
|
| 280 |
+
const pool = getRunColors(12);
|
| 281 |
// Shapes supprimés: on n'utilise que la couleur
|
| 282 |
// Ready signal for async load completion
|
| 283 |
let readyResolve = null;
|
|
|
|
| 377 |
}
|
| 378 |
|
| 379 |
axisLabelY = isRankStrict ? 'Rank' : prettyMetricLabel(metricKey);
|
| 380 |
+
const tChange = (window.d3 && d3.transition) ? d3.transition().duration(260).ease(d3.easeCubicOut) : null;
|
| 381 |
const { innerWidth, innerHeight } = updateScales();
|
| 382 |
|
| 383 |
+
const colorForRun = (run, idx) => {
|
| 384 |
+
if (SHARED_RUN_COLOR && Object.prototype.hasOwnProperty.call(SHARED_RUN_COLOR, run)) return SHARED_RUN_COLOR[run];
|
| 385 |
+
const j = (typeof idx === 'number' ? idx : runs.indexOf(run));
|
| 386 |
+
return pool[(j >= 0 ? j : 0) % pool.length];
|
| 387 |
+
};
|
| 388 |
+
const series = runs.map((r, i) => ({ run:r, color: colorForRun(r, i), values:(map[r]||[]).slice().sort((a,b)=>a.step-b.step).map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value), stderr: pt.stderr } : pt) }));
|
| 389 |
|
| 390 |
// zones ± stderr (métriques non rank)
|
| 391 |
gAreas.selectAll('*').remove();
|
|
|
|
| 419 |
.attr('opacity',0)
|
| 420 |
.attr('stroke', d=>d.color)
|
| 421 |
.attr('d', d=>lineGen(d.values))
|
| 422 |
+
.transition(tChange || undefined)
|
| 423 |
.attr('opacity',0.9);
|
| 424 |
paths
|
| 425 |
+
.transition(tChange || undefined)
|
| 426 |
.attr('stroke', d=>d.color)
|
| 427 |
.attr('opacity',0.9)
|
| 428 |
.attr('d', d=>lineGen(d.values));
|
|
|
|
| 433 |
const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d=> `${d.run}-${d.step}`);
|
| 434 |
ptsSel.enter().append('circle').attr('class','pt')
|
| 435 |
.attr('data-run', d=>d.run)
|
| 436 |
+
.attr('r', 1.5)
|
| 437 |
.attr('fill', d=>d.color)
|
| 438 |
.attr('fill-opacity', 0.6)
|
| 439 |
.attr('stroke', 'none')
|
| 440 |
.attr('cx', d=>xScale(d.step))
|
| 441 |
.attr('cy', d=>yScale(d.value))
|
| 442 |
.merge(ptsSel)
|
| 443 |
+
.attr('fill', d=>d.color)
|
| 444 |
+
.transition(tChange || undefined)
|
| 445 |
+
.attr('r', 2)
|
| 446 |
.attr('cx', d=>xScale(d.step))
|
| 447 |
.attr('cy', d=>yScale(d.value));
|
| 448 |
ptsSel.exit().remove();
|
|
|
|
| 593 |
if (r.ok && window.d3 && window.d3.csvParse) {
|
| 594 |
const txt = await r.text();
|
| 595 |
const rows = window.d3.csvParse(txt);
|
| 596 |
+
const runList = Array.from(new Set(rows.map(row => String(row.run||'').trim()).filter(Boolean))).sort();
|
| 597 |
+
const poolLegend = (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function')
|
| 598 |
+
? window.ColorPalettes.getColors('categorical', runList.length)
|
| 599 |
+
: (()=>{ const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB'; return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...((window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'])]; })();
|
| 600 |
+
// Build shared run->color map once
|
| 601 |
+
SHARED_RUN_COLOR = {};
|
| 602 |
+
runList.forEach((name, i) => { SHARED_RUN_COLOR[name] = poolLegend[i % poolLegend.length]; });
|
| 603 |
+
legendItemsHost.innerHTML = runList.map((name) => {
|
| 604 |
+
const color = SHARED_RUN_COLOR[name];
|
| 605 |
return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`;
|
| 606 |
}).join('');
|
| 607 |
+
// Re-render all cells with the shared mapping to ensure perfect sync
|
| 608 |
+
try {
|
| 609 |
+
const currentMetric = (select && select.value) || def;
|
| 610 |
+
if (currentMetric) applyAll(currentMetric);
|
| 611 |
+
} catch {}
|
| 612 |
// Legend hover ghosting across all cells
|
| 613 |
legendItemsHost.querySelectorAll('.item').forEach(el => {
|
| 614 |
el.addEventListener('mouseenter', () => {
|
app/src/content/embeds/d3-line.html
CHANGED
|
@@ -1,17 +1,6 @@
|
|
| 1 |
<div class="d3-line-simple"></div>
|
| 2 |
<style>
|
| 3 |
.d3-line-simple { position: relative; }
|
| 4 |
-
/* Theme-driven axis/tick/grid variables */
|
| 5 |
-
.d3-line-simple {
|
| 6 |
-
--axis-color: rgba(0,0,0,0.25);
|
| 7 |
-
--tick-color: rgba(0,0,0,0.55);
|
| 8 |
-
--grid-color: rgba(0,0,0,0.05);
|
| 9 |
-
}
|
| 10 |
-
[data-theme="dark"] .d3-line-simple {
|
| 11 |
-
--axis-color: rgba(255,255,255,0.25);
|
| 12 |
-
--tick-color: rgba(255,255,255,0.70);
|
| 13 |
-
--grid-color: rgba(255,255,255,0.08);
|
| 14 |
-
}
|
| 15 |
.d3-line-simple .controls {
|
| 16 |
margin-top: 0;
|
| 17 |
display: flex;
|
|
|
|
| 1 |
<div class="d3-line-simple"></div>
|
| 2 |
<style>
|
| 3 |
.d3-line-simple { position: relative; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
.d3-line-simple .controls {
|
| 5 |
margin-top: 0;
|
| 6 |
display: flex;
|
app/src/content/embeds/d3-neural-network.html
CHANGED
|
@@ -105,7 +105,7 @@
|
|
| 105 |
const gOutText = gRoot.append('g').attr('class','out-probs');
|
| 106 |
|
| 107 |
// Network structure (compact: 8 -> 8 -> 10)
|
| 108 |
-
const layerSizes = [8, 8,
|
| 109 |
const layers = layerSizes.map((n, li)=> Array.from({length:n}, (_, i)=>({ id:`L${li}N${i}`, layer: li, index: i, a:0 })));
|
| 110 |
// Links only between hidden->hidden and hidden->output
|
| 111 |
const links = [];
|
|
|
|
| 105 |
const gOutText = gRoot.append('g').attr('class','out-probs');
|
| 106 |
|
| 107 |
// Network structure (compact: 8 -> 8 -> 10)
|
| 108 |
+
const layerSizes = [8, 8, 10];
|
| 109 |
const layers = layerSizes.map((n, li)=> Array.from({length:n}, (_, i)=>({ id:`L${li}N${i}`, layer: li, index: i, a:0 })));
|
| 110 |
// Links only between hidden->hidden and hidden->output
|
| 111 |
const links = [];
|
app/src/content/embeds/d3-pie.html
CHANGED
|
@@ -201,7 +201,11 @@
|
|
| 201 |
|
| 202 |
// Catégories (triées) + échelle de couleurs harmonisée avec banner.html
|
| 203 |
const categories = Array.from(new Set(rows.map(r => r.eagle_cathegory || 'Unknown'))).sort();
|
| 204 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
const colorOf = (cat) => color(cat || 'Unknown');
|
| 206 |
|
| 207 |
// Clear plots grid
|
|
|
|
| 201 |
|
| 202 |
// Catégories (triées) + échelle de couleurs harmonisée avec banner.html
|
| 203 |
const categories = Array.from(new Set(rows.map(r => r.eagle_cathegory || 'Unknown'))).sort();
|
| 204 |
+
const getCatColors = (n) => {
|
| 205 |
+
try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch(_) {}
|
| 206 |
+
return (d3.schemeTableau10 ? d3.schemeTableau10.slice(0, n) : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'].slice(0, n));
|
| 207 |
+
};
|
| 208 |
+
const color = d3.scaleOrdinal().domain(categories).range(getCatColors(categories.length));
|
| 209 |
const colorOf = (cat) => color(cat || 'Unknown');
|
| 210 |
|
| 211 |
// Clear plots grid
|
app/src/content/embeds/d3-scatter.html
CHANGED
|
@@ -122,12 +122,16 @@
|
|
| 122 |
|
| 123 |
function refreshPalette(){
|
| 124 |
try {
|
| 125 |
-
const
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
| 129 |
} catch {
|
| 130 |
-
|
|
|
|
| 131 |
}
|
| 132 |
// Recolor existing marks/labels after palette changes
|
| 133 |
try { if (data && data.length) draw(); } catch {}
|
|
|
|
| 122 |
|
| 123 |
function refreshPalette(){
|
| 124 |
try {
|
| 125 |
+
const cats = categories && categories.length ? categories.length : 6;
|
| 126 |
+
if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {
|
| 127 |
+
const arr = window.ColorPalettes.getColors('categorical', cats) || [];
|
| 128 |
+
if (arr && arr.length) { color.range(arr); return; }
|
| 129 |
+
}
|
| 130 |
+
// fallback
|
| 131 |
+
color.range((d3.schemeTableau10 ? d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab']).slice(0, cats));
|
| 132 |
} catch {
|
| 133 |
+
const cats = categories && categories.length ? categories.length : 6;
|
| 134 |
+
color.range((d3.schemeTableau10 ? d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab']).slice(0, cats));
|
| 135 |
}
|
| 136 |
// Recolor existing marks/labels after palette changes
|
| 137 |
try { if (data && data.length) draw(); } catch {}
|
app/src/content/embeds/vibe-code-d3-embeds-directives.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
### Quickstart (TL;DR)
|
| 4 |
- Create a single self-contained HTML fragment: root div + scoped style + IIFE script.
|
| 5 |
- Draw marks/axes in SVG; render UI (legend and controls) in HTML.
|
| 6 |
-
- Place legend and controls
|
| 7 |
- Load data from public `/data` first, then fall back to `assets/data`.
|
| 8 |
- Use `window.ColorPalettes` for colors; stick to CSS variables for theming.
|
| 9 |
|
|
@@ -160,27 +160,25 @@ Minimal skeleton:
|
|
| 160 |
- Prefer CSS-only where possible.
|
| 161 |
- Keep backgrounds light and borders subtle; the outer card frame is handled by `HtmlEmbed.astro`.
|
| 162 |
|
| 163 |
-
Standard axis/tick/grid colors (
|
| 164 |
|
| 165 |
```css
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
--
|
| 169 |
-
--
|
|
|
|
| 170 |
}
|
| 171 |
-
[data-theme="dark"]
|
| 172 |
-
--axis-color:
|
| 173 |
-
--tick-color:
|
| 174 |
-
--grid-color: rgba(255,255,255
|
| 175 |
}
|
| 176 |
-
/*
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
/* Gridlines: */
|
| 182 |
-
// grid.call(d3.axisLeft(y).ticks(6).tickSize(-innerWidth).tickFormat(''))
|
| 183 |
-
// .call(g => g.selectAll('.tick line').attr('stroke','var(--grid-color)'));
|
| 184 |
```
|
| 185 |
|
| 186 |
#### 3.1) Text on fixed-colored backgrounds
|
|
|
|
| 3 |
### Quickstart (TL;DR)
|
| 4 |
- Create a single self-contained HTML fragment: root div + scoped style + IIFE script.
|
| 5 |
- Draw marks/axes in SVG; render UI (legend and controls) in HTML.
|
| 6 |
+
- Place legend and controls BELOW the chart (header appended after the chart). Include a legend title "Legend" and a select labeled "Metric" when relevant.
|
| 7 |
- Load data from public `/data` first, then fall back to `assets/data`.
|
| 8 |
- Use `window.ColorPalettes` for colors; stick to CSS variables for theming.
|
| 9 |
|
|
|
|
| 160 |
- Prefer CSS-only where possible.
|
| 161 |
- Keep backgrounds light and borders subtle; the outer card frame is handled by `HtmlEmbed.astro`.
|
| 162 |
|
| 163 |
+
Standard axis/tick/grid colors (global variables from `_variables.css`):
|
| 164 |
|
| 165 |
```css
|
| 166 |
+
/* Provided globally */
|
| 167 |
+
:root {
|
| 168 |
+
--axis-color: var(--text-color);
|
| 169 |
+
--tick-color: var(--muted-color);
|
| 170 |
+
--grid-color: rgba(0,0,0,.08);
|
| 171 |
}
|
| 172 |
+
[data-theme="dark"] {
|
| 173 |
+
--axis-color: var(--text-color);
|
| 174 |
+
--tick-color: var(--muted-color);
|
| 175 |
+
--grid-color: rgba(255,255,255,.10);
|
| 176 |
}
|
| 177 |
+
/* Apply inside charts */
|
| 178 |
+
.your-root-class .axes path,
|
| 179 |
+
.your-root-class .axes line { stroke: var(--axis-color); }
|
| 180 |
+
.your-root-class .axes text { fill: var(--tick-color); }
|
| 181 |
+
.your-root-class .grid line { stroke: var(--grid-color); }
|
|
|
|
|
|
|
|
|
|
| 182 |
```
|
| 183 |
|
| 184 |
#### 3.1) Text on fixed-colored backgrounds
|
app/src/scripts/color-palettes.js
CHANGED
|
@@ -214,6 +214,7 @@
|
|
| 214 |
};
|
| 215 |
window.ColorPalettes = {
|
| 216 |
refresh: updatePalettes,
|
|
|
|
| 217 |
getPrimary: () => getPrimaryHex(),
|
| 218 |
getColors: (key, count = 6) => {
|
| 219 |
const primary = getPrimaryHex();
|
|
|
|
| 214 |
};
|
| 215 |
window.ColorPalettes = {
|
| 216 |
refresh: updatePalettes,
|
| 217 |
+
notify: () => { try { const primary = getPrimaryHex(); document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary } })); } catch {} },
|
| 218 |
getPrimary: () => getPrimaryHex(),
|
| 219 |
getColors: (key, count = 6) => {
|
| 220 |
const primary = getPrimaryHex();
|