@eaprelsky/nocturna-wheel
Version:
A JavaScript library for rendering astrological natal charts
1,275 lines (1,125 loc) • 188 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.NocturnaWheel = {}));
})(this, (function (exports) { 'use strict';
/**
* ServiceRegistry.js
* A simple service locator/registry with all dependencies inlined.
*/
// Self-contained implementation
class ServiceRegistry {
// Private map to store service instances
static #instances = new Map();
/**
* Registers a service instance with the registry
* @param {string} key - Service identifier
* @param {Object} instance - Service instance
*/
static register(key, instance) {
this.#instances.set(key, instance);
}
/**
* Retrieves a service instance from the registry
* @param {string} key - Service identifier
* @returns {Object|undefined} The service instance, or undefined if not found
*/
static get(key) {
return this.#instances.get(key);
}
/**
* Checks if a service is registered
* @param {string} key - Service identifier
* @returns {boolean} True if the service is registered
*/
static has(key) {
return this.#instances.has(key);
}
/**
* Clears all registered services
* Useful for testing or reinitialization
*/
static clear() {
this.#instances.clear();
}
/**
* Gets or creates a basic SvgUtils-compatible instance
* @returns {Object} An object with SVG utility methods
*/
static getSvgUtils() {
if (!this.has('svgUtils')) {
// Create a simple SVG utilities object
const svgUtils = {
svgNS: "http://www.w3.org/2000/svg",
createSVGElement(tagName, attributes = {}) {
const element = document.createElementNS(this.svgNS, tagName);
for (const [key, value] of Object.entries(attributes)) {
element.setAttribute(key, value);
}
return element;
},
addTooltip(element, text) {
const title = document.createElementNS(this.svgNS, "title");
title.textContent = text;
element.appendChild(title);
return element;
},
pointOnCircle(centerX, centerY, radius, angle) {
const radians = (angle - 90) * (Math.PI / 180);
return {
x: centerX + radius * Math.cos(radians),
y: centerY + radius * Math.sin(radians)
};
}
};
this.register('svgUtils', svgUtils);
}
return this.get('svgUtils');
}
/**
* Gets or creates an IconProvider instance
* @param {string} basePath - Optional base path for SVG assets
* @returns {Object} The IconProvider instance
*/
static getIconProvider(basePath = './assets/svg/zodiac/') {
if (!this.has('iconProvider')) {
// Create a simple icon provider
const iconProvider = {
basePath: basePath,
getPlanetIconPath(planetName) {
return `${this.basePath}zodiac-planet-${planetName.toLowerCase()}.svg`;
},
getZodiacIconPath(signName) {
return `${this.basePath}zodiac-sign-${signName.toLowerCase()}.svg`;
},
getAspectIconPath(aspectType) {
return `${this.basePath}zodiac-aspect-${aspectType.toLowerCase()}.svg`;
},
createTextFallback(svgUtils, options, text) {
const { x, y, size = '16px', color = '#000000', className = 'icon-fallback' } = options;
const textElement = svgUtils.createSVGElement("text", {
x: x,
y: y,
'text-anchor': 'middle',
'dominant-baseline': 'middle',
'font-size': size,
'class': className,
'fill': color
});
textElement.textContent = text;
return textElement;
}
};
this.register('iconProvider', iconProvider);
}
return this.get('iconProvider');
}
/**
* Initializes all core services at once
* @param {Object} options - Initialization options
*/
static initializeServices(options = {}) {
// Initialize SvgUtils
this.getSvgUtils();
// Initialize IconProvider with the assets base path
this.getIconProvider(options.assetBasePath || './assets/svg/zodiac/');
console.log("ServiceRegistry: Core services initialized");
}
}
/**
* ChartConfig.js
* Configuration class for the natal chart rendering.
*/
class ChartConfig {
/**
* Creates a new configuration with default settings
* @param {Object} customConfig - Custom configuration to merge with defaults
*/
constructor(customConfig = {}) {
// Astronomical data - pure positional data without styling
this.astronomicalData = {
ascendant: 0, // Ascendant longitude in degrees
mc: 90, // Midheaven longitude in degrees
latitude: 51.5, // Default latitude (London)
houseSystem: "Placidus", // Default house system
planets: {
// Default planet positions
sun: 0,
moon: 0,
mercury: 0,
venus: 0,
mars: 0,
jupiter: 0,
saturn: 0,
uranus: 0,
neptune: 0,
pluto: 0
}
};
// Aspect settings
this.aspectSettings = {
enabled: true,
orb: 6, // Default orb for aspects
types: {
conjunction: { angle: 0, orb: 8, color: "#ff0000", enabled: true },
opposition: { angle: 180, orb: 8, color: "#0000ff", enabled: true },
trine: { angle: 120, orb: 6, color: "#00ff00", enabled: true },
square: { angle: 90, orb: 6, color: "#ff00ff", enabled: true },
sextile: { angle: 60, orb: 4, color: "#00ffff", enabled: true }
// Other aspects can be added here
}
};
// Planet settings
this.planetSettings = {
enabled: true,
primaryEnabled: true, // Toggle for primary (inner circle) planets
secondaryEnabled: true, // Toggle for secondary (innermost circle) planets
dotSize: 3, // Size of the position dot
iconSize: 24, // Size of the planet icon
orbs: {
// Default orbs for each planet
sun: 8,
moon: 8,
mercury: 6,
venus: 6,
mars: 6,
jupiter: 6,
saturn: 6,
uranus: 4,
neptune: 4,
pluto: 4
},
colors: {
// Default colors for each planet
sun: "#ff9900",
moon: "#aaaaaa",
mercury: "#3399cc",
venus: "#cc66cc",
mars: "#cc3333",
jupiter: "#9966cc",
saturn: "#336633",
uranus: "#33cccc",
neptune: "#3366ff",
pluto: "#663366"
},
visible: {
// Default visibility for each planet
sun: true,
moon: true,
mercury: true,
venus: true,
mars: true,
jupiter: true,
saturn: true,
uranus: true,
neptune: true,
pluto: true
}
};
// House settings - only UI related settings, no calculations
this.houseSettings = {
enabled: true,
lineColor: "#666666",
textColor: "#333333",
fontSize: 10,
rotationAngle: 0 // Custom rotation angle for house system
};
// Zodiac settings
this.zodiacSettings = {
enabled: true,
colors: {
aries: "#ff6666",
taurus: "#66cc66",
gemini: "#ffcc66",
cancer: "#6699cc",
leo: "#ff9900",
virgo: "#996633",
libra: "#6699ff",
scorpio: "#cc3366",
sagittarius: "#cc66ff",
capricorn: "#339966",
aquarius: "#3399ff",
pisces: "#9966cc"
},
fontSize: 10
};
// Radii for different chart layers
this.radius = {
innermost: 90, // Innermost circle (for dual charts, transits, synastry)
zodiacInner: 120, // Inner circle (aspect container)
zodiacMiddle: 150, // Middle circle (house boundaries)
zodiacOuter: 180, // Outer circle (zodiac ring)
planet: 105, // Default planet placement radius
aspectInner: 20, // Center space for aspects
aspectOuter: 120, // Outer boundary for aspects
houseNumberRadius: 210 // Radius for house numbers
};
// SVG settings
this.svg = {
width: 460,
height: 460,
viewBox: "0 0 460 460",
center: { x: 230, y: 230 }
};
// Assets settings
this.assets = {
basePath: "./assets/",
zodiacIconPath: "svg/zodiac/",
planetIconPath: "svg/zodiac/"
};
// Theme settings
this.theme = {
backgroundColor: "transparent",
textColor: "#333333",
lineColor: "#666666",
lightLineColor: "#cccccc",
fontFamily: "'Arial', sans-serif"
};
// House cusps cache - will be populated by HouseCalculator
this.houseCusps = [];
// Merge custom config with defaults (deep merge)
this.mergeConfig(customConfig);
// Initialize house cusps if we have enough data
this._initializeHouseCusps();
}
/**
* Merges custom configuration with the default configuration
* @param {Object} customConfig - Custom configuration object
*/
mergeConfig(customConfig) {
// Helper function for deep merge
const deepMerge = (target, source) => {
if (typeof source !== 'object' || source === null) {
return source;
}
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (typeof source[key] === 'object' && source[key] !== null &&
typeof target[key] === 'object' && target[key] !== null) {
// If both are objects, recurse
target[key] = deepMerge(target[key], source[key]);
} else {
// Otherwise just copy
target[key] = source[key];
}
}
}
return target;
};
// Apply deep merge to all top-level properties
for (const key in customConfig) {
if (Object.prototype.hasOwnProperty.call(customConfig, key) &&
Object.prototype.hasOwnProperty.call(this, key)) {
this[key] = deepMerge(this[key], customConfig[key]);
}
}
}
/**
* Initializes house cusps using the HouseCalculator
* @private
*/
_initializeHouseCusps() {
// Only calculate if we have the necessary data
if (typeof this.astronomicalData.ascendant === 'number' &&
typeof this.astronomicalData.mc === 'number') {
try {
// Create calculator instance
const houseCalculator = new HouseCalculator();
// Calculate house cusps using the current house system
this.houseCusps = houseCalculator.calculateHouseCusps(
this.astronomicalData.ascendant,
this.astronomicalData.houseSystem,
{
latitude: this.astronomicalData.latitude,
mc: this.astronomicalData.mc
}
);
} catch (error) {
console.error("Failed to calculate house cusps:", error);
// Set empty cusps array if calculation fails
this.houseCusps = [];
}
}
}
/**
* Gets settings for a specific planet
* @param {string} planetName - Name of the planet
* @returns {Object} - Planet settings
*/
getPlanetSettings(planetName) {
const planetLower = planetName.toLowerCase();
return {
color: this.planetSettings.colors[planetLower] || "#000000",
size: this.planetSettings.size,
visible: this.planetSettings.visible[planetLower] !== false
};
}
/**
* Gets settings for a specific aspect
* @param {string} aspectType - Type of aspect
* @returns {Object} - Aspect settings
*/
getAspectSettings(aspectType) {
const aspectLower = aspectType.toLowerCase();
return this.aspectSettings.types[aspectLower] ||
{ angle: 0, orb: this.aspectSettings.orb, color: "#999999", enabled: true };
}
/**
* Gets settings for a specific zodiac sign
* @param {string} signName - Name of the zodiac sign
* @returns {Object} - Zodiac sign settings
*/
getZodiacSettings(signName) {
const signLower = signName.toLowerCase();
return {
color: this.zodiacSettings.colors[signLower] || "#666666",
fontSize: this.zodiacSettings.fontSize
};
}
/**
* Updates aspect settings
* @param {Object} settings - New aspect settings
*/
updateAspectSettings(settings) {
this.aspectSettings = { ...this.aspectSettings, ...settings };
}
/**
* Updates planet settings
* @param {Object} settings - New planet settings
*/
updatePlanetSettings(settings) {
this.planetSettings = { ...this.planetSettings, ...settings };
}
/**
* Updates house settings (visual settings only)
* @param {Object} settings - New house settings
*/
updateHouseSettings(settings) {
this.houseSettings = { ...this.houseSettings, ...settings };
}
/**
* Updates zodiac settings
* @param {Object} settings - New zodiac settings
*/
updateZodiacSettings(settings) {
this.zodiacSettings = { ...this.zodiacSettings, ...settings };
}
/**
* Sets radius for a specific layer
* @param {string} layerName - Layer name
* @param {number} value - Radius value
*/
setRadius(layerName, value) {
if (Object.prototype.hasOwnProperty.call(this.radius, layerName)) {
this.radius[layerName] = value;
}
}
/**
* Toggles the visibility of a planet
* @param {string} planetName - Name of the planet
* @param {boolean} visible - Whether the planet should be visible
*/
togglePlanetVisibility(planetName, visible) {
if (this.planetSettings && this.planetSettings.visible) {
this.planetSettings.visible[planetName] = visible;
}
}
/**
* Toggles the visibility of houses
* @param {boolean} visible - Whether houses should be visible
*/
toggleHousesVisibility(visible) {
this.houseSettings.enabled = visible;
}
/**
* Toggles the visibility of aspects
* @param {boolean} visible - Whether aspects should be visible
*/
toggleAspectsVisibility(visible) {
this.aspectSettings.enabled = visible;
}
/**
* Toggles the visibility of primary planets (inner circle)
* @param {boolean} visible - Whether primary planets should be visible
*/
togglePrimaryPlanetsVisibility(visible) {
this.planetSettings.primaryEnabled = visible;
}
/**
* Toggles the visibility of secondary planets (innermost circle)
* @param {boolean} visible - Whether secondary planets should be visible
*/
toggleSecondaryPlanetsVisibility(visible) {
this.planetSettings.secondaryEnabled = visible;
}
/**
* Sets the current house system and recalculates house cusps
* @param {string} systemName - Name of the house system to use
* @returns {boolean} - Success status
*/
setHouseSystem(systemName) {
// Update the house system name
this.astronomicalData.houseSystem = systemName;
// Recalculate house cusps with new system
this._initializeHouseCusps();
return true;
}
/**
* Gets the current house cusps
* @returns {Array} - Array of house cusps
*/
getHouseCusps() {
// If we don't have house cusps data yet, calculate it
if (!this.houseCusps || this.houseCusps.length === 0) {
this._initializeHouseCusps();
}
// Ensure we're returning the correct format - convert to legacy format if needed
if (this.houseCusps.length > 0 && typeof this.houseCusps[0] === 'number') {
return this.houseCusps.map(longitude => ({ lon: longitude }));
}
return this.houseCusps;
}
/**
* Sets the Ascendant position and recalculates house cusps
* @param {number} ascendant - Ascendant longitude in degrees
* @returns {boolean} - Success status
*/
setAscendant(ascendant) {
if (typeof ascendant !== 'number' || ascendant < 0 || ascendant >= 360) {
return false;
}
this.astronomicalData.ascendant = ascendant;
this._initializeHouseCusps();
return true;
}
/**
* Sets the Midheaven position and recalculates house cusps
* @param {number} mc - Midheaven longitude in degrees
* @returns {boolean} - Success status
*/
setMidheaven(mc) {
if (typeof mc !== 'number' || mc < 0 || mc >= 360) {
return false;
}
this.astronomicalData.mc = mc;
this._initializeHouseCusps();
return true;
}
/**
* Sets the geographic latitude and recalculates house cusps
* @param {number} latitude - Geographic latitude in degrees
* @returns {boolean} - Success status
*/
setLatitude(latitude) {
if (typeof latitude !== 'number' || latitude < -90 || latitude > 90) {
return false;
}
this.astronomicalData.latitude = latitude;
this._initializeHouseCusps();
return true;
}
/**
* Sets a planet's position
* @param {string} planetName - Name of the planet
* @param {number} longitude - Longitude in degrees
* @returns {boolean} - Success status
*/
setPlanetPosition(planetName, longitude) {
const planetLower = planetName.toLowerCase();
if (typeof longitude !== 'number' || longitude < 0 || longitude >= 360) {
return false;
}
if (this.astronomicalData.planets.hasOwnProperty(planetLower)) {
this.astronomicalData.planets[planetLower] = longitude;
return true;
}
return false;
}
/**
* Gets a planet's position
* @param {string} planetName - Name of the planet
* @returns {number|null} - Planet longitude or null if not found
*/
getPlanetPosition(planetName) {
const planetLower = planetName.toLowerCase();
if (this.astronomicalData.planets.hasOwnProperty(planetLower)) {
return this.astronomicalData.planets[planetLower];
}
return null;
}
/**
* Gets the current house system
* @returns {string} - Name of the current house system
*/
getHouseSystem() {
return this.astronomicalData.houseSystem;
}
/**
* Gets the available house systems by creating a temporary calculator
* @returns {Array} - Array of available house system names
*/
getAvailableHouseSystems() {
const calculator = new HouseCalculator();
return calculator.getAvailableHouseSystems();
}
}
/**
* SvgUtils.js
* Utility class for working with SVG elements
*/
class SvgUtils {
constructor() {
this.svgNS = "http://www.w3.org/2000/svg";
}
/**
* Creates an SVG element with the specified tag
* @param {string} tagName - Name of the SVG tag
* @param {Object} attributes - Object with attributes to set
* @returns {Element} Created SVG element
*/
createSVGElement(tagName, attributes = {}) {
const element = document.createElementNS(this.svgNS, tagName);
for (const [key, value] of Object.entries(attributes)) {
element.setAttribute(key, value);
}
return element;
}
/**
* Adds a tooltip (title) to an SVG element
* @param {Element} element - SVG element
* @param {string} text - Tooltip text
* @returns {Element} The element with tooltip
*/
addTooltip(element, text) {
const title = document.createElementNS(this.svgNS, "title");
title.textContent = text;
element.appendChild(title);
return element;
}
/**
* Calculates coordinates of a point on a circle
* @param {number} centerX - X coordinate of the center
* @param {number} centerY - Y coordinate of the center
* @param {number} radius - Circle radius
* @param {number} angle - Angle in degrees
* @returns {Object} Object with x and y coordinates
*/
pointOnCircle(centerX, centerY, radius, angle) {
// Convert angle to radians (accounting for 0 starting at the top)
const radians = (angle - 90) * (Math.PI / 180);
return {
x: centerX + radius * Math.cos(radians),
y: centerY + radius * Math.sin(radians)
};
}
}
/**
* SVGManager.js
* Handles the creation, management, and querying of the main SVG element and its layer groups.
*/
class SVGManager {
/**
* Constructor
* @param {Object} options - Manager options
* @param {SvgUtils} [options.svgUtils] - SvgUtils instance (optional, will use from registry if not provided)
*/
constructor(options = {}) {
this.svgNS = "http://www.w3.org/2000/svg";
this.svg = null; // Reference to the main SVG element
this.groups = {}; // References to the layer groups (g elements)
// Use injected svgUtils or get from registry
this.svgUtils = options.svgUtils || ServiceRegistry.getSvgUtils();
// Define standard group order (bottom to top)
this.groupOrder = [
'zodiac',
'houseDivisions',
'aspects', // Render aspects below planets and houses
'primaryPlanets', // Inner circle planets
'secondaryPlanets', // Innermost circle planets
'houses' // House numbers on top
// Add other groups if needed, e.g., 'tooltips'
];
}
/**
* Initializes the main SVG element within a container.
* @param {string|Element} containerSelector - ID/CSS selector of the container element or the element itself.
* @param {Object} options - SVG attributes (e.g., viewBox, width, height, class, preserveAspectRatio).
* @returns {SVGElement | null} The created SVG element or null if container not found.
*/
initialize(containerSelector, options = {}) {
// Handle both string selectors and DOM elements
let container;
if (typeof containerSelector === 'string') {
container = document.querySelector(containerSelector);
if (!container) {
console.error(`SVGManager: Container not found with selector: ${containerSelector}`);
return null;
}
} else if (containerSelector instanceof Element || containerSelector instanceof HTMLElement) {
container = containerSelector;
} else {
console.error(`SVGManager: Invalid container. Expected string selector or DOM element.`);
return null;
}
// Clear container
container.innerHTML = '';
// Default options
const defaultOptions = {
width: "100%",
height: "100%",
viewBox: "0 0 460 460", // Default size, should match ChartConfig ideally
preserveAspectRatio: "xMidYMid meet",
class: "nocturna-wheel-svg" // Default class
};
const svgOptions = { ...defaultOptions, ...options };
// Create SVG element
this.svg = document.createElementNS(this.svgNS, "svg");
// Set attributes
for (const [key, value] of Object.entries(svgOptions)) {
this.svg.setAttribute(key, value);
}
// Append to container
container.appendChild(this.svg);
console.log("SVGManager: SVG initialized");
return this.svg;
}
/**
* Creates the standard layer groups within the SVG in the predefined order.
*/
createStandardGroups() {
if (!this.svg) {
console.error("SVGManager: Cannot create groups, SVG not initialized.");
return;
}
// Clear existing groups before creating new ones (or check if they exist)
this.groups = {};
// Remove existing group elements from SVG if any
this.svg.querySelectorAll('g').forEach(g => g.remove());
console.log("SVGManager: Creating standard groups:", this.groupOrder);
this.groupOrder.forEach(groupName => {
this.createGroup(groupName);
});
// Create a legacy 'planets' group for backward compatibility
// This will be deprecated in future versions
this.createGroup('planets');
}
/**
* Creates a named group (<g>) element and appends it to the SVG.
* If the group already exists, it returns the existing group.
* @param {string} name - The name (and ID) for the group.
* @returns {SVGElement | null} The created or existing group element, or null if SVG not initialized.
*/
createGroup(name) {
if (!this.svg) {
console.error(`SVGManager: Cannot create group '${name}', SVG not initialized.`);
return null;
}
if (this.groups[name]) {
return this.groups[name]; // Return existing group
}
const group = document.createElementNS(this.svgNS, "g");
group.setAttribute("id", `group-${name}`); // Set ID for easy debugging/selection
group.setAttribute("class", `svg-group svg-group-${name}`); // Add class
this.svg.appendChild(group); // Append to SVG (order matters based on creation sequence)
this.groups[name] = group;
// console.log(`SVGManager: Created group '${name}'`);
return group;
}
/**
* Retrieves a previously created group element by name.
* @param {string} name - The name of the group.
* @returns {SVGElement | null} The group element or null if not found or not initialized.
*/
getGroup(name) {
if (!this.svg) {
console.warn(`SVGManager: Cannot get group '${name}', SVG not initialized.`);
return null;
}
// For backward compatibility, map 'planets' to 'primaryPlanets'
if (name === 'planets') {
console.warn('SVGManager: Using deprecated "planets" group. Use "primaryPlanets" or "secondaryPlanets" instead.');
name = 'primaryPlanets';
}
if (!this.groups[name]) {
console.warn(`SVGManager: Group '${name}' not found. Creating it.`);
// Attempt to create if missing, might indicate an issue elsewhere
return this.createGroup(name);
}
return this.groups[name];
}
/**
* Retrieves all created group elements.
* @returns {Object<string, SVGElement>} An object mapping group names to their SVGElement references.
*/
getAllGroups() {
return this.groups;
}
/**
* Returns the main SVG element.
* @returns {SVGElement | null} The main SVG element or null if not initialized.
*/
getSVG() {
return this.svg;
}
} // End of SVGManager class
/**
* BaseRenderer.js
* Self-contained base class for all renderers.
* This version has all dependencies inlined to avoid import issues.
*/
// Define a completely self-contained renderer base class
class BaseRenderer {
/**
* Constructor
* @param {Object} options - Renderer options.
* @param {string} options.svgNS - SVG namespace.
* @param {Object} options.config - Chart configuration object.
* @param {Object} options.svgUtils - SVG utility service.
*/
constructor(options) {
if (!options || !options.svgNS || !options.config) {
throw new Error(`${this.constructor.name}: Missing required options (svgNS, config)`);
}
// Store base options
this.svgNS = options.svgNS;
this.config = options.config;
this.options = options;
// Use the provided svgUtils or create minimal internal SVG utilities
this.svgUtils = options.svgUtils || {
// Store SVG namespace
svgNS: this.svgNS,
// Create SVG element
createSVGElement: (tagName, attributes = {}) => {
const element = document.createElementNS(this.svgNS, tagName);
for (const [key, value] of Object.entries(attributes)) {
element.setAttribute(key, value);
}
return element;
},
// Add tooltip to element
addTooltip: (element, text) => {
const title = document.createElementNS(this.svgNS, "title");
title.textContent = text;
element.appendChild(title);
return element;
},
// Calculate point on circle
pointOnCircle: (centerX, centerY, radius, angle) => {
const radians = (angle - 90) * (Math.PI / 180);
return {
x: centerX + radius * Math.cos(radians),
y: centerY + radius * Math.sin(radians)
};
}
};
// Get dimensions from config
this.centerX = this.config.svg.center.x;
this.centerY = this.config.svg.center.y;
this.innerRadius = this.config.radius.zodiacInner;
this.middleRadius = this.config.radius.zodiacMiddle;
this.outerRadius = this.config.radius.zodiacOuter;
}
/**
* Clears all child nodes of the given parent SVG group.
* @param {Element} parentGroup - The SVG group to clear.
*/
clearGroup(parentGroup) {
if (parentGroup) {
parentGroup.innerHTML = '';
}
}
/**
* Abstract render method to be implemented by subclasses.
* @param {Element} parentGroup - The parent SVG group element.
* @returns {Array} Array of rendered SVG elements.
*/
render(parentGroup) {
throw new Error(`${this.constructor.name}: render() not implemented.`);
}
}
/**
* AstrologyUtils.js
* Utility class for astrological calculations
*/
class AstrologyUtils {
/**
* Capitalizes the first letter of a string
* @param {string} string - Input string
* @returns {string} String with capitalized first letter
*/
static capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
/**
* Determines the house based on position and rotation angle
* @param {number} position - Position in degrees (0-359)
* @param {number} rotationAngle - Rotation angle of the house system
* @returns {number} House number (1-12)
*/
static getHouseFromPosition(position, rotationAngle = 0) {
// Adjust position relative to house system rotation
const adjustedPosition = (position - rotationAngle + 360) % 360;
// Determine house
return Math.floor(adjustedPosition / 30) + 1;
}
/**
* Returns the list of zodiac sign names
* @returns {Array} Array of zodiac sign names
*/
static getZodiacSigns() {
return [
"aries", "taurus", "gemini", "cancer", "leo", "virgo",
"libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces"
];
}
/**
* Returns the available house systems with descriptions
* @returns {Object} Object mapping house system names to their descriptions
*/
static getHouseSystems() {
return {
"Placidus": "Most commonly used house system in Western astrology, based on time of day",
"Koch": "Developed by Walter Koch, a time-based system similar to Placidus",
"Campanus": "Medieval system dividing the prime vertical into equal parts",
"Regiomontanus": "Similar to Campanus, but using the celestial equator instead of prime vertical",
"Equal": "Divides the ecliptic into 12 equal segments of 30° each from the Ascendant",
"Whole Sign": "Assigns the entire rising sign to the 1st house, with subsequent signs as houses",
"Porphyry": "Simple system that divides each quadrant into three equal parts",
"Topocentric": "Modern system similar to Placidus but more accurate for extreme latitudes"
};
}
/**
* Returns the list of planets
* @returns {Array} Array of planet names
*/
static getPlanets() {
return [
"sun", "moon", "mercury", "venus", "mars",
"jupiter", "saturn", "uranus", "neptune", "pluto"
];
}
/**
* Converts a house number to a Roman numeral
* @param {number} house - House number (1-12)
* @returns {string} Roman numeral
*/
static houseToRoman(house) {
const romanNumerals = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
return romanNumerals[house - 1] || '';
}
/**
* Returns the full name of a planet in the specified language
* @param {string} planetCode - Planet code (sun, moon, etc.)
* @param {string} language - Language code (default: 'en')
* @returns {string} Full planet name
*/
static getPlanetFullName(planetCode, language = 'en') {
const planetNames = {
en: {
"sun": "Sun",
"moon": "Moon",
"mercury": "Mercury",
"venus": "Venus",
"mars": "Mars",
"jupiter": "Jupiter",
"saturn": "Saturn",
"uranus": "Uranus",
"neptune": "Neptune",
"pluto": "Pluto"
},
ru: {
"sun": "Солнце",
"moon": "Луна",
"mercury": "Меркурий",
"venus": "Венера",
"mars": "Марс",
"jupiter": "Юпитер",
"saturn": "Сатурн",
"uranus": "Уран",
"neptune": "Нептун",
"pluto": "Плутон"
}
};
const names = planetNames[language] || planetNames.en;
return names[planetCode.toLowerCase()] || this.capitalizeFirstLetter(planetCode);
}
/**
* Returns the full name of a zodiac sign in the specified language
* @param {string} signCode - Zodiac sign code (aries, taurus, etc.)
* @param {string} language - Language code (default: 'en')
* @returns {string} Full zodiac sign name
*/
static getZodiacSignFullName(signCode, language = 'en') {
const signNames = {
en: {
"aries": "Aries",
"taurus": "Taurus",
"gemini": "Gemini",
"cancer": "Cancer",
"leo": "Leo",
"virgo": "Virgo",
"libra": "Libra",
"scorpio": "Scorpio",
"sagittarius": "Sagittarius",
"capricorn": "Capricorn",
"aquarius": "Aquarius",
"pisces": "Pisces"
},
ru: {
"aries": "Овен",
"taurus": "Телец",
"gemini": "Близнецы",
"cancer": "Рак",
"leo": "Лев",
"virgo": "Дева",
"libra": "Весы",
"scorpio": "Скорпион",
"sagittarius": "Стрелец",
"capricorn": "Козерог",
"aquarius": "Водолей",
"pisces": "Рыбы"
}
};
const names = signNames[language] || signNames.en;
return names[signCode.toLowerCase()] || this.capitalizeFirstLetter(signCode);
}
}
/**
* ZodiacRenderer.js
* Class for rendering the zodiac circle and signs.
*/
class ZodiacRenderer extends BaseRenderer {
/**
* Constructor
* @param {Object} options - Renderer options.
* @param {string} options.svgNS - SVG namespace.
* @param {ChartConfig} options.config - Chart configuration object.
* @param {string} options.assetBasePath - Base path for assets.
* @param {IconProvider} [options.iconProvider] - Icon provider service.
*/
constructor(options) {
super(options);
if (!options.assetBasePath) {
throw new Error("ZodiacRenderer: Missing required option assetBasePath");
}
this.iconProvider = options.iconProvider; // Store the icon provider
this.signIconRadius = (this.outerRadius + this.middleRadius) / 2;
this.signIconSize = 30;
}
/**
* Renders the zodiac wheel components.
* @param {Element} parentGroup - The parent SVG group element.
* @returns {Array} Array of rendered SVG elements (or empty array).
*/
render(parentGroup) {
if (!parentGroup) {
console.error("ZodiacRenderer: parentGroup is null or undefined.");
return [];
}
this.clearGroup(parentGroup);
const renderedElements = [];
renderedElements.push(...this.renderBaseCircles(parentGroup));
renderedElements.push(...this.renderDivisionLines(parentGroup));
renderedElements.push(...this.renderZodiacSigns(parentGroup));
console.log("ZodiacRenderer: Rendering complete.");
return renderedElements;
}
/**
* Renders the base circles for the chart layout.
* @param {Element} parentGroup - The parent SVG group.
* @returns {Array<Element>} Array containing the created circle elements.
*/
renderBaseCircles(parentGroup) {
const elements = [];
const circles = [
{ r: this.outerRadius, class: "chart-outer-circle" },
{ r: this.middleRadius, class: "chart-middle-circle" },
{ r: this.innerRadius, class: "chart-inner-circle" }
];
circles.forEach(circleData => {
const circle = this.svgUtils.createSVGElement("circle", {
cx: this.centerX,
cy: this.centerY,
r: circleData.r,
class: `zodiac-element ${circleData.class}` // Add base class
});
parentGroup.appendChild(circle);
elements.push(circle);
});
return elements;
}
/**
* Renders the division lines between zodiac signs.
* @param {Element} parentGroup - The parent SVG group.
* @returns {Array<Element>} Array containing the created line elements.
*/
renderDivisionLines(parentGroup) {
const elements = [];
for (let i = 0; i < 12; i++) {
const angle = i * 30; // 30 degrees per sign
// Lines span from the inner zodiac ring to the outer zodiac ring
const point1 = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.middleRadius, angle);
const point2 = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.outerRadius, angle);
// Instead of skipping lines, use special classes for cardinal points
let specialClass = "";
if (angle === 0) specialClass = "aries-point"; // Aries point (0°)
else if (angle === 90) specialClass = "cancer-point"; // Cancer point (90°)
else if (angle === 180) specialClass = "libra-point"; // Libra point (180°)
else if (angle === 270) specialClass = "capricorn-point"; // Capricorn point (270°)
const line = this.svgUtils.createSVGElement("line", {
x1: point1.x,
y1: point1.y,
x2: point2.x,
y2: point2.y,
class: `zodiac-element zodiac-division-line ${specialClass}`
});
parentGroup.appendChild(line);
elements.push(line);
}
return elements;
}
/**
* Renders the zodiac sign icons.
* @param {Element} parentGroup - The parent SVG group.
* @returns {Array<Element>} Array containing the created image elements.
*/
renderZodiacSigns(parentGroup) {
const elements = [];
const zodiacSigns = AstrologyUtils.getZodiacSigns(); // Assumes AstrologyUtils is available
for (let i = 0; i < 12; i++) {
const signName = zodiacSigns[i];
// Place icon in the middle of the sign's 30-degree sector
const angle = i * 30 + 15;
// Calculate position for the icon center
const point = this.svgUtils.pointOnCircle(this.centerX, this.centerY, this.signIconRadius, angle);
// Get icon path using IconProvider if available
let iconHref;
if (this.iconProvider) {
iconHref = this.iconProvider.getZodiacIconPath(signName);
} else {
// Fallback to old path construction
iconHref = `${this.options.assetBasePath}svg/zodiac/zodiac-sign-${signName}.svg`;
}
console.log(`Loading zodiac sign: ${iconHref}`);
const icon = this.svgUtils.createSVGElement("image", {
x: point.x - this.signIconSize / 2, // Offset to center the icon
y: point.y - this.signIconSize / 2,
width: this.signIconSize,
height: this.signIconSize,
href: iconHref,
class: `zodiac-element zodiac-sign zodiac-sign-${signName}` // Add base and specific class
});
// Fallback for missing icons
icon.addEventListener('error', () => {
console.warn(`Zodiac sign icon not found: ${iconHref}`);
icon.setAttribute('href', ''); // Remove broken link
// Create a text fallback
const fallbackText = signName.substring(0, 3).toUpperCase();
// Use IconProvider's createTextFallback if available
let textElement;
if (this.iconProvider) {
textElement = this.iconProvider.createTextFallback(
this.svgUtils,
{
x: point.x,
y: point.y,