How to Build a Rashi Chart Visualizer (React Native + API Tutorial)
Complete tutorial for rendering North, South, and East Indian chart layouts. Includes planetary positions, house placements, aspects, and interactive features.
How to Build a Rashi Chart Visualizer (React Native + API Tutorial)
Visualizing Vedic astrology charts is one of the most challenging aspects of building kundli applications. Unlike Western charts which use circular wheels, Vedic charts employ three distinct regional layouts - North Indian (diamond), South Indian (square grid), and East Indian (rectangular). Each style has unique conventions for displaying planetary positions and house placements.
This comprehensive tutorial shows you how to build a production-ready Rashi chart visualizer supporting all three Indian styles, complete with planetary glyphs, aspect lines, house numbers, and interactive features.
What We Will Build
Our chart visualizer includes:
- Three Chart Styles - North, South, and East Indian layouts
- Planetary Positions - Visual glyphs with accurate placements
- House Numbers - Clear house identification in each style
- Aspect Lines - Connections between aspecting planets
- Retrograde Indicators - Special markers for retrograde planets
- Lagna Highlighting - Visual emphasis on ascendant
- Interactive Features - Tap planets for details, zoom, pan
- Export Options - Save as image or PDF
Understanding Chart Styles
North Indian Chart (Diamond Layout)
- Diamond-shaped with Lagna (ascendant) always at top
- Houses rotate but shape stays fixed
- Most popular in Northern India
- Houses arranged in counter-clockwise pattern
South Indian Chart (Square Grid)
- Square grid with fixed house positions
- Houses 1-12 always in same position
- Planets move between houses
- Most popular in Southern India
- Easiest to read for beginners
East Indian Chart (Rectangular Layout)
- Rectangular format similar to North Indian
- Lagna position varies by rashi
- Houses in counter-clockwise spiral
- Popular in Bengal and Eastern India
Step 1: Understanding the API Response
First, let's understand the actual birth chart API structure:
// API Response Structure from /birth-chart endpoint
interface BirthChartResponse {
// Planets grouped by rashi
aries: RashiData;
taurus: RashiData;
gemini: RashiData;
cancer: RashiData;
leo: RashiData;
virgo: RashiData;
libra: RashiData;
scorpio: RashiData;
sagittarius: RashiData;
capricorn: RashiData;
aquarius: RashiData;
pisces: RashiData;
// Planet-keyed metadata
meta: {
Sun: PlanetData;
Moon: PlanetData;
Mars: PlanetData;
Mercury: PlanetData;
Jupiter: PlanetData;
Venus: PlanetData;
Saturn: PlanetData;
Rahu: PlanetData;
Ketu: PlanetData;
Lagna: PlanetData;
};
// House descriptions
houses: Array<{
number: number;
name: string;
description: string;
}>;
}
interface RashiData {
rashi: string;
signs: PlanetData[];
}
interface PlanetData {
graha: string;
rashi: string;
longitude: number;
nakshatra: {
anga: string;
key: number;
ratio: number;
abhijit: boolean;
left: number;
name: string;
pada: number;
};
isRetrograde: boolean;
}
Now let's create our chart rendering types:
// types/chart.ts
export interface ChartPlanet {
name: string;
rashi: string;
longitude: number;
nakshatra: string;
pada: number;
isRetrograde: boolean;
}
export interface ChartData {
lagna: ChartPlanet;
planets: ChartPlanet[];
planetsByRashi: Record<string, ChartPlanet[]>;
}
export type ChartStyle = 'north' | 'south' | 'east';
export interface ChartConfig {
style: ChartStyle;
size: number;
showPlanetNames: boolean;
colors: {
background: string;
border: string;
lagna: string;
planet: string;
};
}
Step 2: Transform API Data
Convert the API response to chart-friendly structure:
// services/chart-transformer.ts
export function transformBirthChart(apiResponse: BirthChartResponse): ChartData {
// Extract Lagna
const lagna: ChartPlanet = {
name: 'Lagna',
rashi: apiResponse.meta.Lagna.rashi,
longitude: apiResponse.meta.Lagna.longitude,
nakshatra: apiResponse.meta.Lagna.nakshatra.name,
pada: apiResponse.meta.Lagna.nakshatra.pada,
isRetrograde: false,
};
// Extract all planets from meta
const planetNames = ['Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'];
const planets: ChartPlanet[] = planetNames.map(name => ({
name,
rashi: apiResponse.meta[name].rashi,
longitude: apiResponse.meta[name].longitude,
nakshatra: apiResponse.meta[name].nakshatra.name,
pada: apiResponse.meta[name].nakshatra.pada,
isRetrograde: apiResponse.meta[name].isRetrograde,
}));
// Group planets by rashi for rendering
const planetsByRashi: Record<string, ChartPlanet[]> = {};
planets.forEach(planet => {
const rashiKey = planet.rashi.toLowerCase();
if (!planetsByRashi[rashiKey]) {
planetsByRashi[rashiKey] = [];
}
planetsByRashi[rashiKey].push(planet);
});
return {
lagna,
planets,
planetsByRashi,
};
}
Step 3: North Indian Chart Renderer
Build the diamond-style chart:
// components/charts/NorthIndianChart.tsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import Svg, { Path, Line, Text as SvgText, G } from 'react-native-svg';
interface Props {
chartData: ChartData;
config: ChartConfig;
}
export function NorthIndianChart({ chartData, config }: Props) {
const { size } = config;
const center = size / 2;
const outerRadius = size * 0.45;
const innerRadius = size * 0.15;
// Rashi order starting from Lagna at top (counter-clockwise)
const rashiOrder = [
'aries', 'taurus', 'gemini', 'cancer', 'leo', 'virgo',
'libra', 'scorpio', 'sagittarius', 'capricorn', 'aquarius', 'pisces'
];
// Find Lagna rashi index
const lagnaRashiIndex = rashiOrder.indexOf(chartData.lagna.rashi.toLowerCase());
// Rashi positions in diamond (starting from top, counter-clockwise)
const rashiAngles = [
90, // Top
120, // Top-left
150, // Left-top
180, // Left
210, // Left-bottom
240, // Bottom-left
270, // Bottom
300, // Bottom-right
330, // Right-bottom
0, // Right
30, // Right-top
60, // Top-right
];
// Adjust angles so Lagna is at top
const adjustedRashis = rashiAngles.map((angle, index) => {
const rashiIndex = (index + lagnaRashiIndex) % 12;
const rashi = rashiOrder[rashiIndex];
return { rashi, angle };
});
// Calculate diamond path
const diamondPath = `
M ${center},${center - outerRadius}
L ${center + outerRadius},${center}
L ${center},${center + outerRadius}
L ${center - outerRadius},${center}
Z
`;
// Inner square path
const innerPath = `
M ${center},${center - innerRadius}
L ${center + innerRadius},${center}
L ${center},${center + innerRadius}
L ${center - innerRadius},${center}
Z
`;
// Calculate position for rashi content
function getRashiPosition(angle: number, distance: number) {
const rad = (angle * Math.PI) / 180;
return {
x: center + Math.cos(rad) * distance,
y: center - Math.sin(rad) * distance,
};
}
return (
<View style={[styles.container, { width: size, height: size }]}>
<Svg width={size} height={size}>
{/* Outer diamond */}
<Path
d={diamondPath}
fill={config.colors.background}
stroke={config.colors.border}
strokeWidth={2}
/>
{/* Inner square */}
<Path
d={innerPath}
fill="none"
stroke={config.colors.border}
strokeWidth={1}
/>
{/* Diagonal lines */}
<Line
x1={center - outerRadius}
y1={center}
x2={center + outerRadius}
y2={center}
stroke={config.colors.border}
strokeWidth={1}
/>
<Line
x1={center}
y1={center - outerRadius}
x2={center}
y2={center + outerRadius}
stroke={config.colors.border}
strokeWidth={1}
/>
{/* Rashis and planets */}
{adjustedRashis.map(({ rashi, angle }) => {
const midRadius = (outerRadius + innerRadius) / 2;
const pos = getRashiPosition(angle, midRadius);
const planets = chartData.planetsByRashi[rashi] || [];
const isLagnaRashi = rashi === chartData.lagna.rashi.toLowerCase();
return (
<G key={rashi}>
{/* Rashi name */}
<SvgText
x={pos.x}
y={pos.y - 25}
fontSize={9}
fill={isLagnaRashi ? config.colors.lagna : '#999'}
fontWeight={isLagnaRashi ? 'bold' : 'normal'}
textAnchor="middle"
>
{rashi.charAt(0).toUpperCase() + rashi.slice(1)}
{isLagnaRashi && ' (ASC)'}
</SvgText>
{/* Planets in this rashi */}
{planets.map((planet, index) => {
const yOffset = -5 + (index * 16);
return (
<G key={planet.name}>
<SvgText
x={pos.x}
y={pos.y + yOffset}
fontSize={13}
fill={config.colors.planet}
fontWeight="bold"
textAnchor="middle"
>
{getPlanetGlyph(planet.name)}
{planet.isRetrograde && '℞'}
</SvgText>
</G>
);
})}
</G>
);
})}
</Svg>
</View>
);
}
function getPlanetGlyph(planet: string): string {
const glyphs: Record<string, string> = {
Sun: '☉',
Moon: '☽',
Mars: '♂',
Mercury: '☿',
Jupiter: '♃',
Venus: '♀',
Saturn: '♄',
Rahu: '☊',
Ketu: '☋',
};
return glyphs[planet] || planet.substring(0, 2);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
});
Step 4: South Indian Chart Renderer
Build the square grid layout with fixed rashi positions:
// components/charts/SouthIndianChart.tsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import Svg, { Path, Text as SvgText, G } from 'react-native-svg';
interface Props {
chartData: ChartData;
config: ChartConfig;
}
export function SouthIndianChart({ chartData, config }: Props) {
const { size } = config;
const cellSize = size / 4;
// Fixed rashi positions in South Indian chart (clockwise from top-left)
const rashiOrder = [
'pisces', 'aries', 'taurus', 'gemini',
'aquarius', null, null, 'cancer',
'capricorn', null, null, 'leo',
'sagittarius', 'scorpio', 'libra', 'virgo',
];
const rashiLayout = [
// Row 1
{ rashi: 'pisces', row: 0, col: 0 },
{ rashi: 'aries', row: 0, col: 1 },
{ rashi: 'taurus', row: 0, col: 2 },
{ rashi: 'gemini', row: 0, col: 3 },
// Row 2
{ rashi: 'aquarius', row: 1, col: 0 },
null, // Center diagonal
null, // Center diagonal
{ rashi: 'cancer', row: 1, col: 3 },
// Row 3
{ rashi: 'capricorn', row: 2, col: 0 },
null, // Center diagonal
null, // Center diagonal
{ rashi: 'leo', row: 2, col: 3 },
// Row 4
{ rashi: 'sagittarius', row: 3, col: 0 },
{ rashi: 'scorpio', row: 3, col: 1 },
{ rashi: 'libra', row: 3, col: 2 },
{ rashi: 'virgo', row: 3, col: 3 },
];
function renderCell(rashiName: string | null, row: number, col: number) {
if (rashiName === null) {
// Center diagonal cells
const isDiagonalTopLeft = row === 1 && col === 1;
return (
<G key={`${row}-${col}`}>
<Path
d={
isDiagonalTopLeft
? `M ${col * cellSize},${row * cellSize} L ${(col + 1) * cellSize},${(row + 1) * cellSize}`
: `M ${(col + 1) * cellSize},${row * cellSize} L ${col * cellSize},${(row + 1) * cellSize}`
}
stroke={config.colors.border}
strokeWidth={1}
/>
</G>
);
}
const planets = chartData.planetsByRashi[rashiName] || [];
const isLagnaRashi = rashiName === chartData.lagna.rashi.toLowerCase();
const x = col * cellSize;
const y = row * cellSize;
return (
<G key={rashiName}>
{/* Cell border */}
<Path
d={`M ${x},${y} L ${x + cellSize},${y} L ${x + cellSize},${y + cellSize} L ${x},${y + cellSize} Z`}
fill={isLagnaRashi ? config.colors.lagna + '20' : 'transparent'}
stroke={config.colors.border}
strokeWidth={isLagnaRashi ? 2 : 1}
/>
{/* Rashi name */}
<SvgText
x={x + cellSize / 2}
y={y + 14}
fontSize={9}
fill={isLagnaRashi ? config.colors.lagna : '#999'}
fontWeight={isLagnaRashi ? 'bold' : 'normal'}
textAnchor="middle"
>
{rashiName.charAt(0).toUpperCase() + rashiName.slice(1)}
{isLagnaRashi && ' (ASC)'}
</SvgText>
{/* Planets */}
{planets.map((planet, index) => {
const planetY = y + 35 + (index * 18);
return (
<SvgText
key={planet.name}
x={x + cellSize / 2}
y={planetY}
fontSize={14}
fill={config.colors.planet}
fontWeight="bold"
textAnchor="middle"
>
{getPlanetGlyph(planet.name)}
{planet.isRetrograde && ' ℞'}
</SvgText>
);
})}
</G>
);
}
return (
<View style={[styles.container, { width: size, height: size }]}>
<Svg width={size} height={size}>
{/* Outer border */}
<Path
d={`M 0,0 L ${size},0 L ${size},${size} L 0,${size} Z`}
fill={config.colors.background}
stroke={config.colors.border}
strokeWidth={2}
/>
{/* Render all cells */}
{rashiLayout.map((cell, index) => {
if (!cell) {
const row = Math.floor(index / 4);
const col = index % 4;
return renderCell(null, row, col);
}
return renderCell(cell.rashi, cell.row, cell.col);
})}
</Svg>
</View>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
});
Step 5: East Indian Chart Renderer
Build the rectangular spiral layout:
// components/charts/EastIndianChart.tsx
import React from 'react';
import { View, StyleSheet } from 'react-native';
import Svg, { Path, Text as SvgText, G } from 'react-native-svg';
interface Props {
chartData: ChartData;
config: ChartConfig;
}
export function EastIndianChart({ chartData, config }: Props) {
const { size } = config;
const cols = 4;
const rows = 3;
const cellWidth = size / cols;
const cellHeight = size / rows;
// East Indian rashi layout (spiral pattern similar to North Indian)
const rashiLayout = [
// Top row (12, 1, 2, 3 from Lagna perspective)
{ rashi: 'pisces', row: 0, col: 0 },
{ rashi: 'aries', row: 0, col: 1 },
{ rashi: 'taurus', row: 0, col: 2 },
{ rashi: 'gemini', row: 0, col: 3 },
// Middle row
{ rashi: 'aquarius', row: 1, col: 0 },
{ rashi: 'cancer', row: 1, col: 3 },
// Bottom row
{ rashi: 'capricorn', row: 2, col: 0 },
{ rashi: 'sagittarius', row: 2, col: 1 },
{ rashi: 'scorpio', row: 2, col: 2 },
{ rashi: 'libra', row: 2, col: 3 },
// Side rashis
{ rashi: 'leo', row: 1, col: 3 },
{ rashi: 'virgo', row: 2, col: 3 },
];
function renderRashi(rashiName: string, row: number, col: number) {
const x = col * cellWidth;
const y = row * cellHeight;
const w = cellWidth;
const h = cellHeight;
const planets = chartData.planetsByRashi[rashiName] || [];
const isLagnaRashi = rashiName === chartData.lagna.rashi.toLowerCase();
return (
<G key={rashiName}>
{/* Cell */}
<Path
d={`M ${x},${y} L ${x + w},${y} L ${x + w},${y + h} L ${x},${y + h} Z`}
fill={isLagnaRashi ? config.colors.lagna + '20' : 'transparent'}
stroke={config.colors.border}
strokeWidth={isLagnaRashi ? 2 : 1}
/>
{/* Rashi name */}
<SvgText
x={x + w / 2}
y={y + 14}
fontSize={9}
fill={isLagnaRashi ? config.colors.lagna : '#999'}
fontWeight={isLagnaRashi ? 'bold' : 'normal'}
textAnchor="middle"
>
{rashiName.charAt(0).toUpperCase() + rashiName.slice(1)}
{isLagnaRashi && ' (ASC)'}
</SvgText>
{/* Planets */}
{planets.map((planet, index) => {
const planetY = y + 35 + (index * 16);
return (
<SvgText
key={planet.name}
x={x + w / 2}
y={planetY}
fontSize={12}
fill={config.colors.planet}
fontWeight="bold"
textAnchor="middle"
>
{getPlanetGlyph(planet.name)}
{planet.isRetrograde && ' ℞'}
</SvgText>
);
})}
</G>
);
}
return (
<View style={[styles.container, { width: size, height: size }]}>
<Svg width={size} height={size}>
{/* Background */}
<Path
d={`M 0,0 L ${size},0 L ${size},${size} L 0,${size} Z`}
fill={config.colors.background}
stroke={config.colors.border}
strokeWidth={2}
/>
{/* Render rashis */}
{rashiLayout.map((cell) =>
renderRashi(cell.rashi, cell.row, cell.col)
)}
</Svg>
</View>
);
}
const styles = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
},
});
Step 6: Chart Controller Component
Unified interface for all chart styles:
// components/ChartViewer.tsx
import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
import { NorthIndianChart } from './charts/NorthIndianChart';
import { SouthIndianChart } from './charts/SouthIndianChart';
import { EastIndianChart } from './charts/EastIndianChart';
import { transformBirthChart } from '../services/chart-transformer';
interface BirthData {
date: string;
time: string;
latitude: number;
longitude: number;
timezone: number;
}
interface Props {
birthData: BirthData;
}
export function ChartViewer({ birthData }: Props) {
const [chartData, setChartData] = useState<ChartData | null>(null);
const [style, setStyle] = useState<ChartStyle>('north');
const [loading, setLoading] = useState(true);
useEffect(() => {
loadChartData();
}, [birthData]);
async function loadChartData() {
try {
// Fetch birth chart from API
const response = await fetch('https://roxyapi.com/api/v2/vedic-astrology/birth-chart', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'YOUR_API_KEY',
},
body: JSON.stringify(birthData),
});
const birthChart = await response.json();
// Transform to chart data structure
const data = transformBirthChart(birthChart);
setChartData(data);
} catch (error) {
console.error('Failed to load chart:', error);
} finally {
setLoading(false);
}
}
const config: ChartConfig = {
style,
size: 400,
showPlanetNames: true,
colors: {
background: '#FFFFFF',
border: '#8B4513',
lagna: '#CD853F',
planet: '#4A4A4A',
},
};
if (loading) return <Text>Loading chart...</Text>;
if (!chartData) return <Text>Failed to load chart</Text>;
return (
<View style={styles.container}>
{/* Style Selector */}
<View style={styles.styleSelector}>
<TouchableOpacity
style={[styles.styleButton, style === 'north' && styles.activeStyle]}
onPress={() => setStyle('north')}
>
<Text style={styles.styleText}>North Indian</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.styleButton, style === 'south' && styles.activeStyle]}
onPress={() => setStyle('south')}
>
<Text style={styles.styleText}>South Indian</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.styleButton, style === 'east' && styles.activeStyle]}
onPress={() => setStyle('east')}
>
<Text style={styles.styleText}>East Indian</Text>
</TouchableOpacity>
</View>
{/* Chart Display */}
<View style={styles.chartContainer}>
{style === 'north' && <NorthIndianChart chartData={chartData} config={config} />}
{style === 'south' && <SouthIndianChart chartData={chartData} config={config} />}
{style === 'east' && <EastIndianChart chartData={chartData} config={config} />}
</View>
{/* Planet Details */}
<ScrollView style={styles.planetList}>
<Text style={styles.planetListTitle}>Planetary Positions</Text>
{chartData.planets.map((planet) => (
<View key={planet.name} style={styles.planetItem}>
<Text style={styles.planetName}>
{getPlanetGlyph(planet.name)} {planet.name}
{planet.isRetrograde && ' ℞'}
</Text>
<Text style={styles.planetDetails}>
{planet.rashi} • {planet.nakshatra} (Pada {planet.pada})
</Text>
<Text style={styles.planetLongitude}>
{planet.longitude.toFixed(2)}°
</Text>
</View>
))}
</ScrollView>
</View>
);
}
// Simplified - aspects calculation removed as not included in basic API response\n// For production apps, you would calculate aspects based on planetary longitudes\n// or use a dedicated aspects API endpoint
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFF8F0',
},
styleSelector: {
flexDirection: 'row',
justifyContent: 'space-around',
padding: 16,
backgroundColor: '#FFFFFF',
},
styleButton: {
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 8,
backgroundColor: '#F3F4F6',
},
activeStyle: {
backgroundColor: '#CD853F',
},
styleText: {
fontSize: 14,
fontWeight: '600',
color: '#4A4A4A',
},
chartContainer: {
alignItems: 'center',
padding: 20,
backgroundColor: '#FFFFFF',
margin: 16,
borderRadius: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 3,
},
options: {
flexDirection: 'row',
justifyContent: 'space-around',
padding: 16,
},
optionButton: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
backgroundColor: '#F3F4F6',
},
optionText: {
fontSize: 14,
color: '#4A4A4A',
},
planetList: {
backgroundColor: '#FFFFFF',
margin: 16,
borderRadius: 16,
padding: 20,
},
planetListTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#8B4513',
marginBottom: 16,
},
planetItem: {
marginBottom: 12,
paddingBottom: 12,
borderBottomWidth: 1,
borderBottomColor: '#F3F4F6',
},
planetName: {
fontSize: 16,
fontWeight: '600',
color: '#4A4A4A',
marginBottom: 4,
},
planetDetails: {
fontSize: 13,
color: '#6B7280',
},
planetLongitude: {
fontSize: 11,
color: '#9CA3AF',
marginTop: 2,
},
});
Step 7: Navamsa Chart Support
Support D9 Navamsa chart using the /navamsa endpoint:
// components/NavamsaChartViewer.tsx
export function NavamsaChartViewer({ birthData }: Props) {
const [navamsaData, setNavamsaData] = useState<ChartData | null>(null);
async function loadNavamsa() {
const response = await fetch('https://roxyapi.com/api/v2/vedic-astrology/navamsa', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'YOUR_API_KEY',
},
body: JSON.stringify(birthData),
});
const navamsa = await response.json();
// Response structure is same as birth chart, but in chart property
const data = transformBirthChart(navamsa.chart);
setNavamsaData(data);
}
return navamsaData ? <ChartViewer chartData={navamsaData} /> : null;
}
Step 8: Interactive Features
Add zoom, pan, and planet details:
// components/InteractiveChart.tsx
import React, { useState } from 'react';
import { PanResponder, Animated } from 'react-native';
export function InteractiveChart({ chartData, config }: Props) {
const [scale] = useState(new Animated.Value(1));
const [pan] = useState(new Animated.ValueXY());
const [selectedPlanet, setSelectedPlanet] = useState<string | null>(null);
const panResponder = PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([null, { dx: pan.x, dy: pan.y }], {
useNativeDriver: false,
}),
onPanResponderRelease: () => {
Animated.spring(pan, {
toValue: { x: 0, y: 0 },
useNativeDriver: false,
}).start();
},
});
function handlePinch(event: any) {
const { scale: newScale } = event.nativeEvent;
Animated.spring(scale, {
toValue: newScale,
useNativeDriver: true,
}).start();
}
function handlePlanetTap(planet: string) {
setSelectedPlanet(planet);
}
return (
<View>
<Animated.View
{...panResponder.panHandlers}
style={{
transform: [
{ scale },
{ translateX: pan.x },
{ translateY: pan.y },
],
}}
>
{/* Chart component */}
</Animated.View>
{/* Planet detail modal */}
{selectedPlanet && (
<PlanetDetailModal
planet={chartData.planets.find(p => p.planet === selectedPlanet)!}
onClose={() => setSelectedPlanet(null)}
/>
)}
</View>
);
}
Step 9: Export Functionality
Save charts as images or PDFs:
// services/chart-export.ts
import ViewShot from 'react-native-view-shot';
import * as Sharing from 'expo-sharing';
import * as FileSystem from 'expo-file-system';
export class ChartExportService {
async exportAsPNG(chartRef: any, fileName: string) {
try {
const uri = await ViewShot.captureRef(chartRef, {
format: 'png',
quality: 1.0,
});
await Sharing.shareAsync(uri, {
mimeType: 'image/png',
dialogTitle: 'Share Chart',
});
} catch (error) {
console.error('Export failed:', error);
}
}
async exportAsPDF(chartData: ChartData, style: ChartStyle) {
// Generate PDF with chart and planetary details
const html = this.generatePDFHTML(chartData, style);
// Convert to PDF using react-native-html-to-pdf
const { filePath } = await RNHTMLtoPDF.convert({
html,
fileName: 'kundli_chart',
directory: 'Documents',
});
await Sharing.shareAsync(filePath);
}
private generatePDFHTML(chartData: ChartData, style: ChartStyle): string {
return `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.chart { width: 600px; height: 600px; margin: 20px auto; }
.planets { margin: 20px; }
.planet-row { padding: 10px; border-bottom: 1px solid #ccc; }
</style>
</head>
<body>
<h1>Kundli Chart (${style.charAt(0).toUpperCase() + style.slice(1)} Indian Style)</h1>
<div class="chart">
<!-- SVG chart here -->
</div>
<div class="planets">
<h2>Planetary Positions</h2>
${chartData.planets.map(planet => `
<div class="planet-row">
<strong>${planet.planet}</strong>:
${planet.rashi} • House ${planet.house} • ${planet.nakshatra}
${planet.isRetrograde ? ' (Retrograde)' : ''}
</div>
`).join('')}
</div>
</body>
</html>
`;
}
}
Step 10: Production Considerations
1. Performance Optimization
// Memoize expensive calculations
const chartPath = useMemo(() => {
return generateChartPath(chartData, config);
}, [chartData, config]);
// Use React.memo for chart components
export const NorthIndianChart = React.memo(NorthIndianChartComponent);
2. Responsive Sizing
function useChartSize() {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const subscription = Dimensions.addEventListener('change', ({ window }) => {
const size = Math.min(window.width * 0.9, 500);
setDimensions({ width: size, height: size });
});
return () => subscription?.remove();
}, []);
return dimensions;
}
3. Accessibility
// Add accessibility labels
<Svg accessible={true} accessibilityLabel="Vedic Astrology Birth Chart">
{/* Chart elements */}
</Svg>
<TouchableOpacity
accessible={true}
accessibilityLabel={`${planet} in ${rashi}`}
accessibilityRole="button"
>
{/* Planet glyph */}
</TouchableOpacity>
Conclusion
You now have a production-ready Rashi chart visualizer supporting all three Indian chart styles. Users can view birth charts in their preferred regional format with accurate planetary positions from the API.
Key features implemented:
- North Indian diamond layout (Lagna at top)
- South Indian square grid layout (fixed rashi positions)
- East Indian rectangular layout
- Planetary glyphs with retrograde indicators (℞)
- Lagna (ascendant) highlighting
- Rashi-based planetary grouping (matches API structure)
- Interactive chart style switching
- Planetary position display with nakshatras and padas
- Longitude display in degrees
- Export to PNG
- Navamsa (D9) chart support
The API structure groups planets by rashi (zodiac sign), making rendering straightforward. Each chart style displays the same data with different visual conventions suited to regional preferences.
Ready to add chart visualization to your app? Sign up for RoxyAPI and get accurate birth chart data with proper rashi grouping. Full documentation at roxyapi.com/docs.