Skip to content

Commit 1ecf700

Browse files
feat(hero): redesign landing with MonoLisa-only typography + script variant
- Migrate entire font system to MonoLisa (64 Unicode subsets on GCS). Drop Fraunces and Inter; use italic + ss02 stylistic set as editorial accent. - Rework hero copy: "the open plot catalogue", "steal like an artist.", lowercase throughout, broader vision-aligned intro (no Python focus). - Add live GitHub release tag to masthead (main + v1.1.0 linked). - New NumbersStrip: 5 quiet stats (languages, libraries, specs, implementations, lines of code) hanging under hero; libraries section peeks below (88svh). - Stack secondary CTAs next to primary button. - Add lines_of_code to /stats endpoint. - Cursor fade after typewriter finishes; script variant enabled globally. - Update README, vision.md, LegalPage disclaimer to drop Python-specific framing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f12d08c commit 1ecf700

File tree

19 files changed

+658
-207
lines changed

19 files changed

+658
-207
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@
88
[![Ruff](https://github.com/MarkusNeusinger/anyplot/actions/workflows/ci-lint.yml/badge.svg?branch=main)](https://github.com/MarkusNeusinger/anyplot/actions/workflows/ci-lint.yml)
99
[![codecov](https://codecov.io/github/MarkusNeusinger/anyplot/graph/badge.svg?token=4EGPSHH0H0)](https://codecov.io/github/MarkusNeusinger/anyplot)
1010

11-
> library-agnostic, ai-powered python plotting examples.
11+
> library-agnostic, ai-powered plotting examples.
1212
1313
---
1414

1515
## What is anyplot?
1616

17-
**anyplot** is an AI-powered platform for Python data visualization that automatically discovers, generates, tests, and
18-
maintains plotting examples. Browse hundreds of plots across all major Python libraries - matplotlib, seaborn, plotly,
19-
bokeh, altair, plotnine, pygal, highcharts, and lets-plot.
17+
**anyplot** is an AI-powered platform for data visualization that automatically discovers, generates, tests, and
18+
maintains plotting examples. Browse hundreds of plots across major visualization libraries — matplotlib, seaborn,
19+
plotly, bokeh, altair, plotnine, pygal, highcharts, and lets-plot — with an architecture ready to welcome additional
20+
ecosystems over time.
2021

2122
**Community-driven, AI-maintained** - Propose plot ideas via GitHub Issues, AI generates the code, automated quality
2223
checks ensure excellence. Zero manual coding required.

api/routers/stats.py

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
from fastapi import APIRouter, Depends
44
from sqlalchemy.ext.asyncio import AsyncSession
55

6-
from api.cache import cache_key, get_cache, get_or_set_cache
6+
from api.cache import cache_key, get_or_set_cache
77
from api.dependencies import optional_db
88
from api.schemas import StatsResponse
99
from core.config import settings
1010
from core.constants import LIBRARIES_METADATA
11-
from core.database import LibraryRepository, SpecRepository
11+
from core.database import ImplRepository, LibraryRepository, SpecRepository
1212
from core.database.connection import get_db_context
1313

1414

@@ -17,26 +17,22 @@
1717

1818
async def _refresh_stats() -> StatsResponse:
1919
"""Standalone factory for background refresh (creates own DB session)."""
20-
# Try to derive from already-cached endpoint responses
21-
specs_cached = get_cache(cache_key("specs_list"))
22-
libs_cached = get_cache(cache_key("libraries"))
23-
24-
if specs_cached is not None and libs_cached is not None:
25-
return StatsResponse(
26-
specs=len(specs_cached),
27-
plots=sum(item.library_count for item in specs_cached),
28-
libraries=len(libs_cached["libraries"]),
29-
)
30-
3120
async with get_db_context() as db:
3221
spec_repo = SpecRepository(db)
3322
lib_repo = LibraryRepository(db)
23+
impl_repo = ImplRepository(db)
3424
specs = await spec_repo.get_all()
3525
libraries = await lib_repo.get_all()
26+
total_loc = await impl_repo.get_total_code_lines()
3627

3728
specs_with_impls = [s for s in specs if s.impls]
3829
total_impls = sum(len(s.impls) for s in specs)
39-
return StatsResponse(specs=len(specs_with_impls), plots=total_impls, libraries=len(libraries))
30+
return StatsResponse(
31+
specs=len(specs_with_impls),
32+
plots=total_impls,
33+
libraries=len(libraries),
34+
lines_of_code=total_loc,
35+
)
4036

4137

4238
@router.get("/stats", response_model=StatsResponse)
@@ -50,26 +46,21 @@ async def get_stats(db: AsyncSession | None = Depends(optional_db)):
5046
return StatsResponse(specs=0, plots=0, libraries=len(LIBRARIES_METADATA))
5147

5248
async def _fetch() -> StatsResponse:
53-
# Try to derive from already-cached endpoint responses
54-
specs_cached = get_cache(cache_key("specs_list"))
55-
libs_cached = get_cache(cache_key("libraries"))
56-
57-
if specs_cached is not None and libs_cached is not None:
58-
return StatsResponse(
59-
specs=len(specs_cached),
60-
plots=sum(item.library_count for item in specs_cached),
61-
libraries=len(libs_cached["libraries"]),
62-
)
63-
64-
# Fallback: DB query (only on cold start if /stats arrives before /specs)
6549
spec_repo = SpecRepository(db)
6650
lib_repo = LibraryRepository(db)
51+
impl_repo = ImplRepository(db)
6752
specs = await spec_repo.get_all()
6853
libraries = await lib_repo.get_all()
54+
total_loc = await impl_repo.get_total_code_lines()
6955

7056
specs_with_impls = [s for s in specs if s.impls]
7157
total_impls = sum(len(s.impls) for s in specs)
72-
return StatsResponse(specs=len(specs_with_impls), plots=total_impls, libraries=len(libraries))
58+
return StatsResponse(
59+
specs=len(specs_with_impls),
60+
plots=total_impls,
61+
libraries=len(libraries),
62+
lines_of_code=total_loc,
63+
)
7364

7465
return await get_or_set_cache(
7566
cache_key("stats"), _fetch, refresh_after=settings.cache_refresh_after, refresh_factory=_refresh_stats

api/schemas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,4 @@ class StatsResponse(BaseModel):
117117
specs: int
118118
plots: int
119119
libraries: int
120+
lines_of_code: int = 0

app/index.html

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@
2525
<link rel="preconnect" href="https://storage.googleapis.com" crossorigin>
2626
<!-- Preconnect to API for faster first request -->
2727
<link rel="preconnect" href="https://api.anyplot.ai" crossorigin>
28-
<!-- Google Fonts: Fraunces (serif headlines) + Inter (sans UI) -->
29-
<link rel="preconnect" href="https://fonts.googleapis.com">
30-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
31-
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,400;0,9..144,500;0,9..144,600;0,9..144,700;1,9..144,400&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
3228

3329
<!-- Preload MonoLisa Basic Latin (most used, variable font = all weights) -->
3430
<link rel="preload" href="https://storage.googleapis.com/anyplot-static/fonts/0-MonoLisa-normal.woff2" as="font" type="font/woff2" crossorigin>

app/src/components/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import ShuffleIcon from '@mui/icons-material/Shuffle';
1010
import { colors, typography, semanticColors } from '../theme';
1111

1212
interface HeaderProps {
13-
stats?: { specs: number; plots: number; libraries: number } | null;
13+
stats?: { specs: number; plots: number; libraries: number; lines_of_code?: number } | null;
1414
onRandom?: (method: 'click' | 'space' | 'doubletap') => void;
1515
}
1616

app/src/components/HeroSection.tsx

Lines changed: 73 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { PlotOfTheDayTerminal } from './PlotOfTheDayTerminal';
77
import type { PlotOfTheDayData } from '../hooks/usePlotOfTheDay';
88

99
interface HeroSectionProps {
10-
stats: { specs: number; plots: number; libraries: number } | null;
10+
stats: { specs: number; plots: number; libraries: number; lines_of_code?: number } | null;
1111
potd?: PlotOfTheDayData | null;
1212
}
1313

@@ -35,8 +35,7 @@ export function HeroSection({ stats, potd = null }: HeroSectionProps) {
3535
animation: 'rise 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) backwards',
3636
fontFamily: typography.mono,
3737
fontSize: '11px',
38-
textTransform: 'uppercase',
39-
letterSpacing: '0.15em',
38+
letterSpacing: '0.08em',
4039
color: colors.primary,
4140
mb: 3,
4241
display: 'flex',
@@ -51,7 +50,7 @@ export function HeroSection({ stats, potd = null }: HeroSectionProps) {
5150
},
5251
}}
5352
>
54-
Data Visualization Catalogue
53+
the open plot catalogue
5554
</Box>
5655

5756
<Box
@@ -65,7 +64,7 @@ export function HeroSection({ stats, potd = null }: HeroSectionProps) {
6564
letterSpacing: '-0.03em',
6665
color: 'var(--ink)',
6766
m: 0,
68-
mb: 4,
67+
mb: 3,
6968
}}
7069
>
7170
<Box
@@ -93,28 +92,59 @@ export function HeroSection({ stats, potd = null }: HeroSectionProps) {
9392
<Box component="span" sx={{ fontWeight: 400, opacity: 0.45 }}>()</Box>
9493
</Box>
9594
<br />
96-
<Box component="span" sx={{ fontFamily: typography.serif, fontWeight: 400 }}>
95+
<Box
96+
component="span"
97+
sx={{
98+
fontFamily: typography.serif,
99+
fontWeight: 400,
100+
fontStyle: 'italic',
101+
fontFeatureSettings: '"ss02"',
102+
whiteSpace: 'nowrap',
103+
fontSize: '0.75em',
104+
}}
105+
>
97106
— any library.
98107
</Box>
99108
</Box>
100109

110+
<Box
111+
component="p"
112+
sx={{
113+
animation: 'rise 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) 0.15s backwards',
114+
fontFamily: typography.serif,
115+
fontSize: { xs: '0.9375rem', sm: '1rem', md: '1.125rem' },
116+
lineHeight: 1.4,
117+
color: 'var(--ink)',
118+
mt: 0,
119+
mb: 2.5,
120+
fontWeight: 500,
121+
letterSpacing: '-0.005em',
122+
whiteSpace: 'nowrap',
123+
}}
124+
>
125+
one spec · every library · always current.
126+
</Box>
127+
101128
<Box
102129
sx={{
103130
animation: 'rise 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) 0.2s backwards',
104131
fontFamily: typography.serif,
105132
fontSize: { xs: '1rem', md: '1.1875rem' },
106-
lineHeight: 1.5,
133+
lineHeight: 1.55,
107134
color: 'var(--ink-soft)',
108135
mb: 4,
109136
fontWeight: 300,
110137
}}
111138
>
112-
A curated catalogue of visualization examples — human ideas,
113-
AI-drafted specs, open implementations across nine Python libraries.
139+
every plot begins as a library-agnostic spec. ai drafts implementations across every
140+
supported library, validates them against quality criteria, and keeps them current. you
141+
discover, copy, and adapt — plug in <em>your</em>
142+
{' '}data. that&apos;s it.
114143
</Box>
115144

116145
<TypewriterText
117-
lines={['get inspired.', 'grab the code.', 'make it yours.']}
146+
lines={['steal like an artist.']}
147+
charDelay={68}
118148
sx={{
119149
animation: 'rise 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) 0.25s backwards',
120150
fontFamily: typography.serif,
@@ -130,55 +160,19 @@ export function HeroSection({ stats, potd = null }: HeroSectionProps) {
130160
<Box
131161
sx={{
132162
display: 'flex',
133-
gap: 1.5,
163+
alignItems: 'center',
164+
gap: 2.5,
134165
flexWrap: 'wrap',
135166
animation: 'rise 0.8s cubic-bezier(0.2, 0.8, 0.2, 1) 0.3s backwards',
136167
}}
137168
>
138-
<PrimaryCta to="/plots" label="Browse the catalogue" />
139-
<GhostCta to="/mcp" label="use via MCP" />
169+
<PrimaryCta to="/plots" label="browse plots" />
170+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
171+
<SecondaryLink to="/mcp" label="or connect via mcp" />
172+
<SecondaryLink href="https://github.com/MarkusNeusinger/anyplot" label="or clone on github" external />
173+
</Box>
140174
</Box>
141175

142-
{stats && (
143-
<Box
144-
sx={{
145-
mt: 5,
146-
pt: 3,
147-
borderTop: '1px solid var(--rule)',
148-
display: 'flex',
149-
gap: 4,
150-
flexWrap: 'wrap',
151-
fontFamily: typography.mono,
152-
fontSize: '11px',
153-
color: 'var(--ink-muted)',
154-
textTransform: 'uppercase',
155-
letterSpacing: '0.1em',
156-
}}
157-
>
158-
{[
159-
{ value: String(stats.specs), label: 'specs' },
160-
{ value: stats.plots.toLocaleString(), label: 'implementations' },
161-
{ value: String(stats.libraries), label: 'libraries' },
162-
].map((item, i) => (
163-
<Box key={i}>
164-
<Box
165-
sx={{
166-
color: 'var(--ink)',
167-
fontWeight: 500,
168-
fontSize: { xs: '1.25rem', md: '1.625rem' },
169-
letterSpacing: '-0.02em',
170-
textTransform: 'none',
171-
mb: 0.5,
172-
fontFamily: typography.serif,
173-
}}
174-
>
175-
{item.value}
176-
</Box>
177-
{item.label}
178-
</Box>
179-
))}
180-
</Box>
181-
)}
182176
</Box>
183177

184178
{/* Right: terminal plot */}
@@ -217,24 +211,40 @@ function PrimaryCta({ to, label }: { to: string; label: string }) {
217211
);
218212
}
219213

220-
function GhostCta({ to, label }: { to: string; label: string }) {
214+
function SecondaryLink({
215+
to,
216+
href,
217+
label,
218+
external,
219+
}: {
220+
to?: string;
221+
href?: string;
222+
label: string;
223+
external?: boolean;
224+
}) {
225+
const linkProps = external
226+
? { component: 'a' as const, href, target: '_blank', rel: 'noopener noreferrer' }
227+
: { component: RouterLink, to };
228+
221229
return (
222230
<Box
223-
component={RouterLink}
224-
to={to}
231+
{...linkProps}
225232
sx={{
226233
textDecoration: 'none',
227234
fontFamily: typography.mono,
228235
fontSize: '13px',
229-
padding: '13px 22px',
230-
borderRadius: '99px',
231-
border: '1px solid var(--rule)',
232-
color: 'var(--ink)',
233-
transition: 'all 0.2s',
234-
'&:hover': { borderColor: 'var(--ink)' },
236+
color: 'var(--ink-soft)',
237+
display: 'inline-flex',
238+
alignItems: 'center',
239+
gap: 0.5,
240+
transition: 'color 0.2s',
241+
'& .arrow': { transition: 'transform 0.2s' },
242+
'&:hover': { color: colors.primary },
243+
'&:hover .arrow': { transform: 'translateX(3px)' },
244+
'&:focus-visible': { outline: `2px solid ${colors.primary}`, outlineOffset: 2, borderRadius: '2px' },
235245
}}
236246
>
237-
{label}
247+
{label}&nbsp;<Box component="span" className="arrow"></Box>
238248
</Box>
239249
);
240250
}

app/src/components/Layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function AppDataProvider({ children }: { children: ReactNode }) {
2121
const themeMode = useThemeMode();
2222
const [specsData, setSpecsData] = useState<SpecInfo[]>([]);
2323
const [librariesData, setLibrariesData] = useState<LibraryInfo[]>([]);
24-
const [stats, setStats] = useState<{ specs: number; plots: number; libraries: number } | null>(null);
24+
const [stats, setStats] = useState<{ specs: number; plots: number; libraries: number; lines_of_code?: number } | null>(null);
2525

2626
// Persistent home state (both ref for sync access and state for reactivity)
2727
const [homeState, setHomeState] = useState<HomeState>(initialHomeState);

0 commit comments

Comments
 (0)