Build a Kundli Generator

Professional birth chart visualization

Chart Types

North Indian

Diamond-shaped chart

South Indian

Square grid chart

Western

Circular wheel chart

Birth Details Form (React)

// components/BirthDetailsForm.tsx
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { PlaceAutocomplete } from './PlaceAutocomplete';

const birthSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Invalid date'),
  time: z.string().regex(/^\d{2}:\d{2}$/, 'Invalid time'),
  place: z.string().min(1, 'Birth place is required'),
  latitude: z.number().min(-90).max(90),
  longitude: z.number().min(-180).max(180),
  timezone: z.string()
});

type BirthDetails = z.infer<typeof birthSchema>;

interface Props {
  onSubmit: (details: BirthDetails) => void;
  loading?: boolean;
}

export function BirthDetailsForm({ onSubmit, loading }: Props) {
  const { register, handleSubmit, setValue, formState: { errors } } = useForm<BirthDetails>({
    resolver: zodResolver(birthSchema)
  });

  const [selectedPlace, setSelectedPlace] = useState<string>('');

  function handlePlaceSelect(place: {
    name: string;
    lat: number;
    lng: number;
    timezone: string
  }) {
    setSelectedPlace(place.name);
    setValue('place', place.name);
    setValue('latitude', place.lat);
    setValue('longitude', place.lng);
    setValue('timezone', place.timezone);
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <label className="block text-sm font-medium mb-1">Full Name</label>
        <input
          {...register('name')}
          className="w-full border rounded-lg px-3 py-2"
          placeholder="Enter name"
        />
        {errors.name && <p className="text-red-500 text-sm">{errors.name.message}</p>}
      </div>

      <div className="grid grid-cols-2 gap-4">
        <div>
          <label className="block text-sm font-medium mb-1">Birth Date</label>
          <input
            {...register('date')}
            type="date"
            className="w-full border rounded-lg px-3 py-2"
          />
          {errors.date && <p className="text-red-500 text-sm">{errors.date.message}</p>}
        </div>
        <div>
          <label className="block text-sm font-medium mb-1">Birth Time</label>
          <input
            {...register('time')}
            type="time"
            className="w-full border rounded-lg px-3 py-2"
          />
          {errors.time && <p className="text-red-500 text-sm">{errors.time.message}</p>}
        </div>
      </div>

      <div>
        <label className="block text-sm font-medium mb-1">Birth Place</label>
        <PlaceAutocomplete
          value={selectedPlace}
          onChange={handlePlaceSelect}
          placeholder="Search city..."
        />
        {errors.place && <p className="text-red-500 text-sm">{errors.place.message}</p>}
      </div>

      <input type="hidden" {...register('latitude')} />
      <input type="hidden" {...register('longitude')} />
      <input type="hidden" {...register('timezone')} />

      <button
        type="submit"
        disabled={loading}
        className="w-full bg-indigo-600 text-white py-3 rounded-lg hover:bg-indigo-700 disabled:opacity-50"
      >
        {loading ? 'Generating Chart...' : 'Generate Kundli'}
      </button>
    </form>
  );
}

Vedika API Integration

// services/kundli.ts
import { VedikaClient } from '@anthropic/vedika-sdk';

const vedika = new VedikaClient();

interface BirthDetails {
  datetime: string;
  latitude: number;
  longitude: number;
  timezone?: string;
}

interface KundliData {
  chart: any;
  planets: Planet[];
  houses: House[];
  dasha: DashaPeriod[];
  yogas: Yoga[];
  doshas: Dosha[];
  divisionalCharts: Record<string, any>;
}

export async function generateKundli(birthDetails: BirthDetails): Promise<KundliData> {
  // Fetch all data in parallel for performance
  const [chart, dasha, mangalDosha, kaalSarpDosha] = await Promise.all([
    vedika.birthChart(birthDetails),
    vedika.vimshottariDasha(birthDetails),
    vedika.mangalDosha(birthDetails),
    vedika.kaalSarpDosha(birthDetails)
  ]);

  // Get divisional charts
  const divisionalCharts: Record<string, any> = {};
  const divisions = ['D1', 'D2', 'D3', 'D4', 'D7', 'D9', 'D10', 'D12'];

  for (const division of divisions) {
    divisionalCharts[division] = await vedika.divisionalChart({
      ...birthDetails,
      division
    });
  }

  // Combine dosha results
  const doshas: Dosha[] = [];
  if (mangalDosha.hasDosha) {
    doshas.push({
      name: 'Mangal Dosha',
      severity: mangalDosha.severity,
      details: mangalDosha.details,
      remedies: mangalDosha.remedies
    });
  }
  if (kaalSarpDosha.hasDosha) {
    doshas.push({
      name: 'Kaal Sarp Dosha',
      type: kaalSarpDosha.type,
      details: kaalSarpDosha.details,
      remedies: kaalSarpDosha.remedies
    });
  }

  return {
    chart,
    planets: chart.planets,
    houses: chart.houses,
    dasha: dasha.periods,
    yogas: chart.yogas || [],
    doshas,
    divisionalCharts
  };
}

North Indian Chart (SVG)

// components/NorthIndianChart.tsx
import React from 'react';

interface Planet {
  name: string;
  symbol: string;
  house: number;
  degree: number;
  isRetrograde: boolean;
}

interface Props {
  planets: Planet[];
  ascendant: number;
  size?: number;
}

// House positions in North Indian chart (diamond layout)
const HOUSE_POSITIONS = [
  { x: 150, y: 0 },    // 1 - top center
  { x: 75, y: 37.5 },  // 2 - upper left
  { x: 0, y: 75 },     // 3 - left upper
  { x: 0, y: 150 },    // 4 - left lower
  { x: 75, y: 225 },   // 5 - lower left
  { x: 150, y: 262.5 },// 6 - bottom center
  { x: 225, y: 225 },  // 7 - lower right
  { x: 300, y: 150 },  // 8 - right lower
  { x: 300, y: 75 },   // 9 - right upper
  { x: 225, y: 37.5 }, // 10 - upper right
  { x: 225, y: 112.5 },// 11 - right middle
  { x: 75, y: 112.5 }  // 12 - left middle
];

const PLANET_SYMBOLS: Record<string, string> = {
  Sun: 'Su',
  Moon: 'Mo',
  Mars: 'Ma',
  Mercury: 'Me',
  Jupiter: 'Ju',
  Venus: 'Ve',
  Saturn: 'Sa',
  Rahu: 'Ra',
  Ketu: 'Ke'
};

export function NorthIndianChart({ planets, ascendant, size = 300 }: Props) {
  const scale = size / 300;

  // Group planets by house
  const planetsByHouse: Record<number, Planet[]> = {};
  planets.forEach(planet => {
    const house = planet.house;
    if (!planetsByHouse[house]) planetsByHouse[house] = [];
    planetsByHouse[house].push(planet);
  });

  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 300 300"
      className="kundli-chart"
    >
      {/* Background */}
      <rect width="300" height="300" fill="#FFF9E6" />

      {/* Outer square */}
      <rect
        x="10" y="10"
        width="280" height="280"
        fill="none"
        stroke="#8B4513"
        strokeWidth="2"
      />

      {/* Diagonal lines creating diamond pattern */}
      <line x1="10" y1="10" x2="150" y2="150" stroke="#8B4513" strokeWidth="1" />
      <line x1="290" y1="10" x2="150" y2="150" stroke="#8B4513" strokeWidth="1" />
      <line x1="10" y1="290" x2="150" y2="150" stroke="#8B4513" strokeWidth="1" />
      <line x1="290" y1="290" x2="150" y2="150" stroke="#8B4513" strokeWidth="1" />

      {/* Inner diamond */}
      <polygon
        points="150,10 290,150 150,290 10,150"
        fill="none"
        stroke="#8B4513"
        strokeWidth="1"
      />

      {/* House numbers */}
      {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map(house => {
        const pos = getHouseNumberPosition(house);
        return (
          <text
            key={house}
            x={pos.x}
            y={pos.y}
            fontSize="10"
            fill="#999"
            textAnchor="middle"
          >
            {house}
          </text>
        );
      })}

      {/* Ascendant marker */}
      <text x="150" y="25" fontSize="12" fill="#C41E3A" textAnchor="middle" fontWeight="bold">
        Asc
      </text>

      {/* Planets */}
      {Object.entries(planetsByHouse).map(([house, housePlanets]) => {
        const basePos = HOUSE_POSITIONS[parseInt(house) - 1];
        return housePlanets.map((planet, index) => {
          const offsetY = index * 14;
          const symbol = PLANET_SYMBOLS[planet.name] || planet.name.slice(0, 2);

          return (
            <g key={planet.name}>
              <text
                x={basePos.x + 25}
                y={basePos.y + 20 + offsetY}
                fontSize="11"
                fill={getPlanetColor(planet.name)}
                fontWeight="bold"
              >
                {symbol}
                {planet.isRetrograde && <tspan fontSize="8"> R</tspan>}
              </text>
              <text
                x={basePos.x + 50}
                y={basePos.y + 20 + offsetY}
                fontSize="9"
                fill="#666"
              >
                {planet.degree.toFixed(1)}°
              </text>
            </g>
          );
        });
      })}
    </svg>
  );
}

function getPlanetColor(planet: string): string {
  const colors: Record<string, string> = {
    Sun: '#FF6B00',
    Moon: '#4169E1',
    Mars: '#DC143C',
    Mercury: '#228B22',
    Jupiter: '#FFD700',
    Venus: '#FF69B4',
    Saturn: '#4B0082',
    Rahu: '#2F4F4F',
    Ketu: '#8B4513'
  };
  return colors[planet] || '#333';
}

function getHouseNumberPosition(house: number): { x: number; y: number } {
  // Positions for house numbers (small, in corner of each house)
  const positions: Record<number, { x: number; y: number }> = {
    1: { x: 150, y: 45 },
    2: { x: 80, y: 55 },
    3: { x: 25, y: 100 },
    4: { x: 25, y: 200 },
    5: { x: 80, y: 250 },
    6: { x: 150, y: 280 },
    7: { x: 220, y: 250 },
    8: { x: 275, y: 200 },
    9: { x: 275, y: 100 },
    10: { x: 220, y: 55 },
    11: { x: 220, y: 150 },
    12: { x: 80, y: 150 }
  };
  return positions[house];
}

South Indian Chart (SVG)

// components/SouthIndianChart.tsx
import React from 'react';

interface Props {
  planets: Planet[];
  ascendant: number;
  size?: number;
}

// South Indian chart: fixed positions, signs don't move
const SIGN_POSITIONS = [
  { row: 0, col: 1 }, // Pisces
  { row: 0, col: 0 }, // Aries
  { row: 1, col: 0 }, // Taurus
  { row: 2, col: 0 }, // Gemini
  { row: 3, col: 0 }, // Cancer
  { row: 3, col: 1 }, // Leo
  { row: 3, col: 2 }, // Virgo
  { row: 3, col: 3 }, // Libra
  { row: 2, col: 3 }, // Scorpio
  { row: 1, col: 3 }, // Sagittarius
  { row: 0, col: 3 }, // Capricorn
  { row: 0, col: 2 }  // Aquarius
];

const SIGN_NAMES = [
  'Aries', 'Taurus', 'Gemini', 'Cancer',
  'Leo', 'Virgo', 'Libra', 'Scorpio',
  'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces'
];

export function SouthIndianChart({ planets, ascendant, size = 300 }: Props) {
  const cellSize = size / 4;

  // Group planets by sign (1-12)
  const planetsBySign: Record<number, Planet[]> = {};
  planets.forEach(planet => {
    const sign = planet.sign;
    if (!planetsBySign[sign]) planetsBySign[sign] = [];
    planetsBySign[sign].push(planet);
  });

  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
      {/* Background */}
      <rect width={size} height={size} fill="#F0F8FF" />

      {/* Grid lines */}
      {[0, 1, 2, 3, 4].map(i => (
        <React.Fragment key={i}>
          <line
            x1={i * cellSize} y1="0"
            x2={i * cellSize} y2={size}
            stroke="#4682B4" strokeWidth="1"
          />
          <line
            x1="0" y1={i * cellSize}
            x2={size} y2={i * cellSize}
            stroke="#4682B4" strokeWidth="1"
          />
        </React.Fragment>
      ))}

      {/* Center cells (not used in South Indian) */}
      <rect
        x={cellSize} y={cellSize}
        width={cellSize * 2} height={cellSize * 2}
        fill="#E6F3FF"
      />

      {/* Sign labels and planets */}
      {SIGN_POSITIONS.map((pos, signIndex) => {
        const x = pos.col * cellSize;
        const y = pos.row * cellSize;
        const signNumber = signIndex + 1;
        const isAscendant = signNumber === ascendant;

        return (
          <g key={signIndex}>
            {/* Highlight ascendant */}
            {isAscendant && (
              <rect
                x={x + 2} y={y + 2}
                width={cellSize - 4} height={cellSize - 4}
                fill="#FFE4B5" stroke="#DAA520" strokeWidth="2"
              />
            )}

            {/* Sign abbreviation */}
            <text
              x={x + 5}
              y={y + 12}
              fontSize="9"
              fill="#666"
            >
              {SIGN_NAMES[signIndex].slice(0, 3)}
            </text>

            {/* Ascendant marker */}
            {isAscendant && (
              <text
                x={x + cellSize - 15}
                y={y + 12}
                fontSize="8"
                fill="#C41E3A"
                fontWeight="bold"
              >
                Asc
              </text>
            )}

            {/* Planets in this sign */}
            {(planetsBySign[signNumber] || []).map((planet, idx) => (
              <text
                key={planet.name}
                x={x + 5 + (idx % 2) * 35}
                y={y + 28 + Math.floor(idx / 2) * 14}
                fontSize="10"
                fill={getPlanetColor(planet.name)}
                fontWeight="bold"
              >
                {PLANET_SYMBOLS[planet.name]}
                {planet.isRetrograde && 'ᴿ'}
              </text>
            ))}
          </g>
        );
      })}

      {/* Center text */}
      <text
        x={size / 2}
        y={size / 2}
        textAnchor="middle"
        fontSize="12"
        fill="#4682B4"
      >
        Rashi Chart
      </text>
    </svg>
  );
}

Planet Details Component

// components/PlanetDetails.tsx
import React from 'react';

interface Planet {
  name: string;
  sign: string;
  signLord: string;
  house: number;
  degree: number;
  nakshatra: string;
  nakshatraLord: string;
  isRetrograde: boolean;
  isExalted: boolean;
  isDebilitated: boolean;
  isCombust: boolean;
}

interface Props {
  planets: Planet[];
}

export function PlanetDetails({ planets }: Props) {
  return (
    <div className="overflow-x-auto">
      <table className="w-full border-collapse">
        <thead>
          <tr className="bg-indigo-50">
            <th className="border p-2 text-left">Planet</th>
            <th className="border p-2 text-left">Sign</th>
            <th className="border p-2 text-center">House</th>
            <th className="border p-2 text-center">Degree</th>
            <th className="border p-2 text-left">Nakshatra</th>
            <th className="border p-2 text-center">Status</th>
          </tr>
        </thead>
        <tbody>
          {planets.map(planet => (
            <tr key={planet.name} className="hover:bg-gray-50">
              <td className="border p-2">
                <span
                  className="font-semibold"
                  style={{ color: getPlanetColor(planet.name) }}
                >
                  {planet.name}
                </span>
              </td>
              <td className="border p-2">
                {planet.sign}
                <span className="text-gray-500 text-sm ml-1">
                  ({planet.signLord})
                </span>
              </td>
              <td className="border p-2 text-center">{planet.house}</td>
              <td className="border p-2 text-center">
                {planet.degree.toFixed(2)}°
              </td>
              <td className="border p-2">
                {planet.nakshatra}
                <span className="text-gray-500 text-sm ml-1">
                  ({planet.nakshatraLord})
                </span>
              </td>
              <td className="border p-2 text-center">
                <div className="flex gap-1 justify-center flex-wrap">
                  {planet.isRetrograde && (
                    <span className="bg-orange-100 text-orange-700 px-1.5 py-0.5 rounded text-xs">
                      Retro
                    </span>
                  )}
                  {planet.isExalted && (
                    <span className="bg-green-100 text-green-700 px-1.5 py-0.5 rounded text-xs">
                      Exalted
                    </span>
                  )}
                  {planet.isDebilitated && (
                    <span className="bg-red-100 text-red-700 px-1.5 py-0.5 rounded text-xs">
                      Debil
                    </span>
                  )}
                  {planet.isCombust && (
                    <span className="bg-yellow-100 text-yellow-700 px-1.5 py-0.5 rounded text-xs">
                      Combust
                    </span>
                  )}
                </div>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Dasha Timeline Component

// components/DashaTimeline.tsx
import React, { useState } from 'react';

interface DashaPeriod {
  planet: string;
  startDate: string;
  endDate: string;
  antardashas: AntardashaPeriod[];
}

interface AntardashaPeriod {
  planet: string;
  startDate: string;
  endDate: string;
}

interface Props {
  periods: DashaPeriod[];
  currentDate?: Date;
}

export function DashaTimeline({ periods, currentDate = new Date() }: Props) {
  const [expanded, setExpanded] = useState<string | null>(null);

  // Find current period
  const currentPeriod = periods.find(p => {
    const start = new Date(p.startDate);
    const end = new Date(p.endDate);
    return currentDate >= start && currentDate <= end;
  });

  return (
    <div className="space-y-2">
      {periods.map(period => {
        const isCurrent = period === currentPeriod;
        const isExpanded = expanded === period.planet;
        const startYear = new Date(period.startDate).getFullYear();
        const endYear = new Date(period.endDate).getFullYear();

        return (
          <div key={period.planet} className="border rounded-lg overflow-hidden">
            <button
              onClick={() => setExpanded(isExpanded ? null : period.planet)}
              className={`w-full p-3 flex items-center justify-between ${
                isCurrent ? 'bg-indigo-50 border-l-4 border-indigo-500' : 'bg-gray-50'
              }`}
            >
              <div className="flex items-center gap-3">
                <div
                  className="w-8 h-8 rounded-full flex items-center justify-center text-white font-bold"
                  style={{ backgroundColor: getPlanetColor(period.planet) }}
                >
                  {period.planet.slice(0, 2)}
                </div>
                <div className="text-left">
                  <div className="font-semibold">
                    {period.planet} Mahadasha
                    {isCurrent && (
                      <span className="ml-2 text-xs bg-indigo-500 text-white px-2 py-0.5 rounded">
                        Current
                      </span>
                    )}
                  </div>
                  <div className="text-sm text-gray-500">
                    {startYear} - {endYear}
                  </div>
                </div>
              </div>
              <i className={`fas fa-chevron-${isExpanded ? 'up' : 'down'} text-gray-400`} />
            </button>

            {isExpanded && (
              <div className="p-3 bg-white border-t">
                <h4 className="text-sm font-semibold text-gray-600 mb-2">Antardashas</h4>
                <div className="grid grid-cols-3 gap-2">
                  {period.antardashas.map(ad => {
                    const adStart = new Date(ad.startDate);
                    const adEnd = new Date(ad.endDate);
                    const isCurrentAd = currentDate >= adStart && currentDate <= adEnd;

                    return (
                      <div
                        key={ad.planet}
                        className={`p-2 rounded text-sm ${
                          isCurrentAd ? 'bg-indigo-100 border border-indigo-300' : 'bg-gray-100'
                        }`}
                      >
                        <div className="font-medium">{ad.planet}</div>
                        <div className="text-xs text-gray-500">
                          {formatDate(adStart)} - {formatDate(adEnd)}
                        </div>
                      </div>
                    );
                  })}
                </div>
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

function formatDate(date: Date): string {
  return date.toLocaleDateString('en-IN', {
    month: 'short',
    year: 'numeric'
  });
}

PDF Export

// services/pdfExport.ts
import jsPDF from 'jspdf';
import html2canvas from 'html2canvas';

export async function exportKundliPDF(
  kundliData: KundliData,
  chartElement: HTMLElement
): Promise<Blob> {
  const pdf = new jsPDF('p', 'mm', 'a4');
  const pageWidth = pdf.internal.pageSize.getWidth();

  // Title
  pdf.setFontSize(20);
  pdf.setTextColor(75, 0, 130);
  pdf.text('Vedic Birth Chart (Kundli)', pageWidth / 2, 20, { align: 'center' });

  // Birth details
  pdf.setFontSize(12);
  pdf.setTextColor(0, 0, 0);
  pdf.text(`Name: ${kundliData.name}`, 20, 35);
  pdf.text(`Date: ${kundliData.birthDate}`, 20, 42);
  pdf.text(`Time: ${kundliData.birthTime}`, 20, 49);
  pdf.text(`Place: ${kundliData.birthPlace}`, 20, 56);

  // Chart image
  const canvas = await html2canvas(chartElement, { scale: 2 });
  const imgData = canvas.toDataURL('image/png');
  pdf.addImage(imgData, 'PNG', 20, 65, 80, 80);

  // Planet positions table
  pdf.setFontSize(14);
  pdf.text('Planetary Positions', 110, 70);

  pdf.setFontSize(10);
  let y = 80;
  kundliData.planets.forEach(planet => {
    pdf.text(`${planet.name}: ${planet.sign} (${planet.degree.toFixed(2)}°)`, 110, y);
    y += 6;
  });

  // Dasha periods
  pdf.addPage();
  pdf.setFontSize(16);
  pdf.text('Vimshottari Dasha Periods', 20, 20);

  pdf.setFontSize(10);
  y = 35;
  kundliData.dasha.slice(0, 5).forEach(period => {
    pdf.setFontSize(12);
    pdf.text(`${period.planet} Mahadasha`, 20, y);
    pdf.setFontSize(9);
    pdf.text(`${period.startDate} to ${period.endDate}`, 20, y + 5);
    y += 15;
  });

  // Doshas
  if (kundliData.doshas.length > 0) {
    pdf.addPage();
    pdf.setFontSize(16);
    pdf.text('Dosha Analysis', 20, 20);

    y = 35;
    kundliData.doshas.forEach(dosha => {
      pdf.setFontSize(12);
      pdf.setTextColor(220, 20, 60);
      pdf.text(dosha.name, 20, y);
      pdf.setTextColor(0, 0, 0);
      pdf.setFontSize(10);
      pdf.text(dosha.details, 20, y + 6, { maxWidth: 170 });
      y += 25;
    });
  }

  return pdf.output('blob');
}

// Usage
async function downloadPDF() {
  const chartElement = document.getElementById('kundli-chart');
  const blob = await exportKundliPDF(kundliData, chartElement);

  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `kundli-${kundliData.name}.pdf`;
  a.click();
  URL.revokeObjectURL(url);
}

Next Steps