├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── GraphExample.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── bgd.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── firegraph-hero.png ├── hero.png ├── next.svg ├── og.png ├── site.webmanifest └── vercel.svg ├── src ├── app │ ├── embed │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── hooks │ │ └── useGithubStars.ts │ ├── layout.tsx │ ├── page.tsx │ └── test-embed │ │ └── page.tsx ├── components │ ├── data-input.tsx │ ├── github-button.tsx │ ├── graph.tsx │ ├── main.tsx │ ├── menu.tsx │ └── ui │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── select.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ └── textarea.tsx ├── lib │ ├── theme.ts │ └── utils.ts └── pages │ └── api │ └── githubStars.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | .env 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firegraph 🔥 2 | 3 | Creating beautiful and interactive graphs with Firegraph. Created by [Firecrawl](https://github.com/mendableai/firecrawl). Start with by plotting GitHub stars or your own CSV data. 4 | 5 | ![Firegraph](public/hero.png) 6 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": false, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | env: { 4 | G1: process.env.G1, 5 | G2: process.env.G2, 6 | G3: process.env.G3, 7 | G4: process.env.G4, 8 | }, 9 | }; 10 | 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firegraph", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@headlessui/react": "^2.0.4", 13 | "@headlessui/tailwindcss": "^0.2.0", 14 | "@radix-ui/react-dialog": "^1.0.5", 15 | "@radix-ui/react-dropdown-menu": "^2.0.6", 16 | "@radix-ui/react-select": "^2.0.0", 17 | "@radix-ui/react-slot": "^1.0.2", 18 | "@radix-ui/react-switch": "^1.0.3", 19 | "@remixicon/react": "^4.2.0", 20 | "@tremor/react": "^3.17.2", 21 | "@vercel/analytics": "^1.3.1", 22 | "axios": "^1.7.2", 23 | "class-variance-authority": "^0.7.0", 24 | "clsx": "^2.1.1", 25 | "cubic-spline": "^3.0.3", 26 | "html2canvas": "^1.4.1", 27 | "lucide": "^0.379.0", 28 | "lucide-react": "^0.379.0", 29 | "next": "14.2.3", 30 | "next-themes": "^0.3.0", 31 | "react": "^18", 32 | "react-dom": "^18", 33 | "sonner": "^1.4.41", 34 | "tailwind-merge": "^2.3.0", 35 | "tailwindcss-animate": "^1.0.7" 36 | }, 37 | "devDependencies": { 38 | "@tailwindcss/forms": "^0.5.7", 39 | "@types/node": "^20", 40 | "@types/react": "^18", 41 | "@types/react-dom": "^18", 42 | "eslint": "^8", 43 | "eslint-config-next": "14.2.3", 44 | "postcss": "^8", 45 | "tailwindcss": "^3.4.3", 46 | "typescript": "^5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/GraphExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/firegraph/975aff51c9e3aacf0ca610494a8d33a34503813b/public/GraphExample.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/firegraph/975aff51c9e3aacf0ca610494a8d33a34503813b/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/firegraph/975aff51c9e3aacf0ca610494a8d33a34503813b/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/firegraph/975aff51c9e3aacf0ca610494a8d33a34503813b/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/bgd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/firegraph/975aff51c9e3aacf0ca610494a8d33a34503813b/public/bgd.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/firegraph/975aff51c9e3aacf0ca610494a8d33a34503813b/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/firegraph/975aff51c9e3aacf0ca610494a8d33a34503813b/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/firegraph/975aff51c9e3aacf0ca610494a8d33a34503813b/public/favicon.ico -------------------------------------------------------------------------------- /public/firegraph-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/firegraph/975aff51c9e3aacf0ca610494a8d33a34503813b/public/firegraph-hero.png -------------------------------------------------------------------------------- /public/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/firegraph/975aff51c9e3aacf0ca610494a8d33a34503813b/public/hero.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/firegraph/975aff51c9e3aacf0ca610494a8d33a34503813b/public/og.png -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/embed/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useSearchParams } from "next/navigation"; 3 | import { useEffect, useState, useRef, Suspense } from "react"; 4 | import Graph from "@/components/graph"; 5 | import { allThemes, Theme } from "@/lib/theme"; 6 | 7 | function EmbedPageContent() { 8 | const searchParams = useSearchParams(); 9 | const padding = searchParams?.get("padding") || "64"; 10 | const theme = 11 | searchParams?.get("theme") || "firecrawl" 12 | const background = searchParams?.get("background") || "false"; 13 | const darkMode = searchParams?.get("darkMode") || "false"; 14 | 15 | const [parsedPadding, setParsedPadding] = useState(0); 16 | const [parsedTheme, setParsedTheme] = useState("firecrawl"); 17 | const [parsedBackground, setParsedBackground] = useState(false); 18 | const [parsedDarkMode, setParsedDarkMode] = useState(false); 19 | const chartRef = useRef(null); 20 | 21 | useEffect(() => { 22 | if (padding) { 23 | setParsedPadding(parseInt(padding, 10)); 24 | } 25 | if (theme) { 26 | setParsedTheme(theme); 27 | } 28 | if (background) { 29 | setParsedBackground(background === "true"); 30 | } 31 | if (darkMode) { 32 | setParsedDarkMode(darkMode === "true"); 33 | } 34 | }, [padding, theme, background, darkMode]); 35 | 36 | if (!parsedTheme) { 37 | return
Loading...
; 38 | } 39 | 40 | 41 | return ( 42 | 50 | ); 51 | } 52 | 53 | export default function EmbedPage() { 54 | return ( 55 | Loading...}> 56 | 57 | 58 | ); 59 | } -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mendableai/firegraph/975aff51c9e3aacf0ca610494a8d33a34503813b/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | 7 | .fill-tremor-content-emphasis { 8 | fill: rgb(113 113 122) !important; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/app/hooks/useGithubStars.ts: -------------------------------------------------------------------------------- 1 | export async function useGithubStars() { 2 | const res = await fetch("https://api.github.com/repos/mendableai/firegraph"); 3 | const data = await res.json(); 4 | return data.stargazers_count; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Toaster } from "sonner"; 5 | import { Analytics } from "@vercel/analytics/react"; 6 | import { useEffect, useState } from "react"; 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | const meta = { 10 | title: 'Firegraph', 11 | description: 'Create beautiful graphs. From GitHub star charts to custom data. Made by Firecrawl.', 12 | cardImage: '/og.png', 13 | robots: 'follow, index', 14 | favicon: '/favicon.ico', 15 | url: 'https://www.firegraph.so/' 16 | }; 17 | 18 | export async function generateMetadata(): Promise { 19 | return { 20 | title: meta.title, 21 | description: meta.description, 22 | referrer: 'origin-when-cross-origin', 23 | keywords: ['Firegraph', 'Graphs', 'Data Visualization', 'GitHub', 'Firecrawl'], 24 | authors: [{ name: 'Firecrawl', url: 'https://www.firecrawl.dev/' }], 25 | creator: 'Firecrawl', 26 | publisher: 'Firecrawl', 27 | robots: meta.robots, 28 | icons: { icon: meta.favicon }, 29 | metadataBase: new URL(meta.url), 30 | openGraph: { 31 | url: meta.url, 32 | title: meta.title, 33 | description: meta.description, 34 | images: [meta.cardImage], 35 | type: 'website', 36 | siteName: meta.title 37 | }, 38 | twitter: { 39 | card: 'summary_large_image', 40 | site: '@Vercel', 41 | creator: '@Vercel', 42 | title: meta.title, 43 | description: meta.description, 44 | images: [meta.cardImage] 45 | } 46 | }; 47 | } 48 | 49 | export default function RootLayout({ 50 | children, 51 | }: Readonly<{ 52 | children: React.ReactNode; 53 | }>) { 54 | return ( 55 | 56 | {children} 57 | 58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | // pages/index.tsx 2 | import MainComponent from "@/components/main"; 3 | import { useGithubStars } from "./hooks/useGithubStars"; 4 | import GithubButton from "@/components/github-button"; 5 | 6 | 7 | export default async function Home() { 8 | const githubStars = await useGithubStars(); 9 | return ( 10 |
11 |
12 | 13 |
14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/app/test-embed/page.tsx: -------------------------------------------------------------------------------- 1 | export default function TestEmbed() { 2 | return ( 3 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/data-input.tsx: -------------------------------------------------------------------------------- 1 | import { Github, ClipboardPasteIcon, FileIcon } from "lucide-react"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogHeader, 7 | DialogTitle, 8 | DialogTrigger, 9 | } from "./ui/dialog"; 10 | import { Button } from "./ui/button"; 11 | import { Input } from "./ui/input"; 12 | import { Textarea } from "./ui/textarea"; 13 | import { useEffect, useState } from "react"; 14 | 15 | type DataInputProps = { 16 | setChartData: any; 17 | setXName: any; 18 | setYName: any; 19 | setGraphTitle: any; 20 | repoUrl: string; 21 | setRepoUrl: any; 22 | open: boolean; 23 | setOpen: any; 24 | openCsv: boolean; 25 | setOpenCsv: any; 26 | pastedCsvData: string; 27 | setPastedCsvData: any; 28 | }; 29 | 30 | export default function DataInput({ 31 | setChartData, 32 | setXName, 33 | setYName, 34 | setGraphTitle, 35 | repoUrl, 36 | setRepoUrl, 37 | open, 38 | setOpen, 39 | openCsv, 40 | setOpenCsv, 41 | pastedCsvData, 42 | setPastedCsvData, 43 | }: DataInputProps) { 44 | const [isClient, setIsClient] = useState(false); 45 | 46 | useEffect(() => { 47 | setIsClient(true); 48 | }, []); 49 | return ( 50 |
51 | {typeof window !== "undefined" && ( 52 |
57 |

58 | Choose data you want to visualize 59 |

60 | 61 |
62 | 63 | 64 | 68 | 69 | 70 | 71 | Enter GitHub URL 72 | 73 | Please enter the GitHub repository URL to fetch stars data. 74 | 75 | 76 | setRepoUrl(e.target.value)} 82 | /> 83 |
84 | 87 | 120 |
121 |
122 |
123 | 124 | 125 | 132 | 133 | 134 | 135 | Paste CSV Data 136 | 137 | Please paste your CSV data or upload a CSV file.
138 | Make sure you have only 2 columns (X, Y) 139 |
140 |
141 | 152 | { 157 | const file = e.target.files![0]; 158 | const reader = new FileReader(); 159 | reader.onload = (e: any) => { 160 | setPastedCsvData(e.target.result); 161 | }; 162 | reader.readAsText(file); 163 | }} 164 | // close the dialog after file is uploaded 165 | /> 166 |
167 | 170 | 224 |
225 |
226 |
227 |
228 |
229 | )} 230 |
231 | ); 232 | } 233 | -------------------------------------------------------------------------------- /src/components/github-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Github } from "lucide-react"; 3 | import { Button } from "./ui/button"; 4 | 5 | export default function GithubButton({ githubStars }: { githubStars: number }) { 6 | return ( 7 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/graph.tsx: -------------------------------------------------------------------------------- 1 | // components/Graph.tsx 2 | 3 | import { Theme } from "@/lib/theme"; 4 | import { AreaChart, Color } from "@tremor/react"; 5 | import { useState, useRef, useEffect } from "react"; 6 | import html2canvas from "html2canvas"; 7 | 8 | export default function Graph({ 9 | padding, 10 | chartData, 11 | width, 12 | xName, 13 | yName, 14 | theme, 15 | background, 16 | darkMode, 17 | chartRef, 18 | graphTitle, 19 | setGraphTitle, 20 | maxValue, 21 | setMaxValue, 22 | finalChartData, 23 | setFinalChartData, 24 | }: { 25 | padding: number; 26 | chartData: any; 27 | width: string; 28 | xName: string; 29 | yName: string; 30 | theme: Theme; 31 | background: boolean; 32 | darkMode: boolean; 33 | chartRef: React.RefObject; 34 | graphTitle: string; 35 | setGraphTitle: (graphTitle: string) => void; 36 | maxValue: number; 37 | setMaxValue: (maxValue: number) => void; 38 | finalChartData: any; 39 | setFinalChartData: (finalChartData: any) => void; 40 | }) { 41 | useEffect(() => { 42 | if ( 43 | chartData.length > 0 && 44 | "Stars" in chartData[0] && 45 | "date" in chartData[0] 46 | ) { 47 | const maxStars = Math.max(...chartData.map((data: any) => data.Stars)); 48 | setMaxValue(maxStars); 49 | 50 | setFinalChartData(chartData); 51 | } else { 52 | const maxYValue = Math.max(...chartData.map((data: any) => data[yName])); 53 | setMaxValue(maxYValue); 54 | setFinalChartData(chartData); 55 | } 56 | }, [chartData, yName]); 57 | 58 | return ( 59 |
60 | {typeof window !== "undefined" && ( 61 |
66 |
77 |
81 |
86 |
87 |
88 |
93 |
98 |
103 |
104 |
105 |
109 | {graphTitle} 110 |
111 |
112 |
113 |
114 |
115 | {yName} 116 |
117 |
118 | { 130 | if (value > 999) { 131 | return (value / 1000).toFixed(1) + "k"; 132 | } 133 | return value.toString(); 134 | }} 135 | maxValue={maxValue} 136 | connectNulls={true} 137 | curveType="monotone" 138 | /> 139 |

140 | {xName} 141 |

142 |
143 |
144 |
145 |
146 |
147 |
148 | )} 149 |
150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /src/components/main.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Menu from "../components/menu"; 4 | import Graph from "../components/graph"; 5 | import { useEffect, useRef, useState } from "react"; 6 | import { Theme, allThemes } from "@/lib/theme"; 7 | import html2canvas from "html2canvas"; 8 | import DataInput from "@/components/data-input"; 9 | import { Button } from "@/components/ui/button"; 10 | import { Github } from "lucide-react"; 11 | 12 | export default function MainComponent() { 13 | const initialData = [ 14 | { Date: "Apr 15, 2024", Stars: 0 }, 15 | { Date: "Apr 16, 2024", Stars: 160 }, 16 | { Date: "Apr 17, 2024", Stars: 320 }, 17 | { Date: "Apr 18, 2024", Stars: 480 }, 18 | { Date: "Apr 19, 2024", Stars: 720 }, 19 | { Date: "Apr 20, 2024", Stars: 990 }, 20 | { Date: "Apr 21, 2024", Stars: 1125 }, 21 | { Date: "Apr 22, 2024", Stars: 1260 }, 22 | { Date: "Apr 23, 2024", Stars: 1530 }, 23 | { Date: "Apr 24, 2024", Stars: 1620 }, 24 | { Date: "Apr 25, 2024", Stars: 1710 }, 25 | { Date: "Apr 26, 2024", Stars: 1755 }, 26 | { Date: "Apr 27, 2024", Stars: 1800 }, 27 | { Date: "Apr 28, 2024", Stars: 2040 }, 28 | { Date: "Apr 29, 2024", Stars: 2175 }, 29 | { Date: "Apr 30, 2024", Stars: 2242 }, 30 | { Date: "May 01, 2024", Stars: 2310 }, 31 | { Date: "May 02, 2024", Stars: 2378 }, 32 | { Date: "May 03, 2024", Stars: 2445 }, 33 | { Date: "May 04, 2024", Stars: 2512 }, 34 | { Date: "May 05, 2024", Stars: 2546 }, 35 | { Date: "May 06, 2024", Stars: 2580 }, 36 | { Date: "May 07, 2024", Stars: 2648 }, 37 | { Date: "May 08, 2024", Stars: 2715 }, 38 | { Date: "May 09, 2024", Stars: 2782 }, 39 | { Date: "May 10, 2024", Stars: 2850 }, 40 | { Date: "May 11, 2024", Stars: 2917 }, 41 | { Date: "May 12, 2024", Stars: 2985 }, 42 | { Date: "May 13, 2024", Stars: 3052 }, 43 | { Date: "May 14, 2024", Stars: 3120 }, 44 | { Date: "May 15, 2024", Stars: 3187 }, 45 | { Date: "May 16, 2024", Stars: 3255 }, 46 | { Date: "May 17, 2024", Stars: 3322 }, 47 | { Date: "May 18, 2024", Stars: 3390 }, 48 | { Date: "May 19, 2024", Stars: 3510 }, 49 | { Date: "May 20, 2024", Stars: 3630 }, 50 | { Date: "May 21, 2024", Stars: 3900 }, 51 | { Date: "May 22, 2024", Stars: 4082 }, 52 | { Date: "May 23, 2024", Stars: 4263 }, 53 | { Date: "May 24, 2024", Stars: 4445 }, 54 | { Date: "May 25, 2024", Stars: 4594 }, 55 | ]; 56 | const [chartData, setChartData] = useState(initialData); 57 | const [xName, setXName] = useState("Date"); 58 | const [yName, setYName] = useState("Stars"); 59 | const [padding, setPadding] = useState(32); 60 | const [width, setWidth] = useState("400"); 61 | const [theme, setTheme] = useState( 62 | () => allThemes["firecrawl"] as Theme 63 | ); 64 | 65 | const [background, setBackground] = useState(true); 66 | const [darkMode, setDarkMode] = useState(false); 67 | const [pastedCsvData, setPastedCsvData] = useState(""); 68 | const [repoUrl, setRepoUrl] = useState(""); 69 | const [open, setOpen] = useState(false); 70 | const [openCsv, setOpenCsv] = useState(false); 71 | const chartRef = useRef(null); 72 | const [graphTitle, setGraphTitle] = useState("Firegraph🔥"); 73 | const [maxValue, setMaxValue] = useState(0); 74 | const [finalChartData, setFinalChartData] = useState(chartData); 75 | 76 | useEffect(() => { 77 | if (typeof window !== "undefined") { 78 | setPadding(window.innerWidth < 450 ? 16 : 32); 79 | } 80 | }, []); 81 | 82 | const handleExport = async (copyAsImage: boolean = false) => { 83 | if (chartRef.current) { 84 | const canvas = await html2canvas(chartRef.current, { 85 | useCORS: true, 86 | allowTaint: true, 87 | backgroundColor: null, 88 | scale: 2, 89 | logging: true, 90 | onclone: (document: Document) => { 91 | Array.from( 92 | document.querySelector(".cc")?.querySelectorAll("*") || [] 93 | ).forEach((e) => { 94 | let existingStyle = e.getAttribute("style") || ""; 95 | e.setAttribute( 96 | "style", 97 | existingStyle + "; font-family: Inter, sans-serif !important" 98 | ); 99 | }); 100 | }, 101 | }); 102 | console.log("coco"); 103 | const link = document.createElement("a"); 104 | link.href = canvas.toDataURL("image/png"); 105 | link.download = "chart.png"; 106 | document.body.appendChild(link); // Append the link to the body 107 | link.click(); 108 | document.body.removeChild(link); // Remove the link after download 109 | if (copyAsImage) { 110 | console.log("copying as image"); 111 | canvas.toBlob((blob) => { 112 | if (blob) { 113 | const item = new ClipboardItem({ "image/png": blob }); 114 | navigator.clipboard.write([item]); 115 | } 116 | }); 117 | } 118 | } 119 | }; 120 | 121 | return ( 122 |
128 |
129 | 143 | 160 | 161 | 174 | {typeof window !== "undefined" && ( 175 | 198 | )} 199 |
200 |
201 | ); 202 | } 203 | -------------------------------------------------------------------------------- /src/components/menu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { 3 | Select, 4 | SelectContent, 5 | SelectItem, 6 | SelectTrigger, 7 | SelectValue, 8 | } from "./ui/select"; 9 | import { Theme, allThemes } from "@/lib/theme"; 10 | import { Button } from "./ui/button"; 11 | import { Switch } from "./ui/switch"; 12 | import { 13 | ChevronUp, 14 | Code, 15 | Download, 16 | } from "lucide-react"; 17 | import { toast } from "sonner"; 18 | import { 19 | DropdownMenu, 20 | DropdownMenuContent, 21 | DropdownMenuItem, 22 | DropdownMenuTrigger, 23 | } from "./ui/dropdown-menu"; 24 | import { useState } from "react"; 25 | 26 | export default function Menu({ 27 | padding, 28 | setPadding, 29 | width, 30 | setWidth, 31 | theme, 32 | setTheme, 33 | background, 34 | setBackground, 35 | darkMode, 36 | setDarkMode, 37 | handleExport, 38 | }: { 39 | padding: number; 40 | setPadding: (padding: number) => void; 41 | width: string; 42 | setWidth: (width: string) => void; 43 | theme: Theme; 44 | setTheme: (theme: Theme) => void; 45 | background: boolean; 46 | setBackground: (background: boolean) => void; 47 | darkMode: boolean; 48 | setDarkMode: (darkMode: boolean) => void; 49 | handleExport: () => void; 50 | }) { 51 | const [isClient, setIsClient] = useState(false); 52 | 53 | useEffect(() => { 54 | setIsClient(true); 55 | }, []); 56 | const generateEmbedCode = () => { 57 | const embedCode = ` 58 | 63 | `; 64 | navigator.clipboard.writeText(embedCode).then(() => { 65 | toast("Embed code copied to clipboard!"); 66 | }); 67 | }; 68 | return ( 69 |
70 | {typeof window !== "undefined" && ( 71 |
76 |
81 | {/*
82 | 86 |
*/} 87 | 88 |
89 |
90 | 95 | Theme 96 | 97 | 133 |
134 |
135 | 140 | Background 141 | 142 | setBackground(checked)} 146 | /> 147 |
148 |
149 | 154 | Dark Mode 155 | 156 | setDarkMode(checked)} 160 | /> 161 |
162 |
163 |
164 |
165 | 170 | Padding 171 | 172 | 186 |
187 |
188 | 193 | Width 194 | 195 | 210 |
211 |
212 | 223 | {/* 224 | 228 | 233 | 234 | 235 | generateEmbedCode()}> 236 |
237 | 238 | Embed 239 |
240 |
241 |
242 |
*/} 243 |
244 |
245 |
246 | 247 | {/*
248 | 255 |
*/} 256 |
257 | )} 258 |
259 | ); 260 | } 261 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-zinc-950 dark:focus-visible:ring-zinc-300", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-zinc-900 text-zinc-50 hover:bg-zinc-900/90 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-50/90", 13 | destructive: 14 | "bg-red-500 text-zinc-50 hover:bg-red-500/90 dark:bg-red-900 dark:text-zinc-50 dark:hover:bg-red-900/90", 15 | outline: 16 | "border border-zinc-200 bg-white hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-800 dark:bg-zinc-950 dark:hover:bg-zinc-800 dark:hover:text-zinc-50", 17 | secondary: 18 | "bg-zinc-100 text-zinc-900 hover:bg-zinc-100/80 dark:bg-zinc-800 dark:text-zinc-50 dark:hover:bg-zinc-800/80", 19 | ghost: "hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-zinc-50", 20 | link: "text-zinc-900 underline-offset-4 hover:underline dark:text-zinc-50", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )) 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )) 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )) 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )) 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )) 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ) 181 | } 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | } 201 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SelectPrimitive from "@radix-ui/react-select" 5 | import { Check, ChevronDown, ChevronUp } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Select = SelectPrimitive.Root 10 | 11 | const SelectGroup = SelectPrimitive.Group 12 | 13 | const SelectValue = SelectPrimitive.Value 14 | 15 | const SelectTrigger = React.forwardRef< 16 | React.ElementRef, 17 | Omit, 'noIcon'> & { noIcon?: boolean } 18 | >(({ className, children, noIcon, ...props }, ref) => ( 19 | span]:line-clamp-1 dark:border-zinc-800 dark:bg-zinc-950 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-400 dark:focus:ring-zinc-300", 23 | className 24 | )} 25 | {...props} 26 | > 27 | {children} 28 | 29 | {noIcon ? <> : } 30 | 31 | 32 | )) 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 34 | 35 | const SelectScrollUpButton = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | 48 | 49 | )) 50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 51 | 52 | const SelectScrollDownButton = React.forwardRef< 53 | React.ElementRef, 54 | React.ComponentPropsWithoutRef 55 | >(({ className, ...props }, ref) => ( 56 | 64 | 65 | 66 | )) 67 | SelectScrollDownButton.displayName = 68 | SelectPrimitive.ScrollDownButton.displayName 69 | 70 | const SelectContent = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >(({ className, children, position = "popper", ...props }, ref) => ( 74 | 75 | 86 | 87 | 94 | {children} 95 | 96 | 97 | 98 | 99 | )) 100 | SelectContent.displayName = SelectPrimitive.Content.displayName 101 | 102 | const SelectLabel = React.forwardRef< 103 | React.ElementRef, 104 | React.ComponentPropsWithoutRef 105 | >(({ className, ...props }, ref) => ( 106 | 111 | )) 112 | SelectLabel.displayName = SelectPrimitive.Label.displayName 113 | 114 | const SelectItem = React.forwardRef< 115 | React.ElementRef, 116 | React.ComponentPropsWithoutRef 117 | >(({ className, children, ...props }, ref) => ( 118 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | {children} 133 | 134 | )) 135 | SelectItem.displayName = SelectPrimitive.Item.displayName 136 | 137 | const SelectSeparator = React.forwardRef< 138 | React.ElementRef, 139 | React.ComponentPropsWithoutRef 140 | >(({ className, ...props }, ref) => ( 141 | 146 | )) 147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 148 | 149 | export { 150 | Select, 151 | SelectGroup, 152 | SelectValue, 153 | SelectTrigger, 154 | SelectContent, 155 | SelectLabel, 156 | SelectItem, 157 | SelectSeparator, 158 | SelectScrollUpButton, 159 | SelectScrollDownButton, 160 | } 161 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner } from "sonner" 5 | 6 | type ToasterProps = React.ComponentProps 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme() 10 | 11 | return ( 12 | 28 | ) 29 | } 30 | 31 | export { Toaster } 32 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SwitchPrimitives from "@radix-ui/react-switch" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )) 27 | Switch.displayName = SwitchPrimitives.Root.displayName 28 | 29 | export { Switch } 30 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |