├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── symbol_request.md └── workflows │ ├── checks.yaml │ └── npm-publish.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── dist ├── astrochart.js └── project │ └── src │ ├── animation │ ├── animator.d.ts │ └── timer.d.ts │ ├── aspect.d.ts │ ├── chart.d.ts │ ├── index.d.ts │ ├── radix.d.ts │ ├── settings.d.ts │ ├── svg.d.ts │ ├── transit.d.ts │ ├── utils.d.ts │ └── zodiac.d.ts ├── doc └── images │ └── transits.png ├── jest.config.js ├── package-lock.json ├── package.json ├── project ├── __tests__ │ ├── __snapshots__ │ │ └── radix.test.ts.snap │ └── radix.test.ts ├── examples │ ├── 2ChartsOnPage │ │ └── 2radix.html │ ├── debug │ │ └── calibration.html │ ├── radix │ │ ├── radix.html │ │ ├── radix_2016_11_15.html │ │ ├── radix_collision.html │ │ └── radix_custom_settings.html │ └── transit │ │ ├── animate.html │ │ ├── stroke_only.html │ │ └── transit.html └── src │ ├── animation │ ├── animator.ts │ └── timer.ts │ ├── aspect.test.ts │ ├── aspect.ts │ ├── chart.test.ts │ ├── chart.ts │ ├── index.ts │ ├── radix.test.ts │ ├── radix.ts │ ├── settings.ts │ ├── svg.test.ts │ ├── svg.ts │ ├── transit.ts │ ├── utils.test.ts │ ├── utils.ts │ ├── zodiac.test.ts │ └── zodiac.ts ├── tsconfig.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.test.ts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'commonjs': true, 5 | 'es6': true 6 | }, 7 | 'extends': [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/eslint-recommended' 10 | ], 11 | 'globals': { 12 | 'Atomics': 'readonly', 13 | 'SharedArrayBuffer': 'readonly' 14 | }, 15 | 'parser': '@typescript-eslint/parser', 16 | 'parserOptions': { 17 | 'sourceType': 'module', 18 | 'ecmaVersion': 2018 19 | }, 20 | 'plugins': [ 21 | '@typescript-eslint' 22 | ], 23 | 'rules': { 24 | 'indent': [ 25 | 'error', 26 | 2, 27 | { 'SwitchCase': 1 } 28 | ], 29 | 'linebreak-style': [ 30 | 'error', 31 | 'unix' 32 | ], 33 | 'quotes': [ 34 | 'error', 35 | 'single', 36 | { 'allowTemplateLiterals': true } 37 | ], 38 | 'semi': [ 39 | 'error', 40 | 'never' 41 | ], 42 | 'no-var': ['error'], 43 | '@typescript-eslint/member-delimiter-style': [ 44 | 'error', 45 | { 46 | 'multiline': { 47 | 'delimiter': 'none', 48 | 'requireLast': true 49 | }, 50 | 'singleline': { 51 | 'delimiter': 'semi', 52 | 'requireLast': false 53 | } 54 | } 55 | ], 56 | '@typescript-eslint/explicit-function-return-type': 'off', 57 | '@typescript-eslint/no-unused-vars': ['warn', { 'vars': 'all', 'args': 'after-used', 'ignoreRestSiblings': false }], 58 | 'no-unused-vars': 'off', 59 | '@typescript-eslint/restrict-plus-operands': 'off', 60 | //'@typescript-eslint/no-unused-vars': 'off', 61 | '@typescript-eslint/prefer-optional-chain': 'off', 62 | '@typescript-eslint/naming-convention': 'off', 63 | '@typescript-eslint/strict-boolean-expressions': 'off', 64 | '@typescript-eslint/prefer-nullish-coalescing': 'off', 65 | 'no-prototype-builtins': 'off' 66 | }, 67 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: afucher 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional info (please complete the following information):** 27 | - Browser [e.g. chrome, safari] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/symbol_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Symbol request 3 | about: Suggest a new symbol to be supported 4 | title: '' 5 | labels: symbol request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What is the symbol that you need?** 11 | The name of the symbol/point, and if it is not a common planet/point, please describe why you need and the use of that. 12 | 13 | **Can you contribute to this request?** 14 | [ ] Yes, I have the svg and will add it to this issue. 15 | [ ] Yes, I will open a PR with the implementation. 16 | 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the symbol request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/checks.yaml: -------------------------------------------------------------------------------- 1 | name: checks 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 16 16 | - run: npm ci 17 | - run: npm run lint 18 | 19 | test: 20 | runs-on: ubuntu-22.04 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 16 26 | - run: npm ci 27 | - run: npm run test:coverage 28 | 29 | build: # sanity check that build does not throw errors 30 | runs-on: ubuntu-22.04 31 | steps: 32 | - uses: actions/checkout@v3 33 | - uses: actions/setup-node@v3 34 | with: 35 | node-version: 16 36 | - run: npm ci 37 | - run: npm run build 38 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 16 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings/* 2 | /node_modules/* 3 | .project 4 | .DS_Store 5 | build/ 6 | coverage/ 7 | 8 | 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at arthurfucher@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2018 Matheus Alves 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | 7 | of this software and associated documentation files (the "Software"), to deal 8 | 9 | in the Software without restriction, including without limitation the rights 10 | 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | 13 | copies of the Software, and to permit persons to whom the Software is 14 | 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | 33 | SOFTWARE. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AstroChart 2 | 3 | ![GitHub release](https://img.shields.io/github/v/release/AstroDraw/AstroChart?style=flat-square) 4 | 5 | A free and open-source TypeScript library for generating SVG charts to display planets in astrology. It does not calculate any positions of the planets in Universe. 6 | 7 | - Pure TypeScript implementation without dependencies. 8 | - SVG graphics. 9 | - Tested code. 10 | 11 | ## Example: 12 | 13 | Chart wheel with transits 14 | 15 | ## Documentation 16 | A documentation is in progress, please checkout [website](https://astrodraw.github.io/). 17 | 18 | ## Contribution 19 | Contribution is always welcome. You can contribute in different ways: 20 | - Start or participate in the [discussions](https://github.com/AstroDraw/AstroChart/discussions) 21 | - Check opened issues, or improve our documentation 22 | - Open [an issue](https://github.com/AstroDraw/AstroChart/issues) to report a bug or give some enchancement idea 23 | - Open a PR with bug fixes or new features. To avoid rework, if is not small, is always good to open an issue to discuss before. 24 | 25 | ## Support 26 | Do you want to support the development of AstroChart? Here is some ways: 27 | 28 | Is your project using? Please [comment here](https://github.com/AstroDraw/AstroChart/discussions/48) so we can share nice projects that are using. 29 | 30 | A nice way to support is sharing this project with other people. 31 | 32 | Also, if you are a company consider sponsoring the project or [buying me a coffee](https://ko-fi.com/afucher) 33 | -------------------------------------------------------------------------------- /dist/project/src/animation/animator.d.ts: -------------------------------------------------------------------------------- 1 | import type { AstroData } from '../radix'; 2 | import type { Settings } from '../settings'; 3 | import type Transit from '../transit'; 4 | import Timer from './timer'; 5 | /** 6 | * Transit chart animator 7 | * 8 | * Animates the object on a circle. 9 | * 10 | * @class 11 | * @public 12 | * @constructor 13 | * @param {Object} from, {"Sun":[12], "Moon":[60]} 14 | * @param {Object} to, {"Sun":[30], "Moon":[180]} 15 | * @param {Object} settings, {cx:100, cy:100, radius:200, prefix:"astro-chart-"} 16 | */ 17 | declare class Animator { 18 | transit: Transit; 19 | isReverse: boolean; 20 | rotation: number; 21 | settings: Settings; 22 | actualPlanetPos: any; 23 | timer: Timer; 24 | timeSinceLoopStart: number; 25 | context: this; 26 | cuspsElement: any; 27 | data: AstroData; 28 | duration: number; 29 | callback: () => void; 30 | constructor(transit: Transit, settings: Settings); 31 | /** 32 | * Animate objects 33 | 34 | * @param {Object} data, targetPositions 35 | * @param {Integer} duration - seconds 36 | * @param {boolean} isReverse 37 | * @param {Function} callbck - start et the end of animation 38 | */ 39 | animate(data: any, duration: number, isReverse: boolean, callback: () => void): void; 40 | update(deltaTime?: number): void; 41 | updateCusps(expectedNumberOfLoops: number): void; 42 | updatePlanets(expectedNumberOfLoops: number): void; 43 | } 44 | export default Animator; 45 | -------------------------------------------------------------------------------- /dist/project/src/animation/timer.d.ts: -------------------------------------------------------------------------------- 1 | declare class Timer { 2 | debug: boolean; 3 | callback: (delta: number) => void; 4 | boundTick_: FrameRequestCallback; 5 | lastGameLoopFrame: number; 6 | requestID_: number | undefined; 7 | constructor(callback: (delta: number) => void, debug: boolean); 8 | start(): void; 9 | stop(): void; 10 | isRunning(): boolean; 11 | tick(): void; 12 | } 13 | export default Timer; 14 | -------------------------------------------------------------------------------- /dist/project/src/aspect.d.ts: -------------------------------------------------------------------------------- 1 | import type { Points } from './radix'; 2 | import type { AspectData, Settings } from './settings'; 3 | export interface FormedAspect { 4 | point: { 5 | name: string; 6 | position: number; 7 | }; 8 | toPoint: { 9 | name: string; 10 | position: number; 11 | }; 12 | aspect: { 13 | name: string; 14 | degree: number; 15 | color: string; 16 | orbit: number; 17 | }; 18 | precision: string; 19 | } 20 | /** 21 | * Aspects calculator 22 | * 23 | * @class 24 | * @public 25 | * @constructor 26 | * @param {AspectPoints} points; {"Sun":[0], "Moon":[90], "Neptune":[120], "As":[30]} 27 | * @param {Object | null } settings 28 | */ 29 | declare class AspectCalculator { 30 | settings: Partial; 31 | toPoints: Points; 32 | context: this; 33 | constructor(toPoints: Points, settings?: Partial); 34 | /** 35 | * Getter for this.toPoints 36 | * @see constructor 37 | * 38 | * @return {Object} 39 | */ 40 | getToPoints(): Points; 41 | /** 42 | * Radix aspects 43 | * 44 | * In radix calculation is the param "points" the same as param "toPoints" in constructor 45 | * , but without special points such as: As,Ds, Mc, Ic, ... 46 | * 47 | * @param {Object} points; {"Sun":[0], "Moon":[90]} 48 | * 49 | * @return {Array} [{"aspect":{"name":"conjunction", "degree":120}"", "point":{"name":"Sun", "position":123}, "toPoint":{"name":"Moon", "position":345}, "precision":0.5}]] 50 | */ 51 | radix(points: Points): FormedAspect[]; 52 | /** 53 | * Transit aspects 54 | * 55 | * @param {Object} points - transiting points; {"Sun":[0, 1], "Uranus":[90, -1], "NAME":[ANGLE, SPEED]}; 56 | * @return {Array} [{"aspect":{"name":"conjunction", "degree":120}"", "point":{"name":"Sun", "position":123}, "toPoint":{"name":"Moon", "position":345}, "precision":0.5}]] 57 | */ 58 | transit(points: Points): FormedAspect[]; 59 | hasAspect(point: number, toPoint: number, aspect: AspectData): boolean; 60 | calcPrecision(point: number, toPoint: number, aspect: number): number; 61 | isTransitPointApproachingToAspect(aspect: number, toPoint: number, point: number): boolean; 62 | compareAspectsByPrecision(a: FormedAspect, b: FormedAspect): number; 63 | } 64 | export default AspectCalculator; 65 | -------------------------------------------------------------------------------- /dist/project/src/chart.d.ts: -------------------------------------------------------------------------------- 1 | import type { Settings } from './settings'; 2 | import Radix from './radix'; 3 | import type { AstroData } from './radix'; 4 | import SVG from './svg'; 5 | /** 6 | * Displays astrology charts. 7 | * 8 | * @class 9 | * @public 10 | * @constructor 11 | * @param {String} elementId - root DOMElement 12 | * @param {int} width 13 | * @param {int} height 14 | * @param {Object} settings 15 | */ 16 | declare class Chart { 17 | paper: SVG; 18 | cx: number; 19 | cy: number; 20 | radius: number; 21 | settings: Settings; 22 | constructor(elementId: string, width: number, height: number, settings?: Partial); 23 | /** 24 | * Display radix horoscope 25 | * 26 | * @param {Object} data 27 | * @example 28 | * { 29 | * "points":{"Moon":[0], "Sun":[30], ... }, 30 | * "cusps":[300, 340, 30, 60, 75, 90, 116, 172, 210, 236, 250, 274] 31 | * } 32 | * 33 | * @return {Radix} radix 34 | */ 35 | radix(data: AstroData): Radix; 36 | /** 37 | * Scale chart 38 | * 39 | * @param {int} factor 40 | */ 41 | scale(factor: number): void; 42 | /** 43 | * Draw the symbol on the axis. 44 | * For debug only. 45 | * 46 | */ 47 | calibrate(): Chart; 48 | } 49 | export default Chart; 50 | -------------------------------------------------------------------------------- /dist/project/src/index.d.ts: -------------------------------------------------------------------------------- 1 | import Chart from './chart'; 2 | import AspectCalculator from './aspect'; 3 | import { Settings } from './settings'; 4 | export { Chart, AspectCalculator, Settings }; 5 | export default Chart; 6 | -------------------------------------------------------------------------------- /dist/project/src/radix.d.ts: -------------------------------------------------------------------------------- 1 | import type { FormedAspect } from './aspect'; 2 | import Transit from './transit'; 3 | import type SVG from './svg'; 4 | import type { Settings } from './settings'; 5 | export type Points = Record; 6 | export interface LocatedPoint { 7 | name: string; 8 | x: number; 9 | y: number; 10 | r: number; 11 | angle: number; 12 | pointer?: number; 13 | index?: number; 14 | } 15 | export interface AstroData { 16 | planets: Points; 17 | cusps: number[]; 18 | } 19 | /** 20 | * Radix charts. 21 | * 22 | * @class 23 | * @public 24 | * @constructor 25 | * @param {this.settings.SVG} paper 26 | * @param {int} cx 27 | * @param {int} cy 28 | * @param {int} radius 29 | * @param {Object} data 30 | */ 31 | declare class Radix { 32 | settings: Settings; 33 | data: AstroData; 34 | paper: SVG; 35 | cx: number; 36 | cy: number; 37 | radius: number; 38 | locatedPoints: LocatedPoint[]; 39 | rulerRadius: number; 40 | pointRadius: number; 41 | toPoints: Points; 42 | shift: number; 43 | universe: Element; 44 | context: this; 45 | constructor(paper: SVG, cx: number, cy: number, radius: number, data: AstroData, settings: Settings); 46 | /** 47 | * Draw background 48 | */ 49 | drawBg(): void; 50 | /** 51 | * Draw universe. 52 | */ 53 | drawUniverse(): void; 54 | /** 55 | * Draw points 56 | */ 57 | drawPoints(): void; 58 | drawAxis(): void; 59 | /** 60 | * Draw cusps 61 | */ 62 | drawCusps(): void; 63 | /** 64 | * Draw aspects 65 | * @param{Array | null} customAspects - posible custom aspects to draw; 66 | */ 67 | aspects(customAspects?: FormedAspect[] | null): Radix; 68 | /** 69 | * Add points of interest for aspects calculation 70 | * @param {Obect} points, {"As":[0],"Ic":[90],"Ds":[180],"Mc":[270]} 71 | * @see (this.settings.AspectCalculator( toPoints) ) 72 | */ 73 | addPointsOfInterest(points: Points): Radix; 74 | drawRuler(): void; 75 | /** 76 | * Draw circles 77 | */ 78 | drawCircles(): void; 79 | /** 80 | * Display transit horoscope 81 | * 82 | * @param {Object} data 83 | * @example 84 | * { 85 | * "planets":{"Moon":[0], "Sun":[30], ... }, 86 | * "cusps":[300, 340, 30, 60, 75, 90, 116, 172, 210, 236, 250, 274], * 87 | * } 88 | * 89 | * @return {Transit} transit 90 | */ 91 | transit(data: AstroData): Transit; 92 | } 93 | export default Radix; 94 | -------------------------------------------------------------------------------- /dist/project/src/settings.d.ts: -------------------------------------------------------------------------------- 1 | import type SVG from './svg'; 2 | export interface AspectData { 3 | degree: number; 4 | orbit: number; 5 | color: string; 6 | } 7 | export type Aspect = Record; 8 | export interface Dignity { 9 | name: string; 10 | position: number; 11 | orbit: number; 12 | } 13 | export interface Settings { 14 | SYMBOL_SCALE: number; 15 | COLOR_BACKGROUND: string; 16 | POINTS_COLOR: string; 17 | POINTS_TEXT_SIZE: number; 18 | POINTS_STROKE: number; 19 | SIGNS_COLOR: string; 20 | SIGNS_STROKE: number; 21 | MARGIN: number; 22 | PADDING: number; 23 | ID_CHART: string; 24 | ID_RADIX: string; 25 | ID_TRANSIT: string; 26 | ID_ASPECTS: string; 27 | ID_POINTS: string; 28 | ID_SIGNS: string; 29 | ID_CIRCLES: string; 30 | ID_AXIS: string; 31 | ID_CUSPS: string; 32 | ID_RULER: string; 33 | ID_BG: string; 34 | CIRCLE_COLOR: string; 35 | CIRCLE_STRONG: number; 36 | LINE_COLOR: string; 37 | INDOOR_CIRCLE_RADIUS_RATIO: number; 38 | INNER_CIRCLE_RADIUS_RATIO: number; 39 | RULER_RADIUS: number; 40 | SYMBOL_SUN: string; 41 | SYMBOL_MOON: string; 42 | SYMBOL_MERCURY: string; 43 | SYMBOL_VENUS: string; 44 | SYMBOL_MARS: string; 45 | SYMBOL_JUPITER: string; 46 | SYMBOL_SATURN: string; 47 | SYMBOL_URANUS: string; 48 | SYMBOL_NEPTUNE: string; 49 | SYMBOL_PLUTO: string; 50 | SYMBOL_CHIRON: string; 51 | SYMBOL_LILITH: string; 52 | SYMBOL_NNODE: string; 53 | SYMBOL_SNODE: string; 54 | SYMBOL_FORTUNE: string; 55 | SYMBOL_AS: string; 56 | SYMBOL_DS: string; 57 | SYMBOL_MC: string; 58 | SYMBOL_IC: string; 59 | SYMBOL_AXIS_FONT_COLOR: string; 60 | SYMBOL_AXIS_STROKE: number; 61 | SYMBOL_CUSP_1: string; 62 | SYMBOL_CUSP_2: string; 63 | SYMBOL_CUSP_3: string; 64 | SYMBOL_CUSP_4: string; 65 | SYMBOL_CUSP_5: string; 66 | SYMBOL_CUSP_6: string; 67 | SYMBOL_CUSP_7: string; 68 | SYMBOL_CUSP_8: string; 69 | SYMBOL_CUSP_9: string; 70 | SYMBOL_CUSP_10: string; 71 | SYMBOL_CUSP_11: string; 72 | SYMBOL_CUSP_12: string; 73 | CUSPS_STROKE: number; 74 | CUSPS_FONT_COLOR: string; 75 | SYMBOL_ARIES: string; 76 | SYMBOL_TAURUS: string; 77 | SYMBOL_GEMINI: string; 78 | SYMBOL_CANCER: string; 79 | SYMBOL_LEO: string; 80 | SYMBOL_VIRGO: string; 81 | SYMBOL_LIBRA: string; 82 | SYMBOL_SCORPIO: string; 83 | SYMBOL_SAGITTARIUS: string; 84 | SYMBOL_CAPRICORN: string; 85 | SYMBOL_AQUARIUS: string; 86 | SYMBOL_PISCES: string; 87 | SYMBOL_SIGNS: string[]; 88 | COLOR_ARIES: string; 89 | COLOR_TAURUS: string; 90 | COLOR_GEMINI: string; 91 | COLOR_CANCER: string; 92 | COLOR_LEO: string; 93 | COLOR_VIRGO: string; 94 | COLOR_LIBRA: string; 95 | COLOR_SCORPIO: string; 96 | COLOR_SAGITTARIUS: string; 97 | COLOR_CAPRICORN: string; 98 | COLOR_AQUARIUS: string; 99 | COLOR_PISCES: string; 100 | COLORS_SIGNS: string[]; 101 | CUSTOM_SYMBOL_FN: null | ((name: string, x: number, y: number, context: SVG) => Element); 102 | SHIFT_IN_DEGREES: number; 103 | STROKE_ONLY: boolean; 104 | ADD_CLICK_AREA: boolean; 105 | COLLISION_RADIUS: number; 106 | ASPECTS: Aspect; 107 | SHOW_DIGNITIES_TEXT: boolean; 108 | DIGNITIES_RULERSHIP: string; 109 | DIGNITIES_DETRIMENT: string; 110 | DIGNITIES_EXALTATION: string; 111 | DIGNITIES_EXACT_EXALTATION: string; 112 | DIGNITIES_FALL: string; 113 | DIGNITIES_EXACT_EXALTATION_DEFAULT: Dignity[]; 114 | ANIMATION_CUSPS_ROTATION_SPEED: number; 115 | DEBUG: boolean; 116 | } 117 | declare const default_settings: Settings; 118 | export default default_settings; 119 | -------------------------------------------------------------------------------- /dist/project/src/svg.d.ts: -------------------------------------------------------------------------------- 1 | import type { Settings } from './settings'; 2 | /** 3 | * SVG tools. 4 | * 5 | * @class 6 | * @public 7 | * @constructor 8 | * @param {String} elementId - root DOM Element 9 | * @param {int} width 10 | * @param {int} height 11 | */ 12 | declare class SVG { 13 | settings: Settings; 14 | _paperElementId: string; 15 | DOMElement: SVGSVGElement; 16 | root: Element; 17 | width: number; 18 | height: number; 19 | context: this; 20 | constructor(elementId: string, width: number, height: number, settings: Settings); 21 | _getSymbol(name: string, x: number, y: number): Element; 22 | /** 23 | * Get a required symbol. 24 | * 25 | * @param {String} name 26 | * @param {int} x 27 | * @param {int} y 28 | * 29 | * @return {SVGElement g} 30 | */ 31 | getSymbol(name: string, x: number, y: number): Element; 32 | /** 33 | * Create transparent rectangle. 34 | * 35 | * Used to improve area click, @see this.settings.ADD_CLICK_AREA 36 | * 37 | * @param {Number} x 38 | * @param {Number} y 39 | * 40 | * @return {Element} rect 41 | */ 42 | createRectForClick(x: number, y: number): Element; 43 | /** 44 | * Get ID for sign wrapper. 45 | * 46 | * @param {String} sign 47 | * 48 | * @return {String id} 49 | */ 50 | getSignWrapperId(sign: string): string; 51 | /** 52 | * Get ID for house wrapper. 53 | * 54 | * @param {String} house 55 | * 56 | * @return {String id} 57 | */ 58 | getHouseIdWrapper(house: string): string; 59 | sun(x: number, y: number): Element; 60 | moon(x: number, y: number): Element; 61 | mercury(x: number, y: number): Element; 62 | venus(x: number, y: number): Element; 63 | mars(x: number, y: number): Element; 64 | jupiter(x: number, y: number): Element; 65 | saturn(x: number, y: number): Element; 66 | uranus(x: number, y: number): Element; 67 | neptune(x: number, y: number): Element; 68 | pluto(x: number, y: number): Element; 69 | chiron(x: number, y: number): Element; 70 | lilith(x: number, y: number): Element; 71 | nnode(x: number, y: number): Element; 72 | snode(x: number, y: number): Element; 73 | fortune(x: number, y: number): Element; 74 | aries(x: number, y: number): Element; 75 | taurus(x: number, y: number): Element; 76 | gemini(x: number, y: number): Element; 77 | cancer(x: number, y: number): Element; 78 | leo(x: number, y: number): Element; 79 | virgo(x: number, y: number): Element; 80 | libra(x: number, y: number): Element; 81 | scorpio(x: number, y: number): Element; 82 | sagittarius(x: number, y: number): Element; 83 | capricorn(x: number, y: number): Element; 84 | aquarius(x: number, y: number): Element; 85 | pisces(x: number, y: number): Element; 86 | /** 87 | * Draw As symbol 88 | */ 89 | ascendant(x: number, y: number): Element; 90 | /** 91 | * Draw Ds symbol 92 | */ 93 | descendant(x: number, y: number): Element; 94 | /** 95 | * Draw MC symbol 96 | */ 97 | mediumCoeli(x: number, y: number): Element; 98 | /** 99 | * Draw IC symbol 100 | */ 101 | immumCoeli(x: number, y: number): Element; 102 | number1(x: number, y: number): Element; 103 | number2(x: number, y: number): Element; 104 | number3(x: number, y: number): Element; 105 | number4(x: number, y: number): Element; 106 | number5(x: number, y: number): Element; 107 | number6(x: number, y: number): Element; 108 | number7(x: number, y: number): Element; 109 | number8(x: number, y: number): Element; 110 | number9(x: number, y: number): Element; 111 | number10(x: number, y: number): Element; 112 | number11(x: number, y: number): Element; 113 | number12(x: number, y: number): Element; 114 | /** 115 | * Draw circular sector 116 | * 117 | * @param {int} x - circle x center position 118 | * @param {int} y - circle y center position 119 | * @param {int} radius - circle radius in px 120 | * @param {int} a1 - angleFrom in degree 121 | * @param {int} a2 - angleTo in degree 122 | * @param {int} thickness - from outside to center in px 123 | * 124 | * @return {SVGElement} segment 125 | * 126 | * @see SVG Path arc: https://www.w3.org/TR/SVG/paths.html#PathData 127 | */ 128 | segment(x: number, y: number, radius: number, a1: number, a2: number, thickness: number, lFlag?: number, sFlag?: number): Element; 129 | /** 130 | * Draw line in circle 131 | * 132 | * @param {int} x1 133 | * @param {int} y2 134 | * @param {int} x2 135 | * @param {int} y2 136 | * @param {String} color - HTML rgb 137 | * 138 | * @return {SVGElement} line 139 | */ 140 | line(x1: number, y1: number, x2: number, y2: number): Element; 141 | /** 142 | * Draw a circle 143 | * 144 | * @param {int} cx 145 | * @param {int} cy 146 | * @param {int} radius 147 | * 148 | * @return {SVGElement} circle 149 | */ 150 | circle(cx: number, cy: number, radius: number): Element; 151 | /** 152 | * Draw a text 153 | * 154 | * @param {String} text 155 | * @param {int} x 156 | * @param {int} y 157 | * @param {String} size - etc. "13px" 158 | * @param {String} color - HTML rgb 159 | * 160 | * @return {SVGElement} text 161 | */ 162 | text(txt: string, x: number, y: number, size: string, color: string): Element; 163 | } 164 | export default SVG; 165 | -------------------------------------------------------------------------------- /dist/project/src/transit.d.ts: -------------------------------------------------------------------------------- 1 | import type { FormedAspect } from './aspect'; 2 | import type { AstroData, LocatedPoint, Points } from './radix'; 3 | import type Radix from './radix'; 4 | import type SVG from './svg'; 5 | import type { Settings } from './settings'; 6 | /** 7 | * Transit charts. 8 | * 9 | * @class 10 | * @public 11 | * @constructor 12 | * @param {this.settings.Radix} radix 13 | * @param {Object} data 14 | */ 15 | declare class Transit { 16 | data: AstroData; 17 | paper: SVG; 18 | cx: number; 19 | cy: number; 20 | toPoints: Points; 21 | radius: number; 22 | settings: Settings; 23 | rulerRadius: number; 24 | pointRadius: number; 25 | shift: number; 26 | universe: Element; 27 | context: this; 28 | locatedPoints: LocatedPoint[]; 29 | constructor(radix: Radix, data: AstroData, settings: Settings); 30 | /** 31 | * Draw background 32 | */ 33 | drawBg(): void; 34 | /** 35 | * Draw planets 36 | * 37 | * @param{undefined | Object} planetsData, posible data planets to draw 38 | */ 39 | drawPoints(planetsData?: Points): void; 40 | /** 41 | * Draw circles 42 | */ 43 | drawCircles(): void; 44 | /** 45 | * Draw cusps 46 | * @param{undefined | Object} cuspsData, posible data cusps to draw 47 | */ 48 | drawCusps(cuspsData?: number[]): void; 49 | drawRuler(): void; 50 | /** 51 | * Draw aspects 52 | * @param{Array | null} customAspects - posible custom aspects to draw; 53 | */ 54 | aspects(customAspects: FormedAspect[]): Transit; 55 | /** 56 | * Moves points to another position. 57 | * 58 | * @param {Object} data - planets target positions. 59 | * @param {Integer} duration - in seconds 60 | * @param {boolean} isReverse 61 | * @param {Function | undefined} callbck - the function executed at the end of animation 62 | */ 63 | animate(data: AstroData, duration: number, isReverse: boolean, callback: () => void): Transit; 64 | } 65 | export default Transit; 66 | -------------------------------------------------------------------------------- /dist/project/src/utils.d.ts: -------------------------------------------------------------------------------- 1 | import type { AstroData, LocatedPoint } from './radix'; 2 | import type { Settings } from './settings'; 3 | /** 4 | * Calculate position of the point on the circle. 5 | * 6 | * @param {int} cx - center x 7 | * @param {int} cy - center y 8 | * @param {int} radius 9 | * @param {double} angle - degree 10 | * 11 | * @return {{x: number, y: number}} Obj - {x:10, y:20} 12 | */ 13 | export declare const getPointPosition: (cx: number, cy: number, radius: number, angle: number, astrology: { 14 | SHIFT_IN_DEGREES: number; 15 | }) => { 16 | x: number; 17 | y: number; 18 | }; 19 | export declare const degreeToRadians: (degrees: number) => number; 20 | export declare const radiansToDegree: (radians: number) => number; 21 | interface TextLocation { 22 | text: string; 23 | x: number; 24 | y: number; 25 | } 26 | /** 27 | * Calculates positions of the point description 28 | * 29 | * @param {Object} point 30 | * @param {Array} texts 31 | * 32 | * @return {Array} [{text:"abc", x:123, y:456}, {text:"cvb", x:456, y:852}, ...] 33 | */ 34 | export declare const getDescriptionPosition: (point: { 35 | x: number; 36 | y: number; 37 | }, texts: string[], astrology: { 38 | COLLISION_RADIUS: number; 39 | SYMBOL_SCALE: number; 40 | }) => TextLocation[]; 41 | /** 42 | * Checks a source data 43 | * @private 44 | * 45 | * @param {Object} data 46 | * @return {{hasError: boolean, messages: string[]}} status 47 | */ 48 | export declare const validate: (data: AstroData) => { 49 | hasError: boolean; 50 | messages: string[]; 51 | }; 52 | /** 53 | * Get empty DOMElement with ID 54 | * 55 | * @param{String} elementID 56 | * @param{DOMElement} parent 57 | * @return {DOMElement} 58 | */ 59 | export declare const getEmptyWrapper: (parent: Element, elementID: string, _paperElementId: string) => Element; 60 | /** 61 | * Remove childs 62 | * 63 | * @param{DOMElement} parent 64 | */ 65 | export declare const removeChilds: (parent: HTMLElement) => void; 66 | /** 67 | * Check circle collision between two objects 68 | * 69 | * @param {Object} circle1, {x:123, y:123, r:50} 70 | * @param {Object} circle2, {x:456, y:456, r:60} 71 | * @return {boolean} 72 | */ 73 | export declare const isCollision: (circle1: { 74 | x: number; 75 | y: number; 76 | r: number; 77 | }, circle2: { 78 | x: number; 79 | y: number; 80 | r: number; 81 | }) => boolean; 82 | /** 83 | * Places a new point in the located list 84 | * 85 | * @param {Array} locatedPoints, [{name:"Mars", x:123, y:123, r:50, ephemeris:45.96}, {name:"Sun", x:1234, y:1234, r:50, ephemeris:100.96}] 86 | * @param {Object} point, {name:"Venus", x:78, y:56, r:50, angle:15.96} 87 | * @param {Object} universe - current universe 88 | * @return {Array} locatedPoints 89 | */ 90 | export declare const assemble: (locatedPoints: LocatedPoint[], point: LocatedPoint, universe: { 91 | cx: number; 92 | cy: number; 93 | r: number; 94 | }, astrology: Settings) => LocatedPoint[]; 95 | /** 96 | * Sets the positions of two points that are in collision. 97 | * 98 | * @param {Object} p1, {..., pointer:123, angle:456} 99 | * @param {Object} p2, {..., pointer:23, angle:56} 100 | */ 101 | export declare const placePointsInCollision: (p1: LocatedPoint, p2: LocatedPoint) => void; 102 | /** 103 | * Check collision between angle and object 104 | * 105 | * @param {double} angle 106 | * @param {Array} points, [{x:456, y:456, r:60, angle:123}, ...] 107 | * @return {boolean} 108 | */ 109 | export declare const isInCollision: (angle: number, points: string | any[], astrology: Settings) => boolean; 110 | interface InitialEndPosition { 111 | startX: number; 112 | startY: number; 113 | endX: number; 114 | endY: number; 115 | } 116 | /** 117 | * Calculates positions of the dashed line passing through the obstacle. 118 | * * 119 | * @param {double} centerX 120 | * @param {double} centerY 121 | * @param {double} angle - line angle 122 | * @param {double} lineStartRadius 123 | * @param {double} lineEndRadius 124 | * @param {double} obstacleRadius 125 | * @param {Array} obstacles, [{x:456, y:456, r:60, angle:123}, ...] 126 | * 127 | * @return {Array} [{startX:1, startY:1, endX:4, endY:4}, {startX:6, startY:6, endX:8, endY:8}] 128 | */ 129 | export declare const getDashedLinesPositions: (centerX: number, centerY: number, angle: number, lineStartRadius: number, lineEndRadius: number, obstacleRadius: number, obstacles: LocatedPoint[], astrology: Settings) => InitialEndPosition[]; 130 | /** 131 | * Calculate ruler positions. 132 | * 133 | * @param {Double} centerX 134 | * @param {Double} centerY 135 | * @param {Double} startRadius 136 | * @param {Double} endRadius 137 | * @param {Boolean} startAngle 138 | * 139 | * @return {Array} [ {startX:1,startY:2, endX:3, endX:4 }, ...] 140 | */ 141 | export declare const getRulerPositions: (centerX: number, centerY: number, startRadius: number, endRadius: number, startAngle: number, astrology: { 142 | SHIFT_IN_DEGREES: number; 143 | }) => InitialEndPosition[]; 144 | /** 145 | * Compare two points 146 | * 147 | * @param {Object} pointA, {name:"Venus", x:78, y:56, r:50, angle:15.96} 148 | * @param {Object} pointB, {name:"Mercury", x:78, y:56, r:50, angle:20.26} 149 | * @return 150 | */ 151 | export declare const comparePoints: (pointA: { 152 | angle: number; 153 | }, pointB: { 154 | angle: number; 155 | }) => number; 156 | export {}; 157 | -------------------------------------------------------------------------------- /dist/project/src/zodiac.d.ts: -------------------------------------------------------------------------------- 1 | import type { Dignity, Settings } from './settings'; 2 | /** 3 | * Zodiac 4 | * 5 | * Gives the position of points in the zodiac. 6 | * Position of point in the zodiac. 7 | * Position of point in houses. 8 | * Dignities of planets. 9 | * 10 | * @class 11 | * @public 12 | * @constructor 13 | * @param {Array} cusps - cusprs in zodiac; [296, 350, 30, 56, 75, 94, 116, 170, 210, 236, 255, 274] 14 | * @param {Object | null } settings 15 | */ 16 | declare class Zodiac { 17 | cusps: number[]; 18 | settings: Settings; 19 | constructor(cusps: number[], settings?: Settings); 20 | /** 21 | * Get astrological sign 22 | * 1 - Arise, ... , 12 - Pisces 23 | * 24 | * @param {double} point - angle of point in circle 25 | * @return { \[1-9] | 1[0-2]\ } 26 | */ 27 | getSign(point: number): number; 28 | /** 29 | * Is retrograde 30 | * 31 | * @param {double} speed 32 | * @return {boolean} 33 | */ 34 | isRetrograde(speed: number): boolean; 35 | /** 36 | * Get house number 37 | * 1 - 12 38 | * 39 | * @param {double} point - angle of point in circle 40 | * @return { \[1-9] | 1[0-2]\ } 41 | */ 42 | getHouseNumber(point: number): number; 43 | /** 44 | * Calculate dignities of planet 45 | * 46 | * r - Rulership 47 | * d - Detriment 48 | * e - Exaltation 49 | * E - Exalatation - Exact exaltation 50 | * f - Fall 51 | * 52 | * @param {Object} planet, { name:"Sun", position:60.2 } 53 | * @param {Array | null } exactExaltation - list of named angles, [{ name:"Sun", position:278, orbit:2 }, { name:"Moon", position:3, , orbit:2 }] 54 | * @return {Array} 55 | */ 56 | getDignities(planet: { 57 | name: string; 58 | position: number; 59 | }, exactExaltation?: Dignity[]): string[]; 60 | toDMS(d: number): string; 61 | hasConjunction(planetPosition: number, pointPosition: number, orbit: number): boolean; 62 | } 63 | export default Zodiac; 64 | -------------------------------------------------------------------------------- /doc/images/transits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroDraw/AstroChart/f1fdbbeda752b3817ec9f851313830cbb09c5601/doc/images/transits.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | collectCoverageFrom: ['./project/src/**/*.{ts,js}'], 5 | testEnvironment: 'jsdom' 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astrodraw/astrochart", 3 | "version": "3.0.2", 4 | "description": "A free and open-source JavaScript library for generating SVG charts to display planets in astrology.", 5 | "keywords": [ 6 | "astrology", 7 | "chart", 8 | "horoscope", 9 | "zodiac", 10 | "ephemetis" 11 | ], 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/AstroDraw/AstroChart.git" 16 | }, 17 | "main": "./dist/astrochart.js", 18 | "types": "./dist/project/src/index.d.ts", 19 | "scripts": { 20 | "test": "jest", 21 | "test:w": "jest --watch", 22 | "test:coverage": "jest --coverage", 23 | "build": "webpack", 24 | "build:w": "webpack --watch", 25 | "lint": "eslint --ignore-path .eslintignore --ext .ts ." 26 | }, 27 | "devDependencies": { 28 | "@types/jest": "^27.4.1", 29 | "@typescript-eslint/eslint-plugin": "^5.53.0", 30 | "@typescript-eslint/parser": "^5.53.0", 31 | "eslint": "^8.34.0", 32 | "eslint-config-standard-with-typescript": "^34.0.0", 33 | "eslint-plugin-import": "^2.27.5", 34 | "eslint-plugin-n": "^15.6.1", 35 | "eslint-plugin-promise": "^6.1.1", 36 | "jest": "^27.5.1", 37 | "terser-webpack-plugin": "^5.3.1", 38 | "ts-jest": "^27.1.4", 39 | "ts-loader": "^9.2.8", 40 | "typescript": "^4.9.5", 41 | "webpack": "^5.72.0", 42 | "webpack-cli": "^4.9.2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /project/__tests__/radix.test.ts: -------------------------------------------------------------------------------- 1 | import Chart from "../src"; 2 | 3 | test(`snapshot radix with default settings`, () => { 4 | const data = { 5 | "planets": { "Fortune": [45.930008627285154], "SNode": [263.2584780960899], "Jupiter": [173.07043720306802], "Mars": [217.97167231451178], "Lilith": [196.19480722950317], "Saturn": [252.92341772675047], "Chiron": [348.1157239728284], "Uranus": [16.7900184974611], "Sun": [297.68062428797253], "Mercury": [289.10132025725494], "Neptune": [338.01899718442604], "Pluto": [285.6473452237151, -0.123] }, 6 | "cusps": [348.20510089894015, 38.108507808919654, 65.20783751818992, 84.96083001338991, 103.77897207128007, 127.1084408347092, 168.20510089894015, 218.10850780891965, 245.20783751818993, 264.9608300133899, 283.77897207128007, 307.1084408347092] 7 | }; 8 | 9 | 10 | document.body.innerHTML = 11 | '
' + 12 | '
'; 13 | const chart = new Chart('test-element', 800, 800); 14 | chart.radix(data); 15 | 16 | 17 | const tree = document.getElementById('test-element') 18 | expect(tree).toMatchSnapshot(); 19 | 20 | }) 21 | 22 | test(`snapshot radix with scale`, () => { 23 | const data = { 24 | "planets": { "Fortune": [45.930008627285154], "SNode": [263.2584780960899], "Jupiter": [173.07043720306802], "Mars": [217.97167231451178], "Lilith": [196.19480722950317], "Saturn": [252.92341772675047], "Chiron": [348.1157239728284], "Uranus": [16.7900184974611], "Sun": [297.68062428797253], "Mercury": [289.10132025725494], "Neptune": [338.01899718442604], "Pluto": [285.6473452237151, -0.123] }, 25 | "cusps": [348.20510089894015, 38.108507808919654, 65.20783751818992, 84.96083001338991, 103.77897207128007, 127.1084408347092, 168.20510089894015, 218.10850780891965, 245.20783751818993, 264.9608300133899, 283.77897207128007, 307.1084408347092] 26 | }; 27 | 28 | 29 | document.body.innerHTML = 30 | '
' + 31 | '
'; 32 | const chart = new Chart('test-element', 800, 800, { SYMBOL_SCALE: 0.8 }); 33 | chart.radix(data); 34 | 35 | 36 | const tree = document.getElementById('test-element') 37 | expect(tree).toMatchSnapshot(); 38 | 39 | }) 40 | -------------------------------------------------------------------------------- /project/examples/2ChartsOnPage/2radix.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2 Radix 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /project/examples/debug/calibration.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Radix 6 | 11 | 12 | 13 |
14 | 15 | 16 | 26 | 27 | -------------------------------------------------------------------------------- /project/examples/radix/radix.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Radix 6 | 11 | 12 | 13 |
14 | 15 | 16 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /project/examples/radix/radix_2016_11_15.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Radix 6 | 11 | 12 | 13 |
    14 |
  • Day:15 Month:11 Year:2016 Time:9:56 [+01:00]
  • 15 |
  • Znojmo, The Czech Republic
  • 16 |
  • Latitude: 48° 51' 20 North
  • 17 |
  • Longitude: 16° 02' 56 East
  • 18 |
  • Placidus house system
  • 19 |
  • Orbit: 2.5
  • 20 |
21 |
22 | 23 | 24 | 36 | 37 | -------------------------------------------------------------------------------- /project/examples/radix/radix_collision.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Radix collision 6 | 11 | 12 | 13 |
14 | 15 | 16 | 31 | 32 | -------------------------------------------------------------------------------- /project/examples/radix/radix_custom_settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Radix 6 | 11 | 12 | 13 |
14 | 15 | 16 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /project/examples/transit/animate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Animated transit 6 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 49 | 50 | -------------------------------------------------------------------------------- /project/examples/transit/stroke_only.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Transit 6 | 15 | 16 | 17 |

Stroke only

18 |
19 | 20 | 21 | 38 | 39 | -------------------------------------------------------------------------------- /project/examples/transit/transit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Transit 6 | 15 | 16 | 17 |
    18 |
  • Radix: 18. 1. 2016 9:45 (Europe/Vienna, lo:16.0542676, la:48.8559107)
  • 19 |
  • Transit: 12. 12. 2016 14:15
  • 20 |
  • Orbit: 2.5°
  • 21 |
22 |
23 | 24 | 25 | 49 | 50 | -------------------------------------------------------------------------------- /project/src/animation/animator.ts: -------------------------------------------------------------------------------- 1 | import type { AstroData } from '../radix' 2 | import type { Settings } from '../settings' 3 | import type Transit from '../transit' 4 | import { radiansToDegree } from '../utils' 5 | import Timer from './timer' 6 | /** 7 | * Transit chart animator 8 | * 9 | * Animates the object on a circle. 10 | * 11 | * @class 12 | * @public 13 | * @constructor 14 | * @param {Object} from, {"Sun":[12], "Moon":[60]} 15 | * @param {Object} to, {"Sun":[30], "Moon":[180]} 16 | * @param {Object} settings, {cx:100, cy:100, radius:200, prefix:"astro-chart-"} 17 | */ 18 | class Animator { 19 | transit: Transit 20 | isReverse: boolean 21 | rotation: number 22 | settings: Settings 23 | actualPlanetPos: any 24 | timer: Timer 25 | timeSinceLoopStart: number 26 | context: this 27 | cuspsElement: any 28 | data: AstroData 29 | duration: number 30 | callback: () => void 31 | constructor (transit: Transit, settings: Settings) { 32 | this.transit = transit 33 | this.isReverse = false 34 | this.rotation = 0 35 | this.settings = settings 36 | // Copy data 37 | this.actualPlanetPos = {} 38 | for (const planet in this.transit.data.planets) { 39 | if (this.transit.data.planets.hasOwnProperty(planet)) { 40 | this.actualPlanetPos[planet] = this.transit.data.planets[planet] 41 | } 42 | } 43 | 44 | this.timer = new Timer(this.update.bind(this), this.settings.DEBUG) 45 | 46 | // time, passed since the start of the loop 47 | this.timeSinceLoopStart = 0 48 | 49 | this.context = this 50 | this.cuspsElement = null 51 | } 52 | 53 | /** 54 | * Animate objects 55 | 56 | * @param {Object} data, targetPositions 57 | * @param {Integer} duration - seconds 58 | * @param {boolean} isReverse 59 | * @param {Function} callbck - start et the end of animation 60 | */ 61 | animate (data: any, duration: number, isReverse: boolean, callback: () => void): void { 62 | this.data = data 63 | this.duration = duration * 1000 64 | this.isReverse = isReverse || false 65 | this.callback = callback 66 | 67 | this.rotation = 0 68 | this.cuspsElement = document.getElementById(this.transit.paper._paperElementId + '-' + this.settings.ID_TRANSIT + '-' + this.settings.ID_CUSPS) 69 | 70 | this.timer.start() 71 | } 72 | 73 | update (deltaTime?: number): void { 74 | deltaTime = deltaTime ?? 1 75 | this.timeSinceLoopStart += deltaTime 76 | if (this.timeSinceLoopStart >= this.duration) { 77 | this.timer.stop() 78 | 79 | if (typeof this.callback === 'function') { 80 | this.callback() 81 | } 82 | 83 | return 84 | } 85 | 86 | const expectedNumberOfLoops = (this.duration - this.timeSinceLoopStart) < deltaTime 87 | ? 1 88 | : Math.round((this.duration - this.timeSinceLoopStart) / deltaTime) 89 | 90 | this.updatePlanets(expectedNumberOfLoops) 91 | this.updateCusps(expectedNumberOfLoops) 92 | } 93 | 94 | /* 95 | * @private 96 | */ 97 | updateCusps (expectedNumberOfLoops: number): void { 98 | const deg360 = radiansToDegree(2 * Math.PI) 99 | let targetCuspAngle = this.transit.data.cusps[0] - this.data.cusps[0] 100 | 101 | if (targetCuspAngle < 0) { 102 | targetCuspAngle += deg360 103 | } 104 | 105 | // speed 106 | if (this.settings.ANIMATION_CUSPS_ROTATION_SPEED > 0) { 107 | targetCuspAngle += (this.isReverse) 108 | ? -1 * ((this.settings.ANIMATION_CUSPS_ROTATION_SPEED * deg360) + deg360) 109 | : this.settings.ANIMATION_CUSPS_ROTATION_SPEED * deg360 110 | } 111 | 112 | let difference = (this.isReverse) 113 | ? this.rotation - targetCuspAngle 114 | : targetCuspAngle - this.rotation 115 | 116 | // zero crossing 117 | if (difference < 0) { 118 | difference += deg360 119 | } 120 | 121 | let increment = difference / expectedNumberOfLoops 122 | 123 | if (this.isReverse) { 124 | increment *= -1 125 | } 126 | this.rotation += increment 127 | 128 | this.cuspsElement.setAttribute('transform', 'rotate(' + this.rotation + ' ' + this.transit.cx + ' ' + this.transit.cy + ')') 129 | 130 | if (expectedNumberOfLoops === 1) { 131 | this.cuspsElement.removeAttribute('transform') 132 | } 133 | } 134 | 135 | /* 136 | * @private 137 | */ 138 | updatePlanets (expectedNumberOfLoops: number): void { 139 | for (const planet in this.data.planets) { 140 | if (this.data.planets.hasOwnProperty(planet)) { 141 | const actualPlanetAngle: number = this.actualPlanetPos[planet][0] 142 | const targetPlanetAngle: number = this.data.planets[planet][0] 143 | const isRetrograde = this.actualPlanetPos[planet][1] != null && this.actualPlanetPos[planet][1] < 0 144 | 145 | let difference 146 | if (this.isReverse && isRetrograde) { 147 | difference = targetPlanetAngle - actualPlanetAngle 148 | } else if (this.isReverse || isRetrograde) { 149 | difference = actualPlanetAngle - targetPlanetAngle 150 | } else { 151 | difference = targetPlanetAngle - actualPlanetAngle 152 | } 153 | 154 | // zero crossing 155 | if (difference < 0) { 156 | difference += radiansToDegree(2 * Math.PI) 157 | } 158 | 159 | let increment = difference / expectedNumberOfLoops 160 | 161 | if (this.isReverse) { 162 | increment *= -1 163 | } 164 | 165 | if (isRetrograde) { 166 | increment *= -1 167 | } 168 | 169 | let newPos = actualPlanetAngle + increment 170 | if (newPos < 0) { 171 | newPos += radiansToDegree(2 * Math.PI) 172 | } 173 | 174 | this.actualPlanetPos[planet][0] = newPos 175 | } 176 | } 177 | 178 | this.transit.drawPoints(this.actualPlanetPos) 179 | } 180 | } 181 | 182 | export default Animator 183 | -------------------------------------------------------------------------------- /project/src/animation/timer.ts: -------------------------------------------------------------------------------- 1 | class Timer { 2 | debug: boolean 3 | callback: (delta: number) => void 4 | boundTick_: FrameRequestCallback 5 | lastGameLoopFrame: number 6 | requestID_: number | undefined 7 | constructor(callback: (delta: number) => void, debug: boolean) { 8 | if (typeof callback !== 'function') { 9 | throw new Error('param \'callback\' has to be a function.') 10 | } 11 | this.debug = debug 12 | 13 | this.callback = callback 14 | this.boundTick_ = this.tick.bind(this) 15 | } 16 | 17 | start(): void { 18 | if (!this.requestID_) { 19 | this.lastGameLoopFrame = new Date().getTime() 20 | this.tick() 21 | if (this.debug) console.log('[astrology.Timer] start') 22 | } 23 | } 24 | 25 | stop(): void { 26 | if (this.requestID_) { 27 | window.cancelAnimationFrame(this.requestID_) 28 | this.requestID_ = undefined 29 | if (this.debug) console.log('[astrology.Timer] stop') 30 | } 31 | } 32 | 33 | isRunning(): boolean { 34 | return !!this.requestID_ 35 | } 36 | 37 | tick(): void { 38 | const now = new Date().getTime() 39 | this.requestID_ = window.requestAnimationFrame(this.boundTick_) 40 | this.callback(now - this.lastGameLoopFrame) 41 | this.lastGameLoopFrame = now 42 | } 43 | } 44 | 45 | export default Timer 46 | -------------------------------------------------------------------------------- /project/src/aspect.test.ts: -------------------------------------------------------------------------------- 1 | import AspectCalculator from "./aspect"; 2 | import default_settings from "./settings"; 3 | 4 | describe('radix', () => { 5 | test('1', () => { 6 | var toPoints = { 7 | Sun: [0], 8 | Moon: [91], 9 | Neptune: [122], 10 | As: [330], 11 | Ic: [90], 12 | }; 13 | 14 | var points = { 15 | Sun: [0], 16 | Moon: [91], 17 | Neptune: [122], 18 | }; 19 | 20 | const settings = { 21 | ...default_settings, 22 | ASPECTS: { 23 | conjunction: { degree: 0, orbit: 10, color: "transparent" }, 24 | square: { degree: 90, orbit: 8, color: "#FF4500" }, 25 | trine: { degree: 120, orbit: 8, color: "#27AE60" }, 26 | opposition: { degree: 180, orbit: 10, color: "#27AE60" }, 27 | }, 28 | }; 29 | 30 | const calculator = new AspectCalculator(toPoints, settings); 31 | const aspects = calculator.radix(points); 32 | 33 | expect(aspects).toStrictEqual([ 34 | { 35 | aspect: { 36 | color: "#FF4500", 37 | degree: 90, 38 | name: "square", 39 | orbit: 8, 40 | }, 41 | point: { 42 | name: "Sun", 43 | position: 0, 44 | }, 45 | precision: "0.0000", 46 | toPoint: { 47 | name: "Ic", 48 | position: 90, 49 | }, 50 | }, 51 | { 52 | aspect: { 53 | color: "#FF4500", 54 | degree: 90, 55 | name: "square", 56 | orbit: 8, 57 | }, 58 | point: { 59 | name: "Sun", 60 | position: 0, 61 | }, 62 | precision: "1.0000", 63 | toPoint: { 64 | name: "Moon", 65 | position: 91, 66 | }, 67 | }, 68 | { 69 | aspect: { 70 | color: "#FF4500", 71 | degree: 90, 72 | name: "square", 73 | orbit: 8, 74 | }, 75 | point: { 76 | name: "Moon", 77 | position: 91, 78 | }, 79 | precision: "1.0000", 80 | toPoint: { 81 | name: "Sun", 82 | position: 0, 83 | }, 84 | }, 85 | { 86 | aspect: { 87 | color: "#27AE60", 88 | degree: 120, 89 | name: "trine", 90 | orbit: 8, 91 | }, 92 | point: { 93 | name: "Moon", 94 | position: 91, 95 | }, 96 | precision: "1.0000", 97 | toPoint: { 98 | name: "As", 99 | position: 330, 100 | }, 101 | }, 102 | { 103 | aspect: { 104 | color: "transparent", 105 | degree: 0, 106 | name: "conjunction", 107 | orbit: 10, 108 | }, 109 | point: { 110 | name: "Moon", 111 | position: 91, 112 | }, 113 | precision: "1.0000", 114 | toPoint: { 115 | name: "Ic", 116 | position: 90, 117 | }, 118 | }, 119 | { 120 | aspect: { 121 | color: "#27AE60", 122 | degree: 120, 123 | name: "trine", 124 | orbit: 8, 125 | }, 126 | point: { 127 | name: "Sun", 128 | position: 0, 129 | }, 130 | precision: "2.0000", 131 | toPoint: { 132 | name: "Neptune", 133 | position: 122, 134 | }, 135 | }, 136 | { 137 | aspect: { 138 | color: "#27AE60", 139 | degree: 120, 140 | name: "trine", 141 | orbit: 8, 142 | }, 143 | point: { 144 | name: "Neptune", 145 | position: 122, 146 | }, 147 | precision: "2.0000", 148 | toPoint: { 149 | name: "Sun", 150 | position: 0, 151 | }, 152 | }, 153 | ]); 154 | }); 155 | }); 156 | 157 | describe('transit', () => { 158 | test('1', () => { 159 | var toPoints = { 160 | "Sun":[0], 161 | }; 162 | 163 | var transit = { 164 | "Sun":[1], 165 | } 166 | 167 | var settings = {...default_settings, ASPECTS: { 168 | "conjunction":{"degree":0, "orbit":10, "color":"transparent"}, 169 | "square":{"degree":90, "orbit":8, "color":"#FF4500"}, 170 | "trine":{"degree":120, "orbit":8, "color":"#27AE60"}, 171 | "opposition":{"degree":180, "orbit":10, "color":"#27AE60"} 172 | }}; 173 | 174 | var calculator = new AspectCalculator( toPoints, settings); 175 | 176 | expect(calculator.transit( transit )).toStrictEqual([ 177 | { 178 | "aspect": { 179 | "color": "transparent", 180 | "degree": 0, 181 | "name": "conjunction", 182 | "orbit": 10 183 | }, 184 | "point": { 185 | "name": "Sun", 186 | "position": 1 187 | }, 188 | "precision": "1.0000", 189 | "toPoint": { 190 | "name": "Sun", 191 | "position": 0 192 | } 193 | } 194 | ]); 195 | }) 196 | 197 | test('2', () => { 198 | var toPoints = { 199 | "Sun":[0], 200 | }; 201 | 202 | var transit = { 203 | "Sun":[359], 204 | } 205 | 206 | const settings = {...default_settings, ASPECTS: { 207 | "conjunction":{"degree":0, "orbit":10, "color":"transparent"}, 208 | "square":{"degree":90, "orbit":8, "color":"#FF4500"}, 209 | "trine":{"degree":120, "orbit":8, "color":"#27AE60"}, 210 | "opposition":{"degree":180, "orbit":10, "color":"#27AE60"} 211 | }} 212 | 213 | var calculator = new AspectCalculator( toPoints, settings); 214 | 215 | expect(calculator.transit( transit )).toStrictEqual([ 216 | { 217 | "aspect": { 218 | "color": "transparent", 219 | "degree": 0, 220 | "name": "conjunction", 221 | "orbit": 10 222 | }, 223 | "point": { 224 | "name": "Sun", 225 | "position": 359 226 | }, 227 | "precision": "-1.0000", 228 | "toPoint": { 229 | "name": "Sun", 230 | "position": 0 231 | } 232 | } 233 | ]); 234 | }) 235 | 236 | test('3', () => { 237 | var toPoints = { 238 | "Sun":[0], 239 | }; 240 | 241 | var transit = { 242 | "Sun":[91], 243 | } 244 | 245 | var settings = {...default_settings, ASPECTS: { 246 | "conjunction":{"degree":0, "orbit":10, "color":"transparent"}, 247 | "square":{"degree":90, "orbit":8, "color":"#FF4500"}, 248 | "trine":{"degree":120, "orbit":8, "color":"#27AE60"}, 249 | "opposition":{"degree":180, "orbit":10, "color":"#27AE60"} 250 | }} 251 | 252 | var calculator = new AspectCalculator( toPoints, settings); 253 | 254 | expect(calculator.transit( transit )).toStrictEqual( 255 | [ 256 | { 257 | "aspect": { 258 | "color": "#FF4500", 259 | "degree": 90, 260 | "name": "square", 261 | "orbit": 8 262 | }, 263 | "point": { 264 | "name": "Sun", 265 | "position": 91 266 | }, 267 | "precision": "1.0000", 268 | "toPoint": { 269 | "name": "Sun", 270 | "position": 0 271 | } 272 | } 273 | ]); 274 | }) 275 | 276 | test('4', () => { 277 | var toPoints = { 278 | "Sun":[0], 279 | }; 280 | 281 | var transit = { 282 | "Sun":[89], 283 | } 284 | 285 | var settings = {...default_settings, ASPECTS: { 286 | "conjunction":{"degree":0, "orbit":10, "color":"transparent"}, 287 | "square":{"degree":90, "orbit":8, "color":"#FF4500"}, 288 | "trine":{"degree":120, "orbit":8, "color":"#27AE60"}, 289 | "opposition":{"degree":180, "orbit":10, "color":"#27AE60"} 290 | }}; 291 | 292 | var calculator = new AspectCalculator( toPoints, settings); 293 | 294 | expect(calculator.transit( transit )).toStrictEqual([ 295 | { 296 | "aspect": { 297 | "color": "#FF4500", 298 | "degree": 90, 299 | "name": "square", 300 | "orbit": 8 301 | }, 302 | "point": { 303 | "name": "Sun", 304 | "position": 89 305 | }, 306 | "precision": "-1.0000", 307 | "toPoint": { 308 | "name": "Sun", 309 | "position": 0 310 | } 311 | } 312 | ]); 313 | }) 314 | 315 | test('5', () => { 316 | var toPoints = { 317 | "Sun":[0], 318 | }; 319 | 320 | var transit = { 321 | "Sun":[181], 322 | } 323 | 324 | var settings = {...default_settings, ASPECTS: { 325 | "conjunction":{"degree":0, "orbit":10, "color":"transparent"}, 326 | "square":{"degree":90, "orbit":8, "color":"#FF4500"}, 327 | "trine":{"degree":120, "orbit":8, "color":"#27AE60"}, 328 | "opposition":{"degree":180, "orbit":10, "color":"#27AE60"} 329 | }}; 330 | 331 | var calculator = new AspectCalculator( toPoints, settings); 332 | 333 | expect(calculator.transit( transit )).toStrictEqual([ 334 | { 335 | "aspect": { 336 | "color": "#27AE60", 337 | "degree": 180, 338 | "name": "opposition", 339 | "orbit": 10 340 | }, 341 | "point": { 342 | "name": "Sun", 343 | "position": 181 344 | }, 345 | "precision": "1.0000", 346 | "toPoint": { 347 | "name": "Sun", 348 | "position": 0 349 | } 350 | } 351 | ]); 352 | }) 353 | 354 | test('6', () => { 355 | var toPoints = { 356 | "Sun":[0], 357 | }; 358 | 359 | var transit = { 360 | "Sun":[179], 361 | } 362 | 363 | var settings = {...default_settings, ASPECTS: { 364 | "conjunction":{"degree":0, "orbit":10, "color":"transparent"}, 365 | "square":{"degree":90, "orbit":8, "color":"#FF4500"}, 366 | "trine":{"degree":120, "orbit":8, "color":"#27AE60"}, 367 | "opposition":{"degree":180, "orbit":10, "color":"#27AE60"} 368 | }}; 369 | 370 | var calculator = new AspectCalculator( toPoints, settings); 371 | 372 | expect(calculator.transit( transit )).toStrictEqual([ 373 | { 374 | "aspect": { 375 | "color": "#27AE60", 376 | "degree": 180, 377 | "name": "opposition", 378 | "orbit": 10 379 | }, 380 | "point": { 381 | "name": "Sun", 382 | "position": 179 383 | }, 384 | "precision": "-1.0000", 385 | "toPoint": { 386 | "name": "Sun", 387 | "position": 0 388 | } 389 | } 390 | ]); 391 | }) 392 | 393 | test('7', () => { 394 | var toPoints = { 395 | "Sun":[0], 396 | }; 397 | 398 | var transit = { 399 | "Sun":[271], 400 | } 401 | 402 | var settings = {...default_settings, ASPECTS: { 403 | "conjunction":{"degree":0, "orbit":10, "color":"transparent"}, 404 | "square":{"degree":90, "orbit":8, "color":"#FF4500"}, 405 | "trine":{"degree":120, "orbit":8, "color":"#27AE60"}, 406 | "opposition":{"degree":180, "orbit":10, "color":"#27AE60"} 407 | }}; 408 | 409 | var calculator = new AspectCalculator( toPoints, settings); 410 | 411 | expect(calculator.transit( transit )).toStrictEqual([ 412 | { 413 | "aspect": { 414 | "color": "#FF4500", 415 | "degree": 90, 416 | "name": "square", 417 | "orbit": 8 418 | }, 419 | "point": { 420 | "name": "Sun", 421 | "position": 271 422 | }, 423 | "precision": "1.0000", 424 | "toPoint": { 425 | "name": "Sun", 426 | "position": 0 427 | } 428 | } 429 | ]); 430 | }) 431 | 432 | test('8', () => { 433 | var toPoints = { 434 | "Sun":[0], 435 | }; 436 | 437 | var transit = { 438 | "Sun":[269], 439 | } 440 | 441 | var settings = {...default_settings, ASPECTS: { 442 | "conjunction":{"degree":0, "orbit":10, "color":"transparent"}, 443 | "square":{"degree":90, "orbit":8, "color":"#FF4500"}, 444 | "trine":{"degree":120, "orbit":8, "color":"#27AE60"}, 445 | "opposition":{"degree":180, "orbit":10, "color":"#27AE60"} 446 | }}; 447 | 448 | var calculator = new AspectCalculator( toPoints, settings); 449 | 450 | expect(calculator.transit( transit )).toStrictEqual([ 451 | { 452 | "aspect": { 453 | "color": "#FF4500", 454 | "degree": 90, 455 | "name": "square", 456 | "orbit": 8 457 | }, 458 | "point": { 459 | "name": "Sun", 460 | "position": 269 461 | }, 462 | "precision": "-1.0000", 463 | "toPoint": { 464 | "name": "Sun", 465 | "position": 0 466 | } 467 | } 468 | ]); 469 | }) 470 | 471 | test('9', () => { 472 | var toPoints = { 473 | "Sun":[359], 474 | }; 475 | 476 | var transit = { 477 | "Sun":[90], 478 | }; 479 | 480 | var settings = {...default_settings, ASPECTS: { 481 | "conjunction":{"degree":0, "orbit":10, "color":"transparent"}, 482 | "square":{"degree":90, "orbit":8, "color":"#FF4500"}, 483 | "trine":{"degree":120, "orbit":8, "color":"#27AE60"}, 484 | "opposition":{"degree":180, "orbit":10, "color":"#27AE60"} 485 | }}; 486 | 487 | var calculator = new AspectCalculator( toPoints, settings); 488 | 489 | expect(calculator.transit( transit )).toStrictEqual([{ 490 | "aspect": { 491 | "color": "#FF4500", 492 | "degree": 90, 493 | "name": "square", 494 | "orbit": 8 495 | }, 496 | "point": { 497 | "name": "Sun", 498 | "position": 90 499 | }, 500 | "precision": "1.0000", 501 | "toPoint": { 502 | "name": "Sun", 503 | "position": 359 504 | } 505 | }]); 506 | }) 507 | 508 | test('10', () => { 509 | var toPoints = { 510 | "Sun":[359], 511 | }; 512 | 513 | var transit = { 514 | "Sun":[88], 515 | }; 516 | 517 | var settings = {...default_settings, ASPECTS: { 518 | "conjunction":{"degree":0, "orbit":10, "color":"transparent"}, 519 | "square":{"degree":90, "orbit":8, "color":"#FF4500"}, 520 | "trine":{"degree":120, "orbit":8, "color":"#27AE60"}, 521 | "opposition":{"degree":180, "orbit":10, "color":"#27AE60"} 522 | }}; 523 | 524 | var calculator = new AspectCalculator( toPoints, settings); 525 | 526 | expect(calculator.transit( transit )).toStrictEqual([{ 527 | "aspect": { 528 | "color": "#FF4500", 529 | "degree": 90, 530 | "name": "square", 531 | "orbit": 8 532 | }, 533 | "point": { 534 | "name": "Sun", 535 | "position": 88 536 | }, 537 | "precision": "-1.0000", 538 | "toPoint": { 539 | "name": "Sun", 540 | "position": 359 541 | } 542 | }]); 543 | }) 544 | 545 | test('11 - speed', () => { 546 | var toPoints = { 547 | "Sun":[0], 548 | }; 549 | 550 | var transit = { 551 | "Sun":[1, 1], 552 | } 553 | 554 | var settings = {...default_settings, ASPECTS: { 555 | "conjunction":{"degree":0, "orbit":10, "color":"transparent"}, 556 | "square":{"degree":90, "orbit":8, "color":"#FF4500"}, 557 | "trine":{"degree":120, "orbit":8, "color":"#27AE60"}, 558 | "opposition":{"degree":180, "orbit":10, "color":"#27AE60"} 559 | }}; 560 | 561 | var calculator = new AspectCalculator( toPoints, settings); 562 | 563 | expect(calculator.transit( transit )).toStrictEqual([ { 564 | "aspect": { 565 | "color": "transparent", 566 | "degree": 0, 567 | "name": "conjunction", 568 | "orbit": 10 569 | }, 570 | "point": { 571 | "name": "Sun", 572 | "position": 1 573 | }, 574 | "precision": "1.0000", 575 | "toPoint": { 576 | "name": "Sun", 577 | "position": 0 578 | } 579 | }]); 580 | }) 581 | 582 | test('12 - speed', () => { 583 | var toPoints = { 584 | "Sun":[0], 585 | }; 586 | 587 | var transit = { 588 | "Sun":[1, -1], 589 | } 590 | 591 | var settings = {...default_settings, ASPECTS: { 592 | "conjunction":{"degree":0, "orbit":10, "color":"transparent"}, 593 | "square":{"degree":90, "orbit":8, "color":"#FF4500"}, 594 | "trine":{"degree":120, "orbit":8, "color":"#27AE60"}, 595 | "opposition":{"degree":180, "orbit":10, "color":"#27AE60"} 596 | }}; 597 | 598 | var calculator = new AspectCalculator( toPoints, settings); 599 | 600 | expect(calculator.transit( transit )).toStrictEqual([{ 601 | "aspect": { 602 | "color": "transparent", 603 | "degree": 0, 604 | "name": "conjunction", 605 | "orbit": 10 606 | }, 607 | "point": { 608 | "name": "Sun", 609 | "position": 1 610 | }, 611 | "precision": "-1.0000", 612 | "toPoint": { 613 | "name": "Sun", 614 | "position": 0 615 | } 616 | }]); 617 | }) 618 | 619 | }) 620 | -------------------------------------------------------------------------------- /project/src/aspect.ts: -------------------------------------------------------------------------------- 1 | import type { Points } from './radix' 2 | import type { AspectData, Settings } from './settings' 3 | import { radiansToDegree } from './utils' 4 | 5 | export interface FormedAspect { 6 | point: { 7 | name: string 8 | position: number 9 | } 10 | toPoint: { 11 | name: string 12 | position: number 13 | } 14 | aspect: { 15 | name: string 16 | degree: number 17 | color: string 18 | orbit: number 19 | } 20 | precision: string 21 | } 22 | 23 | const DEFAULT_ASPECTS = { 24 | conjunction: { degree: 0, orbit: 10, color: 'transparent' }, 25 | square: { degree: 90, orbit: 8, color: '#FF4500' }, 26 | trine: { degree: 120, orbit: 8, color: '#27AE60' }, 27 | opposition: { degree: 180, orbit: 10, color: '#27AE60' } 28 | } 29 | /** 30 | * Aspects calculator 31 | * 32 | * @class 33 | * @public 34 | * @constructor 35 | * @param {AspectPoints} points; {"Sun":[0], "Moon":[90], "Neptune":[120], "As":[30]} 36 | * @param {Object | null } settings 37 | */ 38 | class AspectCalculator { 39 | settings: Partial 40 | toPoints: Points 41 | context: this 42 | constructor (toPoints: Points, settings?: Partial) { 43 | if (toPoints == null) { 44 | throw new Error('Param \'toPoint\' must not be empty.') 45 | } 46 | 47 | this.settings = settings ?? {} 48 | this.settings.ASPECTS = settings?.ASPECTS ?? DEFAULT_ASPECTS 49 | 50 | this.toPoints = toPoints 51 | 52 | this.context = this 53 | } 54 | 55 | /** 56 | * Getter for this.toPoints 57 | * @see constructor 58 | * 59 | * @return {Object} 60 | */ 61 | getToPoints (): Points { 62 | return this.toPoints 63 | } 64 | 65 | /** 66 | * Radix aspects 67 | * 68 | * In radix calculation is the param "points" the same as param "toPoints" in constructor 69 | * , but without special points such as: As,Ds, Mc, Ic, ... 70 | * 71 | * @param {Object} points; {"Sun":[0], "Moon":[90]} 72 | * 73 | * @return {Array} [{"aspect":{"name":"conjunction", "degree":120}"", "point":{"name":"Sun", "position":123}, "toPoint":{"name":"Moon", "position":345}, "precision":0.5}]] 74 | */ 75 | radix (points: Points): FormedAspect[] { 76 | if (points == null) { 77 | return [] 78 | } 79 | 80 | const aspects: FormedAspect[] = [] 81 | 82 | for (const point in points) { 83 | if (points.hasOwnProperty(point)) { 84 | for (const toPoint in this.toPoints) { 85 | if (this.toPoints.hasOwnProperty(toPoint)) { 86 | if (point !== toPoint) { 87 | for (const aspect in this.settings.ASPECTS) { 88 | if (this.hasAspect(points[point][0], this.toPoints[toPoint][0], this.settings.ASPECTS[aspect])) { 89 | aspects.push( 90 | { 91 | aspect: { name: aspect, degree: this.settings.ASPECTS[aspect].degree, orbit: this.settings.ASPECTS[aspect].orbit, color: this.settings.ASPECTS[aspect].color }, 92 | point: { name: point, position: points[point][0] }, 93 | toPoint: { name: toPoint, position: this.toPoints[toPoint][0] }, 94 | precision: this.calcPrecision(points[point][0], this.toPoints[toPoint][0], this.settings.ASPECTS[aspect].degree).toFixed(4) 95 | } 96 | ) 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | return aspects.sort(this.compareAspectsByPrecision) 106 | } 107 | 108 | /** 109 | * Transit aspects 110 | * 111 | * @param {Object} points - transiting points; {"Sun":[0, 1], "Uranus":[90, -1], "NAME":[ANGLE, SPEED]}; 112 | * @return {Array} [{"aspect":{"name":"conjunction", "degree":120}"", "point":{"name":"Sun", "position":123}, "toPoint":{"name":"Moon", "position":345}, "precision":0.5}]] 113 | */ 114 | transit (points: Points): FormedAspect[] { 115 | if (points == null) { 116 | return [] 117 | } 118 | 119 | const aspects = [] 120 | 121 | for (const point in points) { 122 | if (points.hasOwnProperty(point)) { 123 | for (const toPoint in this.toPoints) { 124 | if (this.toPoints.hasOwnProperty(toPoint)) { 125 | for (const aspect in this.settings.ASPECTS) { 126 | if (this.hasAspect(points[point][0], this.toPoints[toPoint][0], this.settings.ASPECTS[aspect])) { 127 | let precision = this.calcPrecision(points[point][0], this.toPoints[toPoint][0], this.settings.ASPECTS[aspect].degree) 128 | 129 | // -1 : is approaching to aspect 130 | // +1 : is moving away 131 | if (this.isTransitPointApproachingToAspect(this.settings.ASPECTS[aspect].degree, this.toPoints[toPoint][0], points[point][0])) { 132 | precision *= -1 133 | } 134 | 135 | // if transit has speed value && transit is retrograde 136 | if (points[point][1] && points[point][1] < 0) { 137 | precision *= -1 138 | } 139 | 140 | aspects.push( 141 | { 142 | aspect: { name: aspect, degree: this.settings.ASPECTS[aspect].degree, orbit: this.settings.ASPECTS[aspect].orbit, color: this.settings.ASPECTS[aspect].color }, 143 | point: { name: point, position: points[point][0] }, 144 | toPoint: { name: toPoint, position: this.toPoints[toPoint][0] }, 145 | precision: precision.toFixed(4) 146 | } 147 | ) 148 | } 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | return aspects.sort(this.compareAspectsByPrecision) 156 | } 157 | 158 | /* 159 | * @private 160 | * @param {double} point 161 | * @param {double} toPoint 162 | * @param {Array} aspects; [DEGREE, ORBIT] 163 | */ 164 | hasAspect (point: number, toPoint: number, aspect: AspectData): boolean { 165 | let result = false 166 | 167 | let gap = Math.abs(point - toPoint) 168 | 169 | if (gap > radiansToDegree(Math.PI)) { 170 | gap = radiansToDegree(2 * Math.PI) - gap 171 | } 172 | 173 | const orbitMin = aspect.degree - (aspect.orbit / 2) 174 | const orbitMax = aspect.degree + (aspect.orbit / 2) 175 | 176 | if (orbitMin <= gap && gap <= orbitMax) { 177 | result = true 178 | } 179 | 180 | return result 181 | } 182 | 183 | /* 184 | * @private 185 | * @param {Object} pointAngle 186 | * @param {Object} toPointAngle 187 | * @param {double} aspectDegree; 188 | */ 189 | calcPrecision (point: number, toPoint: number, aspect: number): number { 190 | let gap = Math.abs(point - toPoint) 191 | 192 | if (gap > radiansToDegree(Math.PI)) { 193 | gap = radiansToDegree(2 * Math.PI) - gap 194 | } 195 | return Math.abs(gap - aspect) 196 | } 197 | 198 | /* 199 | * Calculate direction of aspect 200 | * whether the transiting planet is approaching or is falling 201 | * @private 202 | * 203 | * //TODO 204 | * This method is tested, and for tests gives the right results. 205 | * But the code is totally unclear. It needs to be rewritten. 206 | * @param {double} aspect - aspect degree; for example 90. 207 | * @param {double} toPoint - angle of standing point 208 | * @param {double} point - angle of transiting planet 209 | * @return {boolean} 210 | */ 211 | isTransitPointApproachingToAspect (aspect: number, toPoint: number, point: number): boolean { 212 | if ((point - toPoint) > 0) { 213 | if ((point - toPoint) > radiansToDegree(Math.PI)) { 214 | point = (point + aspect) % radiansToDegree(2 * Math.PI) 215 | } else { 216 | toPoint = (toPoint + aspect) % radiansToDegree(2 * Math.PI) 217 | } 218 | } else { 219 | if ((toPoint - point) > radiansToDegree(Math.PI)) { 220 | toPoint = (toPoint + aspect) % radiansToDegree(2 * Math.PI) 221 | } else { 222 | point = (point + aspect) % radiansToDegree(2 * Math.PI) 223 | } 224 | } 225 | 226 | let _point = point 227 | let _toPoint = toPoint 228 | 229 | const difference = _point - _toPoint 230 | 231 | if (Math.abs(difference) > radiansToDegree(Math.PI)) { 232 | _point = toPoint 233 | _toPoint = point 234 | } 235 | 236 | return (_point - _toPoint < 0) 237 | } 238 | 239 | /* 240 | * Aspects comparator 241 | * by precision 242 | * @private 243 | * @param {Object} a 244 | * @param {Object} b 245 | */ 246 | compareAspectsByPrecision (a: FormedAspect, b: FormedAspect): number { 247 | return parseFloat(a.precision) - parseFloat(b.precision) 248 | } 249 | } 250 | 251 | export default AspectCalculator 252 | -------------------------------------------------------------------------------- /project/src/chart.test.ts: -------------------------------------------------------------------------------- 1 | import Chart from "./chart" 2 | 3 | describe.only('constructor', () => { 4 | test('should throw error when empty houses', () => { 5 | const chart = new Chart('paper', 1, 1) 6 | 7 | expect(() => { 8 | chart.radix({planets: {}, cusps: []}) 9 | }).toThrowError(`Count of 'cusps' values has to be 12.`) 10 | }) 11 | 12 | test('should throw error when houses are less than 12', () => { 13 | const chart = new Chart('paper', 1, 1) 14 | 15 | expect(() => { 16 | chart.radix({planets: {}, cusps: [1, 2, 3]}) 17 | }).toThrowError(`Count of 'cusps' values has to be 12.`) 18 | }) 19 | 20 | }) -------------------------------------------------------------------------------- /project/src/chart.ts: -------------------------------------------------------------------------------- 1 | import default_settings from './settings' 2 | import type { Settings } from './settings' 3 | import Radix from './radix' 4 | import type { AstroData } from './radix' 5 | import SVG from './svg' 6 | import { getPointPosition } from './utils' 7 | /** 8 | * Displays astrology charts. 9 | * 10 | * @class 11 | * @public 12 | * @constructor 13 | * @param {String} elementId - root DOMElement 14 | * @param {int} width 15 | * @param {int} height 16 | * @param {Object} settings 17 | */ 18 | 19 | class Chart { 20 | paper: SVG 21 | cx: number 22 | cy: number 23 | radius: number 24 | settings: Settings 25 | constructor (elementId: string, width: number, height: number, settings?: Partial) { 26 | const chartSettings = default_settings 27 | if (settings != null) { 28 | Object.assign(chartSettings, settings) 29 | if (!('COLORS_SIGNS' in settings)) chartSettings.COLORS_SIGNS = [default_settings.COLOR_ARIES, default_settings.COLOR_TAURUS, default_settings.COLOR_GEMINI, default_settings.COLOR_CANCER, default_settings.COLOR_LEO, default_settings.COLOR_VIRGO, default_settings.COLOR_LIBRA, default_settings.COLOR_SCORPIO, default_settings.COLOR_SAGITTARIUS, default_settings.COLOR_CAPRICORN, default_settings.COLOR_AQUARIUS, default_settings.COLOR_PISCES] 30 | } 31 | 32 | if ((elementId !== '') && (document.getElementById(elementId) == null)) { 33 | const paper = document.createElement('div') 34 | paper.setAttribute('id', elementId) 35 | document.body.appendChild(paper) 36 | } 37 | 38 | this.paper = new SVG(elementId, width, height, chartSettings) 39 | this.cx = this.paper.width / 2 40 | this.cy = this.paper.height / 2 41 | this.radius = this.paper.height / 2 - chartSettings.MARGIN 42 | this.settings = chartSettings 43 | } 44 | 45 | /** 46 | * Display radix horoscope 47 | * 48 | * @param {Object} data 49 | * @example 50 | * { 51 | * "points":{"Moon":[0], "Sun":[30], ... }, 52 | * "cusps":[300, 340, 30, 60, 75, 90, 116, 172, 210, 236, 250, 274] 53 | * } 54 | * 55 | * @return {Radix} radix 56 | */ 57 | radix (data: AstroData): Radix { 58 | const radix = new Radix(this.paper, this.cx, this.cy, this.radius, data, this.settings) 59 | 60 | radix.drawBg() 61 | radix.drawUniverse() 62 | radix.drawRuler() 63 | radix.drawPoints() 64 | radix.drawCusps() 65 | radix.drawAxis() 66 | radix.drawCircles() 67 | 68 | return radix 69 | } 70 | 71 | /** 72 | * Scale chart 73 | * 74 | * @param {int} factor 75 | */ 76 | scale (factor: number): void { 77 | this.paper.root.setAttribute('transform', 'translate(' + (-this.cx * (factor - 1)) + ',' + (-this.cy * (factor - 1)) + ') scale(' + factor + ')') 78 | } 79 | 80 | /** 81 | * Draw the symbol on the axis. 82 | * For debug only. 83 | * 84 | */ 85 | calibrate (): Chart { 86 | let positions 87 | let circle 88 | let line 89 | const startRadius = 60 90 | 91 | const planets = ['Sun', 'Moon', 'Mercury', 'Venus', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto', 'Chiron', 'Lilith', 'NNode'] 92 | 93 | for (let planet = 0; planet < planets.length; planet++) { 94 | positions = getPointPosition(this.cx, this.cy, this.radius * 2, planet * 30, this.settings) 95 | 96 | line = this.paper.line(this.cx, this.cy, positions.x, positions.y) 97 | line.setAttribute('stroke', this.settings.LINE_COLOR) 98 | this.paper.root.appendChild(line) 99 | 100 | circle = this.paper.circle(this.cx, this.cy, startRadius + startRadius * planet) 101 | circle.setAttribute('stroke', this.settings.LINE_COLOR) 102 | circle.setAttribute('stroke-width', '1') 103 | this.paper.root.appendChild(circle) 104 | } 105 | 106 | for (let n = 0, ln = planets.length; n < ln; n++) { 107 | const radius = startRadius + startRadius * n 108 | 109 | for (let i = 0; i < 12; i++) { 110 | positions = getPointPosition(this.cx, this.cy, radius, i * 30, this.settings) 111 | 112 | circle = this.paper.circle(positions.x, positions.y, this.settings.COLLISION_RADIUS * this.settings.SYMBOL_SCALE) 113 | circle.setAttribute('stroke', 'red') 114 | circle.setAttribute('stroke-width', '1') 115 | this.paper.root.appendChild(circle) 116 | 117 | this.paper.root.appendChild(this.paper.getSymbol(planets[n], positions.x, positions.y)) 118 | } 119 | } 120 | 121 | return this 122 | } 123 | } 124 | 125 | export default Chart 126 | -------------------------------------------------------------------------------- /project/src/index.ts: -------------------------------------------------------------------------------- 1 | import Chart from './chart' 2 | import AspectCalculator from './aspect' 3 | import { Settings } from './settings' 4 | 5 | export { Chart, AspectCalculator, Settings } 6 | export default Chart -------------------------------------------------------------------------------- /project/src/radix.test.ts: -------------------------------------------------------------------------------- 1 | import Radix from './radix'; 2 | import SVG from './svg'; 3 | import default_settings from './settings'; 4 | 5 | describe('Radix', () => { 6 | const data = { 7 | planets: { 8 | Sun: [0], 9 | Moon: [90], 10 | }, 11 | cusps: [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330] 12 | }; 13 | 14 | it('should draw aspect lines with default colors', () => { 15 | document.body.innerHTML = '
'; 16 | const settings = default_settings; 17 | const paper = new SVG('chart', 500, 500, settings); 18 | const radix = new Radix(paper, 500, 500, 500, data, settings); 19 | radix.aspects(); 20 | 21 | const aspectLines = document.querySelectorAll('#chart-astrology-aspects > line'); 22 | expect(aspectLines[0].getAttribute('stroke')).toBe(default_settings.ASPECTS.square.color); 23 | }); 24 | 25 | it('should draw aspect lines with custom colors via settings', () => { 26 | document.body.innerHTML = '
'; 27 | 28 | const settings = { 29 | ...default_settings, 30 | ASPECTS: { 31 | square: { degree: 90, orbit: 10, color: 'purple' }, 32 | }, 33 | }; 34 | 35 | const paper = new SVG('chart', 500, 500, settings); 36 | const radix = new Radix(paper, 500, 500, 500, data, settings); 37 | radix.aspects(); 38 | 39 | const aspectLines = document.querySelectorAll('#chart-astrology-aspects > line'); 40 | expect(aspectLines[0].getAttribute('stroke')).toBe('purple'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /project/src/radix.ts: -------------------------------------------------------------------------------- 1 | import Zodiac from './zodiac' 2 | import AspectCalculator from './aspect' 3 | import type { FormedAspect } from './aspect' 4 | import Transit from './transit' 5 | import { 6 | validate 7 | , radiansToDegree 8 | , getEmptyWrapper 9 | , getPointPosition 10 | , getRulerPositions 11 | , getDescriptionPosition 12 | , getDashedLinesPositions 13 | , assemble 14 | } from './utils' 15 | import type SVG from './svg' 16 | import type { Settings } from './settings' 17 | 18 | export type Points = Record 19 | export interface LocatedPoint { name: string; x: number; y: number; r: number; angle: number; pointer?: number; index?: number } 20 | export interface AstroData { 21 | planets: Points 22 | cusps: number[] 23 | } 24 | 25 | /** 26 | * Radix charts. 27 | * 28 | * @class 29 | * @public 30 | * @constructor 31 | * @param {this.settings.SVG} paper 32 | * @param {int} cx 33 | * @param {int} cy 34 | * @param {int} radius 35 | * @param {Object} data 36 | */ 37 | class Radix { 38 | settings: Settings 39 | data: AstroData 40 | paper: SVG 41 | cx: number 42 | cy: number 43 | radius: number 44 | locatedPoints: LocatedPoint[] 45 | rulerRadius: number 46 | pointRadius: number 47 | toPoints: Points 48 | shift: number 49 | universe: Element 50 | context: this 51 | constructor(paper: SVG, cx: number, cy: number, radius: number, data: AstroData, settings: Settings) { 52 | this.settings = settings 53 | // Validate data 54 | const status = validate(data) 55 | if (status.hasError) { 56 | throw new Error(status.messages.join(' | ')) 57 | } 58 | 59 | this.data = data 60 | this.paper = paper 61 | this.cx = cx 62 | this.cy = cy 63 | this.radius = radius 64 | 65 | // after calling this.drawPoints() it contains current position of point 66 | this.locatedPoints = [] 67 | this.rulerRadius = ((this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO) / this.settings.RULER_RADIUS) 68 | this.pointRadius = this.radius - (this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO + 2 * this.rulerRadius + (this.settings.PADDING * this.settings.SYMBOL_SCALE)) 69 | 70 | // @see aspects() 71 | // @see setPointsOfInterest() 72 | this.toPoints = JSON.parse(JSON.stringify(this.data.planets)) // Clone object 73 | 74 | this.shift = 0 75 | if (this.data.cusps && this.data.cusps[0]) { 76 | const deg360 = radiansToDegree(2 * Math.PI) 77 | this.shift = deg360 - this.data.cusps[0] 78 | } 79 | 80 | // preparing wrapper for aspects. It is the lowest layer 81 | const divisionForAspects = document.createElementNS(this.paper.root.namespaceURI, 'g') 82 | divisionForAspects.setAttribute('id', this.paper.root.id + '-' + this.settings.ID_ASPECTS) 83 | this.paper.root.appendChild(divisionForAspects) 84 | 85 | this.universe = document.createElementNS(this.paper.root.namespaceURI, 'g') 86 | this.universe.setAttribute('id', this.paper.root.id + '-' + this.settings.ID_RADIX) 87 | this.paper.root.appendChild(this.universe) 88 | 89 | this.context = this 90 | } 91 | 92 | /** 93 | * Draw background 94 | */ 95 | drawBg(): void { 96 | const universe = this.universe 97 | const wrapper = getEmptyWrapper(universe, this.paper.root.id + '-' + this.settings.ID_BG, this.paper.root.id) 98 | 99 | const LARGE_ARC_FLAG = 1 100 | const start = 0 // degree 101 | const end = 359.99 // degree 102 | const hemisphere = this.paper.segment(this.cx, this.cy, this.radius - this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO, start, end, this.radius / this.settings.INDOOR_CIRCLE_RADIUS_RATIO, LARGE_ARC_FLAG) 103 | hemisphere.setAttribute('fill', this.settings.STROKE_ONLY ? 'none' : this.settings.COLOR_BACKGROUND) 104 | wrapper.appendChild(hemisphere) 105 | } 106 | 107 | /** 108 | * Draw universe. 109 | */ 110 | drawUniverse(): void { 111 | const universe = this.universe 112 | const wrapper = getEmptyWrapper(universe, this.paper.root.id + '-' + this.settings.ID_RADIX + '-' + this.settings.ID_SIGNS, this.paper.root.id) 113 | 114 | // colors 115 | for (let i = 0, step = 30, start = this.shift, len = this.settings.COLORS_SIGNS.length; i < len; i++) { 116 | const segment = this.paper.segment(this.cx, this.cy, this.radius, start, start + step, this.radius - this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO) 117 | segment.setAttribute('fill', this.settings.STROKE_ONLY ? 'none' : this.settings.COLORS_SIGNS[i]) 118 | segment.setAttribute('id', this.paper.root.id + '-' + this.settings.ID_RADIX + '-' + this.settings.ID_SIGNS + '-' + i) 119 | segment.setAttribute('stroke', this.settings.STROKE_ONLY ? this.settings.CIRCLE_COLOR : 'none') 120 | segment.setAttribute('stroke-width', this.settings.STROKE_ONLY ? '1' : '0') 121 | wrapper.appendChild(segment) 122 | 123 | start += step 124 | } 125 | 126 | // signs 127 | for (let i = 0, step = 30, start = 15 + this.shift, len = this.settings.SYMBOL_SIGNS.length; i < len; i++) { 128 | const position = getPointPosition(this.cx, this.cy, this.radius - (this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO) / 2, start, this.settings) 129 | wrapper.appendChild(this.paper.getSymbol(this.settings.SYMBOL_SIGNS[i], position.x, position.y)) 130 | start += step 131 | } 132 | } 133 | 134 | /** 135 | * Draw points 136 | */ 137 | drawPoints(): void { 138 | if (this.data.planets == null) { 139 | return 140 | } 141 | 142 | const universe = this.universe 143 | const wrapper = getEmptyWrapper(universe, this.paper.root.id + '-' + this.settings.ID_RADIX + '-' + this.settings.ID_POINTS, this.paper.root.id) 144 | 145 | const gap = this.radius - (this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO + this.radius / this.settings.INDOOR_CIRCLE_RADIUS_RATIO) 146 | const step = (gap - 2 * (this.settings.PADDING * this.settings.SYMBOL_SCALE)) / Object.keys(this.data.planets).length 147 | 148 | const pointerRadius = this.radius - (this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO + this.rulerRadius) 149 | let startPosition 150 | let endPosition 151 | 152 | for (const planet in this.data.planets) { 153 | if (this.data.planets.hasOwnProperty(planet)) { 154 | const position = getPointPosition(this.cx, this.cy, this.pointRadius, this.data.planets[planet][0] + this.shift, this.settings) 155 | const point = { name: planet, x: position.x, y: position.y, r: (this.settings.COLLISION_RADIUS * this.settings.SYMBOL_SCALE), angle: this.data.planets[planet][0] + this.shift, pointer: this.data.planets[planet][0] + this.shift } 156 | this.locatedPoints = assemble(this.locatedPoints, point, { cx: this.cx, cy: this.cy, r: this.pointRadius }, this.settings) 157 | } 158 | } 159 | 160 | if (this.settings.DEBUG) console.log('Radix count of points: ' + this.locatedPoints.length) 161 | if (this.settings.DEBUG) console.log('Radix located points:\n' + JSON.stringify(this.locatedPoints)) 162 | 163 | this.locatedPoints.forEach(function (point: LocatedPoint) { 164 | // draw pointer 165 | startPosition = getPointPosition(this.cx, this.cy, pointerRadius, this.data.planets[point.name][0] + this.shift, this.settings) 166 | endPosition = getPointPosition(this.cx, this.cy, pointerRadius - this.rulerRadius / 2, this.data.planets[point.name][0] + this.shift, this.settings) 167 | const pointer = this.paper.line(startPosition.x, startPosition.y, endPosition.x, endPosition.y) 168 | pointer.setAttribute('stroke', this.settings.CIRCLE_COLOR) 169 | pointer.setAttribute('stroke-width', (this.settings.CUSPS_STROKE * this.settings.SYMBOL_SCALE)) 170 | wrapper.appendChild(pointer) 171 | 172 | // draw pointer line 173 | if (!this.settings.STROKE_ONLY && (this.data.planets[point.name][0] + this.shift) !== point.angle) { 174 | startPosition = endPosition 175 | endPosition = getPointPosition(this.cx, this.cy, this.pointRadius + (this.settings.COLLISION_RADIUS * this.settings.SYMBOL_SCALE), point.angle, this.settings) 176 | const line = this.paper.line(startPosition.x, startPosition.y, endPosition.x, endPosition.y) 177 | line.setAttribute('stroke', this.settings.LINE_COLOR) 178 | line.setAttribute('stroke-width', 0.5 * (this.settings.CUSPS_STROKE * this.settings.SYMBOL_SCALE)) 179 | wrapper.appendChild(line) 180 | } 181 | 182 | // draw symbol 183 | const symbol = this.paper.getSymbol(point.name, point.x, point.y) 184 | symbol.setAttribute('id', this.paper.root.id + '-' + this.settings.ID_RADIX + '-' + this.settings.ID_POINTS + '-' + point.name) 185 | wrapper.appendChild(symbol) 186 | 187 | // draw point descriptions 188 | let textsToShow = [(Math.floor(this.data.planets[point.name][0]) % 30).toString()] 189 | 190 | const zodiac = new Zodiac(this.data.cusps, this.settings) 191 | 192 | if (this.data.planets[point.name][1] && zodiac.isRetrograde(this.data.planets[point.name][1])) { 193 | textsToShow.push('R') 194 | } else { 195 | textsToShow.push('') 196 | } 197 | 198 | if (this.settings.SHOW_DIGNITIES_TEXT) 199 | textsToShow = textsToShow.concat(zodiac.getDignities({ name: point.name, position: this.data.planets[point.name][0] }, this.settings.DIGNITIES_EXACT_EXALTATION_DEFAULT).join(',')) 200 | 201 | const pointDescriptions = getDescriptionPosition(point, textsToShow, this.settings) 202 | pointDescriptions.forEach(function (dsc) { 203 | wrapper.appendChild(this.paper.text(dsc.text, dsc.x, dsc.y, this.settings.POINTS_TEXT_SIZE, this.settings.SIGNS_COLOR)) 204 | }, this) 205 | }, this) 206 | } 207 | 208 | drawAxis(): void { 209 | if (this.data.cusps == null) { 210 | return 211 | } 212 | 213 | const universe = this.universe 214 | const wrapper = getEmptyWrapper(universe, this.paper.root.id + '-' + this.settings.ID_RADIX + '-' + this.settings.ID_AXIS, this.paper.root.id) 215 | 216 | const axisRadius = this.radius + ((this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO) / 4) 217 | 218 | const AS = 0 219 | const IC = 3 220 | const DC = 6 221 | const MC = 9 222 | let overlapLine 223 | let startPosition 224 | let endPosition 225 | 226 | [AS, IC, DC, MC].forEach(function (i) { 227 | let textPosition 228 | // overlap 229 | startPosition = getPointPosition(this.cx, this.cy, this.radius, this.data.cusps[i] + this.shift, this.settings) 230 | endPosition = getPointPosition(this.cx, this.cy, axisRadius, this.data.cusps[i] + this.shift, this.settings) 231 | overlapLine = this.paper.line(startPosition.x, startPosition.y, endPosition.x, endPosition.y) 232 | overlapLine.setAttribute('stroke', this.settings.LINE_COLOR) 233 | overlapLine.setAttribute('stroke-width', (this.settings.SYMBOL_AXIS_STROKE * this.settings.SYMBOL_SCALE)) 234 | wrapper.appendChild(overlapLine) 235 | 236 | // As 237 | if (i === AS) { 238 | // Text 239 | textPosition = getPointPosition(this.cx, this.cy, axisRadius + (20 * this.settings.SYMBOL_SCALE), this.data.cusps[i] + this.shift, this.settings) 240 | wrapper.appendChild(this.paper.getSymbol(this.settings.SYMBOL_AS, textPosition.x, textPosition.y)) 241 | } 242 | 243 | // Ds 244 | if (i === DC) { 245 | // Text 246 | textPosition = getPointPosition(this.cx, this.cy, axisRadius + (2 * this.settings.SYMBOL_SCALE), this.data.cusps[i] + this.shift, this.settings) 247 | wrapper.appendChild(this.paper.getSymbol(this.settings.SYMBOL_DS, textPosition.x, textPosition.y)) 248 | } 249 | 250 | // Ic 251 | if (i === IC) { 252 | // Text 253 | textPosition = getPointPosition(this.cx, this.cy, axisRadius + (10 * this.settings.SYMBOL_SCALE), this.data.cusps[i] - 2 + this.shift, this.settings) 254 | wrapper.appendChild(this.paper.getSymbol(this.settings.SYMBOL_IC, textPosition.x, textPosition.y)) 255 | } 256 | 257 | // Mc 258 | if (i === MC) { 259 | // Text 260 | textPosition = getPointPosition(this.cx, this.cy, axisRadius + (10 * this.settings.SYMBOL_SCALE), this.data.cusps[i] + 2 + this.shift, this.settings) 261 | wrapper.appendChild(this.paper.getSymbol(this.settings.SYMBOL_MC, textPosition.x, textPosition.y)) 262 | } 263 | }, this) 264 | } 265 | 266 | /** 267 | * Draw cusps 268 | */ 269 | drawCusps(): void { 270 | if (this.data.cusps == null) { 271 | return 272 | } 273 | 274 | let lines 275 | const universe = this.universe 276 | const wrapper = getEmptyWrapper(universe, this.paper.root.id + '-' + this.settings.ID_RADIX + '-' + this.settings.ID_CUSPS, this.paper.root.id) 277 | 278 | const numbersRadius = this.radius / this.settings.INDOOR_CIRCLE_RADIUS_RATIO + (this.settings.COLLISION_RADIUS * this.settings.SYMBOL_SCALE) 279 | 280 | const AS = 0 281 | const IC = 3 282 | const DC = 6 283 | const MC = 9 284 | const mainAxis = [AS, IC, DC, MC] 285 | 286 | // Cusps 287 | for (let i = 0, ln = this.data.cusps.length; i < ln; i++) { 288 | // Draws a dashed line when an point is in the way 289 | lines = getDashedLinesPositions( 290 | this.cx, 291 | this.cy, 292 | this.data.cusps[i] + this.shift, 293 | this.radius / this.settings.INDOOR_CIRCLE_RADIUS_RATIO, 294 | this.radius - (this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO + this.rulerRadius), 295 | this.pointRadius, 296 | this.locatedPoints, 297 | this.settings 298 | ) 299 | 300 | lines.forEach(function (line) { 301 | const newLine = this.paper.line(line.startX, line.startY, line.endX, line.endY) 302 | newLine.setAttribute('stroke', this.settings.LINE_COLOR) 303 | 304 | if (mainAxis.includes(i)) { 305 | newLine.setAttribute('stroke-width', (this.settings.SYMBOL_AXIS_STROKE * this.settings.SYMBOL_SCALE)) 306 | } else { 307 | newLine.setAttribute('stroke-width', (this.settings.CUSPS_STROKE * this.settings.SYMBOL_SCALE)) 308 | } 309 | 310 | wrapper.appendChild(newLine) 311 | }, this) 312 | 313 | // Cup number 314 | const deg360 = radiansToDegree(2 * Math.PI) 315 | const startOfCusp = this.data.cusps[i] 316 | const endOfCusp = this.data.cusps[(i + 1) % 12] 317 | const gap = endOfCusp - startOfCusp > 0 ? endOfCusp - startOfCusp : endOfCusp - startOfCusp + deg360 318 | const textPosition = getPointPosition(this.cx, this.cy, numbersRadius, ((startOfCusp + gap / 2) % deg360) + this.shift, this.settings) 319 | wrapper.appendChild(this.paper.getSymbol((i + 1).toString(), textPosition.x, textPosition.y)) 320 | } 321 | } 322 | 323 | /** 324 | * Draw aspects 325 | * @param{Array | null} customAspects - posible custom aspects to draw; 326 | */ 327 | aspects(customAspects?: FormedAspect[] | null): Radix { 328 | const aspectsList = customAspects != null && Array.isArray(customAspects) 329 | ? customAspects 330 | : new AspectCalculator(this.toPoints, this.settings).radix(this.data.planets) 331 | 332 | const universe = this.universe 333 | const wrapper = getEmptyWrapper(universe, this.paper.root.id + '-' + this.settings.ID_ASPECTS, this.paper.root.id) 334 | 335 | const duplicateCheck: string[] = [] 336 | 337 | for (let i = 0, ln = aspectsList.length; i < ln; i++) { 338 | const key = aspectsList[i].aspect.name + '-' + aspectsList[i].point.name + '-' + aspectsList[i].toPoint.name 339 | const opositeKey = aspectsList[i].aspect.name + '-' + aspectsList[i].toPoint.name + '-' + aspectsList[i].point.name 340 | if (!duplicateCheck.includes(opositeKey)) { 341 | duplicateCheck.push(key) 342 | 343 | const startPoint = getPointPosition(this.cx, this.cy, this.radius / this.settings.INDOOR_CIRCLE_RADIUS_RATIO, aspectsList[i].toPoint.position + this.shift, this.settings) 344 | const endPoint = getPointPosition(this.cx, this.cy, this.radius / this.settings.INDOOR_CIRCLE_RADIUS_RATIO, aspectsList[i].point.position + this.shift, this.settings) 345 | 346 | const line = this.paper.line(startPoint.x, startPoint.y, endPoint.x, endPoint.y) 347 | line.setAttribute('stroke', this.settings.STROKE_ONLY ? this.settings.LINE_COLOR : aspectsList[i].aspect.color) 348 | line.setAttribute('stroke-width', (this.settings.CUSPS_STROKE * this.settings.SYMBOL_SCALE).toString()) 349 | 350 | line.setAttribute('data-name', aspectsList[i].aspect.name) 351 | line.setAttribute('data-degree', aspectsList[i].aspect.degree.toString()) 352 | line.setAttribute('data-point', aspectsList[i].point.name) 353 | line.setAttribute('data-toPoint', aspectsList[i].toPoint.name) 354 | line.setAttribute('data-precision', aspectsList[i].precision.toString()) 355 | 356 | wrapper.appendChild(line) 357 | } 358 | } 359 | 360 | return this.context 361 | } 362 | 363 | /** 364 | * Add points of interest for aspects calculation 365 | * @param {Obect} points, {"As":[0],"Ic":[90],"Ds":[180],"Mc":[270]} 366 | * @see (this.settings.AspectCalculator( toPoints) ) 367 | */ 368 | addPointsOfInterest(points: Points): Radix { 369 | for (const point in points) { 370 | if (points.hasOwnProperty(point)) { 371 | this.toPoints[point] = points[point] 372 | } 373 | } 374 | 375 | return this.context 376 | } 377 | 378 | drawRuler(): void { 379 | const universe = this.universe 380 | const wrapper = getEmptyWrapper(universe, this.paper.root.id + '-' + this.settings.ID_RADIX + '-' + this.settings.ID_RULER, this.paper.root.id) 381 | 382 | const startRadius = (this.radius - (this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO + this.rulerRadius)) 383 | const rays = getRulerPositions(this.cx, this.cy, startRadius, startRadius + this.rulerRadius, this.shift, this.settings) 384 | 385 | rays.forEach(function (ray) { 386 | const line = this.paper.line(ray.startX, ray.startY, ray.endX, ray.endY) 387 | line.setAttribute('stroke', this.settings.CIRCLE_COLOR) 388 | line.setAttribute('stroke-width', (this.settings.CUSPS_STROKE * this.settings.SYMBOL_SCALE)) 389 | wrapper.appendChild(line) 390 | }, this) 391 | 392 | const circle = this.paper.circle(this.cx, this.cy, startRadius) 393 | circle.setAttribute('stroke', this.settings.CIRCLE_COLOR) 394 | circle.setAttribute('stroke-width', (this.settings.CUSPS_STROKE * this.settings.SYMBOL_SCALE).toString()) 395 | wrapper.appendChild(circle) 396 | } 397 | 398 | /** 399 | * Draw circles 400 | */ 401 | drawCircles(): void { 402 | const universe = this.universe 403 | const wrapper = getEmptyWrapper(universe, this.paper.root.id + '-' + this.settings.ID_RADIX + '-' + this.settings.ID_CIRCLES, this.paper.root.id) 404 | 405 | // indoor circle 406 | let circle = this.paper.circle(this.cx, this.cy, this.radius / this.settings.INDOOR_CIRCLE_RADIUS_RATIO) 407 | circle.setAttribute('stroke', this.settings.CIRCLE_COLOR) 408 | circle.setAttribute('stroke-width', (this.settings.CIRCLE_STRONG * this.settings.SYMBOL_SCALE).toString()) 409 | wrapper.appendChild(circle) 410 | 411 | // outdoor circle 412 | circle = this.paper.circle(this.cx, this.cy, this.radius) 413 | circle.setAttribute('stroke', this.settings.CIRCLE_COLOR) 414 | circle.setAttribute('stroke-width', (this.settings.CIRCLE_STRONG * this.settings.SYMBOL_SCALE).toString()) 415 | wrapper.appendChild(circle) 416 | 417 | // inner circle 418 | circle = this.paper.circle(this.cx, this.cy, this.radius - this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO) 419 | circle.setAttribute('stroke', this.settings.CIRCLE_COLOR) 420 | circle.setAttribute('stroke-width', (this.settings.CIRCLE_STRONG * this.settings.SYMBOL_SCALE).toString()) 421 | wrapper.appendChild(circle) 422 | } 423 | 424 | /** 425 | * Display transit horoscope 426 | * 427 | * @param {Object} data 428 | * @example 429 | * { 430 | * "planets":{"Moon":[0], "Sun":[30], ... }, 431 | * "cusps":[300, 340, 30, 60, 75, 90, 116, 172, 210, 236, 250, 274], * 432 | * } 433 | * 434 | * @return {Transit} transit 435 | */ 436 | transit(data: AstroData): Transit { 437 | // remove axis (As, Ds, Mc, Ic) from radix 438 | getEmptyWrapper(this.universe, this.paper.root.id + '-' + this.settings.ID_RADIX + '-' + this.settings.ID_AXIS, this.paper.root.id) 439 | const transit = new Transit(this.context, data, this.settings) 440 | transit.drawBg() 441 | transit.drawPoints() 442 | transit.drawCusps() 443 | transit.drawRuler() 444 | transit.drawCircles() 445 | return transit 446 | } 447 | } 448 | 449 | export default Radix 450 | -------------------------------------------------------------------------------- /project/src/settings.ts: -------------------------------------------------------------------------------- 1 | import type SVG from './svg' 2 | 3 | export interface AspectData { degree: number; orbit: number; color: string } 4 | export type Aspect = Record 5 | export interface Dignity { 6 | name: string 7 | position: number 8 | orbit: number 9 | } 10 | 11 | export interface Settings { 12 | SYMBOL_SCALE: number 13 | COLOR_BACKGROUND: string 14 | POINTS_COLOR: string 15 | POINTS_TEXT_SIZE: number 16 | POINTS_STROKE: number 17 | SIGNS_COLOR: string 18 | SIGNS_STROKE: number 19 | MARGIN: number 20 | PADDING: number 21 | ID_CHART: string 22 | ID_RADIX: string 23 | ID_TRANSIT: string 24 | ID_ASPECTS: string 25 | ID_POINTS: string 26 | ID_SIGNS: string 27 | ID_CIRCLES: string 28 | ID_AXIS: string 29 | ID_CUSPS: string 30 | ID_RULER: string 31 | ID_BG: string 32 | CIRCLE_COLOR: string 33 | CIRCLE_STRONG: number 34 | LINE_COLOR: string 35 | INDOOR_CIRCLE_RADIUS_RATIO: number 36 | INNER_CIRCLE_RADIUS_RATIO: number 37 | RULER_RADIUS: number 38 | SYMBOL_SUN: string 39 | SYMBOL_MOON: string 40 | SYMBOL_MERCURY: string 41 | SYMBOL_VENUS: string 42 | SYMBOL_MARS: string 43 | SYMBOL_JUPITER: string 44 | SYMBOL_SATURN: string 45 | SYMBOL_URANUS: string 46 | SYMBOL_NEPTUNE: string 47 | SYMBOL_PLUTO: string 48 | SYMBOL_CHIRON: string 49 | SYMBOL_LILITH: string 50 | SYMBOL_NNODE: string 51 | SYMBOL_SNODE: string 52 | SYMBOL_FORTUNE: string 53 | SYMBOL_AS: string 54 | SYMBOL_DS: string 55 | SYMBOL_MC: string 56 | SYMBOL_IC: string 57 | SYMBOL_AXIS_FONT_COLOR: string 58 | SYMBOL_AXIS_STROKE: number 59 | SYMBOL_CUSP_1: string 60 | SYMBOL_CUSP_2: string 61 | SYMBOL_CUSP_3: string 62 | SYMBOL_CUSP_4: string 63 | SYMBOL_CUSP_5: string 64 | SYMBOL_CUSP_6: string 65 | SYMBOL_CUSP_7: string 66 | SYMBOL_CUSP_8: string 67 | SYMBOL_CUSP_9: string 68 | SYMBOL_CUSP_10: string 69 | SYMBOL_CUSP_11: string 70 | SYMBOL_CUSP_12: string 71 | CUSPS_STROKE: number 72 | CUSPS_FONT_COLOR: string 73 | SYMBOL_ARIES: string 74 | SYMBOL_TAURUS: string 75 | SYMBOL_GEMINI: string 76 | SYMBOL_CANCER: string 77 | SYMBOL_LEO: string 78 | SYMBOL_VIRGO: string 79 | SYMBOL_LIBRA: string 80 | SYMBOL_SCORPIO: string 81 | SYMBOL_SAGITTARIUS: string 82 | SYMBOL_CAPRICORN: string 83 | SYMBOL_AQUARIUS: string 84 | SYMBOL_PISCES: string 85 | SYMBOL_SIGNS: string[] 86 | COLOR_ARIES: string 87 | COLOR_TAURUS: string 88 | COLOR_GEMINI: string 89 | COLOR_CANCER: string 90 | COLOR_LEO: string 91 | COLOR_VIRGO: string 92 | COLOR_LIBRA: string 93 | COLOR_SCORPIO: string 94 | COLOR_SAGITTARIUS: string 95 | COLOR_CAPRICORN: string 96 | COLOR_AQUARIUS: string 97 | COLOR_PISCES: string 98 | COLORS_SIGNS: string[] 99 | CUSTOM_SYMBOL_FN: null | ((name: string, x: number, y: number, context: SVG) => Element) 100 | SHIFT_IN_DEGREES: number 101 | STROKE_ONLY: boolean 102 | ADD_CLICK_AREA: boolean 103 | COLLISION_RADIUS: number 104 | ASPECTS: Aspect 105 | SHOW_DIGNITIES_TEXT: boolean 106 | DIGNITIES_RULERSHIP: string 107 | DIGNITIES_DETRIMENT: string 108 | DIGNITIES_EXALTATION: string 109 | DIGNITIES_EXACT_EXALTATION: string 110 | DIGNITIES_FALL: string 111 | DIGNITIES_EXACT_EXALTATION_DEFAULT: Dignity[] 112 | ANIMATION_CUSPS_ROTATION_SPEED: number 113 | DEBUG: boolean 114 | } 115 | 116 | const settings: Settings = { 117 | 118 | // Scale of symbols 119 | SYMBOL_SCALE: 1, 120 | 121 | // BG color 122 | COLOR_BACKGROUND: '#fff', 123 | 124 | // Color of planet's symbols 125 | POINTS_COLOR: '#000', 126 | 127 | // Size of description text next to the planet: angle, retrograde, dignities, ... 128 | POINTS_TEXT_SIZE: 8, 129 | 130 | // Points strength of lines 131 | POINTS_STROKE: 1.8, 132 | 133 | // Font color of signs symbols 134 | SIGNS_COLOR: '#000', 135 | 136 | // Signs strength of lines 137 | SIGNS_STROKE: 1.5, 138 | 139 | // Chart margin 140 | MARGIN: 50, // px 141 | 142 | // Chart Padding 143 | PADDING: 18, // px 144 | 145 | // Module wrapper element ID 146 | ID_CHART: 'astrology', 147 | 148 | // Radix chart element ID 149 | ID_RADIX: 'radix', 150 | 151 | // Transit chart element ID 152 | ID_TRANSIT: 'transit', 153 | 154 | // Aspects wrapper element ID 155 | ID_ASPECTS: 'aspects', 156 | 157 | // Aspects wrapper element ID 158 | ID_POINTS: 'planets', 159 | 160 | // Signs wrapper element ID 161 | ID_SIGNS: 'signs', 162 | 163 | // Circles wrapper element ID 164 | ID_CIRCLES: 'circles', 165 | 166 | // Axis wrapper element ID 167 | ID_AXIS: 'axis', 168 | 169 | // Cusps wrapper element ID 170 | ID_CUSPS: 'cusps', 171 | 172 | // Cusps wrapper element ID 173 | ID_RULER: 'ruler', 174 | 175 | // Background wrapper element ID 176 | ID_BG: 'bg', 177 | 178 | // Color of circles in charts 179 | CIRCLE_COLOR: '#333', 180 | 181 | // Circles strength of lines 182 | CIRCLE_STRONG: 2, 183 | 184 | // Color of lines in charts 185 | LINE_COLOR: '#333', 186 | 187 | // radius / INDOOR_CIRCLE_RADIUS_RATIO 188 | INDOOR_CIRCLE_RADIUS_RATIO: 2, 189 | 190 | // radius - radius/INNER_CIRCLE_RADIUS_RATIO 191 | INNER_CIRCLE_RADIUS_RATIO: 8, 192 | 193 | // ( radius / INNER_CIRCLE_RADIUS_RATIO ) / RULER_RADIUS 194 | RULER_RADIUS: 4, 195 | 196 | // Points 197 | SYMBOL_SUN: 'Sun', 198 | SYMBOL_MOON: 'Moon', 199 | SYMBOL_MERCURY: 'Mercury', 200 | SYMBOL_VENUS: 'Venus', 201 | SYMBOL_MARS: 'Mars', 202 | SYMBOL_JUPITER: 'Jupiter', 203 | SYMBOL_SATURN: 'Saturn', 204 | SYMBOL_URANUS: 'Uranus', 205 | SYMBOL_NEPTUNE: 'Neptune', 206 | SYMBOL_PLUTO: 'Pluto', 207 | SYMBOL_CHIRON: 'Chiron', 208 | SYMBOL_LILITH: 'Lilith', 209 | SYMBOL_NNODE: 'NNode', 210 | SYMBOL_SNODE: 'SNode', 211 | SYMBOL_FORTUNE: 'Fortune', 212 | 213 | // Axis 214 | SYMBOL_AS: 'As', 215 | SYMBOL_DS: 'Ds', 216 | SYMBOL_MC: 'Mc', 217 | SYMBOL_IC: 'Ic', 218 | 219 | SYMBOL_AXIS_FONT_COLOR: '#333', 220 | SYMBOL_AXIS_STROKE: 1.6, 221 | 222 | // Cusps 223 | SYMBOL_CUSP_1: '1', 224 | SYMBOL_CUSP_2: '2', 225 | SYMBOL_CUSP_3: '3', 226 | SYMBOL_CUSP_4: '4', 227 | SYMBOL_CUSP_5: '5', 228 | SYMBOL_CUSP_6: '6', 229 | SYMBOL_CUSP_7: '7', 230 | SYMBOL_CUSP_8: '8', 231 | SYMBOL_CUSP_9: '9', 232 | SYMBOL_CUSP_10: '10', 233 | SYMBOL_CUSP_11: '11', 234 | SYMBOL_CUSP_12: '12', 235 | 236 | // Cusps strength of lines 237 | CUSPS_STROKE: 1, 238 | CUSPS_FONT_COLOR: '#000', 239 | 240 | // Signs 241 | SYMBOL_ARIES: 'Aries', 242 | SYMBOL_TAURUS: 'Taurus', 243 | SYMBOL_GEMINI: 'Gemini', 244 | SYMBOL_CANCER: 'Cancer', 245 | SYMBOL_LEO: 'Leo', 246 | SYMBOL_VIRGO: 'Virgo', 247 | SYMBOL_LIBRA: 'Libra', 248 | SYMBOL_SCORPIO: 'Scorpio', 249 | SYMBOL_SAGITTARIUS: 'Sagittarius', 250 | SYMBOL_CAPRICORN: 'Capricorn', 251 | SYMBOL_AQUARIUS: 'Aquarius', 252 | SYMBOL_PISCES: 'Pisces', 253 | SYMBOL_SIGNS: ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces'], 254 | 255 | // http://www.rapidtables.com/web/color/html-color-codes.htm 256 | COLOR_ARIES: '#FF4500', 257 | COLOR_TAURUS: '#8B4513', 258 | COLOR_GEMINI: '#87CEEB', 259 | COLOR_CANCER: '#27AE60', 260 | COLOR_LEO: '#FF4500', 261 | COLOR_VIRGO: '#8B4513', 262 | COLOR_LIBRA: '#87CEEB', 263 | COLOR_SCORPIO: '#27AE60', 264 | COLOR_SAGITTARIUS: '#FF4500', 265 | COLOR_CAPRICORN: '#8B4513', 266 | COLOR_AQUARIUS: '#87CEEB', 267 | COLOR_PISCES: '#27AE60', 268 | COLORS_SIGNS: ['#FF4500', '#8B4513', '#87CEEB', '#27AE60', '#FF4500', '#8B4513', '#87CEEB', '#27AE60', '#FF4500', '#8B4513', '#87CEEB', '#27AE60'], 269 | 270 | CUSTOM_SYMBOL_FN: null, 271 | 272 | // 0 degree is on the West 273 | SHIFT_IN_DEGREES: 180, 274 | 275 | // No fill, only stroke 276 | STROKE_ONLY: false, 277 | 278 | ADD_CLICK_AREA: false, 279 | 280 | // Planets collision circle radius for SYMBOL_SCALE : 1 281 | // Scaling changes the collision radius 282 | COLLISION_RADIUS: 10, // px 283 | 284 | // Aspects 285 | ASPECTS: { 286 | conjunction: { degree: 0, orbit: 10, color: 'transparent' }, 287 | square: { degree: 90, orbit: 8, color: '#FF4500' }, 288 | trine: { degree: 120, orbit: 8, color: '#27AE60' }, 289 | opposition: { degree: 180, orbit: 10, color: '#27AE60' } 290 | }, 291 | 292 | // Dignities 293 | SHOW_DIGNITIES_TEXT: true, 294 | DIGNITIES_RULERSHIP: 'r', 295 | DIGNITIES_DETRIMENT: 'd', 296 | DIGNITIES_EXALTATION: 'e', 297 | DIGNITIES_EXACT_EXALTATION: 'E', 298 | DIGNITIES_FALL: 'f', 299 | 300 | // Source: Aleister Crowley 301 | DIGNITIES_EXACT_EXALTATION_DEFAULT: [ 302 | { name: 'Sun', position: 19, orbit: 2 }, // 19 Arise 303 | { name: 'Moon', position: 33, orbit: 2 }, // 3 Taurus 304 | { name: 'Mercury', position: 155, orbit: 2 }, // 15 Virgo 305 | { name: 'Venus', position: 357, orbit: 2 }, // 27 Pisces 306 | { name: 'Mars', position: 298, orbit: 2 }, // 28 Capricorn 307 | { name: 'Jupiter', position: 105, orbit: 2 }, // 15 Cancer 308 | { name: 'Saturn', position: 201, orbit: 2 }, // 21 Libra 309 | { name: 'NNode', position: 63, orbit: 2 } // 3 Geminy 310 | ], 311 | 312 | // 0 - 4 313 | ANIMATION_CUSPS_ROTATION_SPEED: 2, 314 | 315 | DEBUG: false 316 | } 317 | 318 | const default_settings = settings 319 | export default default_settings 320 | -------------------------------------------------------------------------------- /project/src/svg.test.ts: -------------------------------------------------------------------------------- 1 | import default_settings from "./settings"; 2 | import SVG from "./svg"; 3 | 4 | describe('getSymbol', () => { 5 | beforeAll(() => { 6 | document.body.innerHTML = 7 | '
' + 8 | '
'; 9 | }); 10 | describe('ADD_CLICK_AREA', () => { 11 | test(`don't create rect for cusps if ADD_CLICK_AREA is false`, () => { 12 | for(var i = 1; i <= 12; i++){ 13 | const svg = new SVG("test-element", 100, 100, {...default_settings, ADD_CLICK_AREA : false}) 14 | const element = svg.getSymbol(i.toString(), 1, 1) 15 | expect(element.getElementsByTagName("rect")).toHaveLength(0) 16 | } 17 | }) 18 | 19 | test(`create rect for cusps if ADD_CLICK_AREA is true`, () => { 20 | for(var i = 1; i <= 12; i++){ 21 | const svg = new SVG("test-element", 100, 100, {...default_settings, ADD_CLICK_AREA : true}) 22 | const element = svg.getSymbol(i.toString(), 1, 1) 23 | expect(element.getElementsByTagName("rect")).toHaveLength(1) 24 | } 25 | }) 26 | 27 | test.each([ 28 | [default_settings.SYMBOL_JUPITER, default_settings.SYMBOL_URANUS] 29 | ])(`create rect for %s if ADD_CLICK_AREA is true`, (planet) => { 30 | const svg = new SVG("test-element", 100, 100, {...default_settings, ADD_CLICK_AREA : true}) 31 | const element = svg.getSymbol(planet, 1, 1) 32 | expect(element.getElementsByTagName("rect")).toHaveLength(1) 33 | }) 34 | 35 | test.each([ 36 | [default_settings.SYMBOL_JUPITER, default_settings.SYMBOL_URANUS] 37 | ])(`don't create rect for %s if ADD_CLICK_AREA is false`, (planet) => { 38 | const svg = new SVG("test-element", 100, 100, {...default_settings, ADD_CLICK_AREA : false}) 39 | const element = svg.getSymbol(planet, 1, 1) 40 | expect(element.getElementsByTagName("rect")).toHaveLength(0) 41 | }) 42 | 43 | test.each([default_settings.SYMBOL_SIGNS])('create rect for %s if ADD_CLICK_AREA is true', (sign) => { 44 | const svg = new SVG("test-element", 100, 100, {...default_settings, ADD_CLICK_AREA : true}) 45 | var element = svg.getSymbol(sign, 1, 1); 46 | expect(element.getElementsByTagName("rect")).toHaveLength(1) 47 | }); 48 | 49 | test.each([default_settings.SYMBOL_SIGNS])(`don't create rect for %s if ADD_CLICK_AREA is false`, (sign) => { 50 | const svg = new SVG("test-element", 100, 100, {...default_settings, ADD_CLICK_AREA : false}) 51 | var element = svg.getSymbol(sign, 1, 1); 52 | expect(element.getElementsByTagName("rect")).toHaveLength(0) 53 | }); 54 | }) 55 | 56 | test('should call custom getSymbol function', () => { 57 | const mockFn = jest.fn() 58 | const svg = new SVG("test-element", 100, 100, {...default_settings, CUSTOM_SYMBOL_FN : mockFn}) 59 | svg.getSymbol(default_settings.SYMBOL_URANUS, 1, 2); 60 | expect(mockFn).toHaveBeenCalledWith(default_settings.SYMBOL_URANUS, 1, 2, svg); 61 | }) 62 | }) -------------------------------------------------------------------------------- /project/src/transit.ts: -------------------------------------------------------------------------------- 1 | import Zodiac from './zodiac' 2 | import AspectCalculator from './aspect' 3 | import type { FormedAspect } from './aspect' 4 | import Animator from './animation/animator' 5 | import { validate, getEmptyWrapper, getPointPosition, getRulerPositions, getDescriptionPosition, assemble, radiansToDegree } from './utils' 6 | import type { AstroData, LocatedPoint, Points } from './radix' 7 | import type Radix from './radix' 8 | import type SVG from './svg' 9 | import type { Settings } from './settings' 10 | 11 | /** 12 | * Transit charts. 13 | * 14 | * @class 15 | * @public 16 | * @constructor 17 | * @param {this.settings.Radix} radix 18 | * @param {Object} data 19 | */ 20 | class Transit { 21 | data: AstroData 22 | paper: SVG 23 | cx: number 24 | cy: number 25 | toPoints: Points 26 | radius: number 27 | settings: Settings 28 | rulerRadius: number 29 | pointRadius: number 30 | shift: number 31 | universe: Element 32 | context: this 33 | locatedPoints: LocatedPoint[] 34 | constructor(radix: Radix, data: AstroData, settings: Settings) { 35 | // Validate data 36 | const status = validate(data) 37 | if (status.hasError) { 38 | throw new Error(status.messages.join(' | ')) 39 | } 40 | 41 | this.data = data 42 | this.paper = radix.paper 43 | this.cx = radix.cx 44 | this.cy = radix.cy 45 | this.toPoints = radix.toPoints 46 | this.radius = radix.radius 47 | this.settings = settings 48 | 49 | this.rulerRadius = ((this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO) / this.settings.RULER_RADIUS) 50 | this.pointRadius = this.radius + (this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO + (this.settings.PADDING * this.settings.SYMBOL_SCALE)) 51 | 52 | this.shift = radix.shift 53 | 54 | this.universe = document.createElementNS(this.paper.root.namespaceURI, 'g') 55 | this.universe.setAttribute('id', this.paper._paperElementId + '-' + this.settings.ID_TRANSIT) 56 | this.paper.root.appendChild(this.universe) 57 | 58 | this.context = this 59 | } 60 | 61 | /** 62 | * Draw background 63 | */ 64 | drawBg(): void { 65 | const universe = this.universe 66 | 67 | const wrapper = getEmptyWrapper(universe, this.paper._paperElementId + '-' + this.settings.ID_BG, this.paper._paperElementId) 68 | 69 | const LARGE_ARC_FLAG = 1 70 | const start = 0 // degree 71 | const end = 359.99 // degree 72 | const hemisphere = this.paper.segment(this.cx, this.cy, this.radius + this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO, start, end, this.radius / this.settings.INDOOR_CIRCLE_RADIUS_RATIO, LARGE_ARC_FLAG) 73 | hemisphere.setAttribute('fill', this.settings.STROKE_ONLY ? 'none' : this.settings.COLOR_BACKGROUND) 74 | wrapper.appendChild(hemisphere) 75 | } 76 | 77 | /** 78 | * Draw planets 79 | * 80 | * @param{undefined | Object} planetsData, posible data planets to draw 81 | */ 82 | drawPoints(planetsData?: Points): void { 83 | const planets = (planetsData == null) ? this.data.planets : planetsData 84 | if (planets == null) { 85 | return 86 | } 87 | 88 | const universe = this.universe 89 | const wrapper = getEmptyWrapper(universe, this.paper._paperElementId + '-' + this.settings.ID_TRANSIT + '-' + this.settings.ID_POINTS, this.paper._paperElementId) 90 | 91 | const gap = this.radius - (this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO + this.radius / this.settings.INDOOR_CIRCLE_RADIUS_RATIO) 92 | const step = (gap - 2 * (this.settings.PADDING * this.settings.SYMBOL_SCALE)) / Object.keys(planets).length 93 | 94 | const pointerRadius = this.radius + (this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO) 95 | let startPosition 96 | let endPosition 97 | 98 | this.locatedPoints = [] 99 | for (const planet in planets) { 100 | if (planets.hasOwnProperty(planet)) { 101 | const position = getPointPosition(this.cx, this.cy, this.pointRadius, planets[planet][0] + this.shift, this.settings) 102 | const point = { name: planet, x: position.x, y: position.y, r: (this.settings.COLLISION_RADIUS * this.settings.SYMBOL_SCALE), angle: planets[planet][0] + this.shift, pointer: planets[planet][0] + this.shift } 103 | this.locatedPoints = assemble(this.locatedPoints, point, { cx: this.cx, cy: this.cy, r: this.pointRadius }, this.settings) 104 | } 105 | } 106 | 107 | if (this.settings.DEBUG) console.log('Transit count of points: ' + this.locatedPoints.length) 108 | if (this.settings.DEBUG) console.log('Transit located points:\n' + JSON.stringify(this.locatedPoints)) 109 | 110 | this.locatedPoints.forEach(function (point) { 111 | // draw pointer 112 | startPosition = getPointPosition(this.cx, this.cy, pointerRadius, planets[point.name][0] + this.shift, this.settings) 113 | endPosition = getPointPosition(this.cx, this.cy, pointerRadius + this.rulerRadius / 2, planets[point.name][0] + this.shift, this.settings) 114 | const pointer = this.paper.line(startPosition.x, startPosition.y, endPosition.x, endPosition.y) 115 | pointer.setAttribute('stroke', this.settings.CIRCLE_COLOR) 116 | pointer.setAttribute('stroke-width', (this.settings.CUSPS_STROKE * this.settings.SYMBOL_SCALE)) 117 | wrapper.appendChild(pointer) 118 | 119 | // draw pointer line 120 | if (!this.settings.STROKE_ONLY && (planets[point.name][0] + this.shift) !== point.angle) { 121 | startPosition = endPosition 122 | endPosition = getPointPosition(this.cx, this.cy, this.pointRadius - (this.settings.COLLISION_RADIUS * this.settings.SYMBOL_SCALE), point.angle, this.settings) 123 | const line = this.paper.line(startPosition.x, startPosition.y, endPosition.x, endPosition.y) 124 | line.setAttribute('stroke', this.settings.LINE_COLOR) 125 | line.setAttribute('stroke-width', 0.5 * (this.settings.CUSPS_STROKE * this.settings.SYMBOL_SCALE)) 126 | wrapper.appendChild(line) 127 | } 128 | 129 | // draw symbol 130 | const symbol = this.paper.getSymbol(point.name, point.x, point.y) 131 | symbol.setAttribute('id', this.paper.root.id + '-' + this.settings.ID_TRANSIT + '-' + this.settings.ID_POINTS + '-' + point.name) 132 | wrapper.appendChild(symbol) 133 | 134 | // draw point descriptions 135 | let textsToShow = [(Math.floor(planets[point.name][0]) % 30).toString()] 136 | 137 | const zodiac = new Zodiac(this.data.cusps, this.settings) 138 | if (planets[point.name][1] && zodiac.isRetrograde(planets[point.name][1])) { 139 | textsToShow.push('R') 140 | } else { 141 | textsToShow.push('') 142 | } 143 | textsToShow = textsToShow.concat(zodiac.getDignities({ name: point.name, position: planets[point.name][0] }, this.settings.DIGNITIES_EXACT_EXALTATION_DEFAULT).join(',')) 144 | 145 | const pointDescriptions = getDescriptionPosition(point, textsToShow, this.settings) 146 | pointDescriptions.forEach(function (dsc) { 147 | wrapper.appendChild(this.paper.text(dsc.text, dsc.x, dsc.y, this.settings.POINTS_TEXT_SIZE, this.settings.SIGNS_COLOR)) 148 | }, this) 149 | }, this) 150 | } 151 | 152 | /** 153 | * Draw circles 154 | */ 155 | drawCircles(): void { 156 | const universe = this.universe 157 | const wrapper = getEmptyWrapper(universe, this.paper._paperElementId + '-' + this.settings.ID_TRANSIT + '-' + this.settings.ID_CIRCLES, this.paper._paperElementId) 158 | const radius = this.radius + this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO 159 | 160 | const circle = this.paper.circle(this.cx, this.cy, radius) 161 | circle.setAttribute('stroke', this.settings.CIRCLE_COLOR) 162 | circle.setAttribute('stroke-width', (this.settings.CIRCLE_STRONG * this.settings.SYMBOL_SCALE).toString()) 163 | wrapper.appendChild(circle) 164 | } 165 | 166 | /** 167 | * Draw cusps 168 | * @param{undefined | Object} cuspsData, posible data cusps to draw 169 | */ 170 | drawCusps(cuspsData?: number[]): void { 171 | const cusps = (cuspsData == null) ? this.data.cusps : cuspsData 172 | if (cusps == null) { 173 | return 174 | } 175 | 176 | let bottomPosition 177 | const universe = this.universe 178 | const wrapper = getEmptyWrapper(universe, this.paper._paperElementId + '-' + this.settings.ID_TRANSIT + '-' + this.settings.ID_CUSPS, this.paper._paperElementId) 179 | const numbersRadius = this.radius + ((this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO - this.rulerRadius) / 2) 180 | 181 | const AS = 0 182 | const IC = 3 183 | const DC = 6 184 | const MC = 9 185 | const mainAxis = [AS, IC, DC, MC] 186 | 187 | // Cusps 188 | for (let i = 0, ln = cusps.length; i < ln; i++) { 189 | // Lines 190 | const startPosition = bottomPosition = getPointPosition(this.cx, this.cy, this.radius, cusps[i] + this.shift, this.settings) 191 | const endPosition = getPointPosition(this.cx, this.cy, this.radius + this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO - this.rulerRadius, cusps[i] + this.shift, this.settings) 192 | const line = this.paper.line(startPosition.x, startPosition.y, endPosition.x, endPosition.y) 193 | line.setAttribute('stroke', this.settings.LINE_COLOR) 194 | line.setAttribute('stroke-width', (this.settings.CUSPS_STROKE * this.settings.SYMBOL_SCALE).toString()) 195 | 196 | wrapper.appendChild(line) 197 | 198 | // Cup number 199 | const deg360 = radiansToDegree(2 * Math.PI) 200 | const startOfCusp = cusps[i] 201 | const endOfCusp = cusps[(i + 1) % 12] 202 | const gap = endOfCusp - startOfCusp > 0 ? endOfCusp - startOfCusp : endOfCusp - startOfCusp + deg360 203 | const textPosition = getPointPosition(this.cx, this.cy, numbersRadius, ((startOfCusp + gap / 2) % deg360) + this.shift, this.settings) 204 | wrapper.appendChild(this.paper.getSymbol((i + 1).toString(), textPosition.x, textPosition.y)) 205 | } 206 | } 207 | 208 | drawRuler(): void { 209 | const universe = this.universe 210 | const wrapper = getEmptyWrapper(universe, this.paper.root.id + '-' + this.settings.ID_TRANSIT + '-' + this.settings.ID_RULER, this.paper._paperElementId) 211 | 212 | const startRadius = (this.radius + (this.radius / this.settings.INNER_CIRCLE_RADIUS_RATIO)) 213 | const rays = getRulerPositions(this.cx, this.cy, startRadius, startRadius - this.rulerRadius, this.shift, this.settings) 214 | 215 | rays.forEach(function (ray) { 216 | const line = this.paper.line(ray.startX, ray.startY, ray.endX, ray.endY) 217 | line.setAttribute('stroke', this.settings.CIRCLE_COLOR) 218 | line.setAttribute('stroke-width', (this.settings.CUSPS_STROKE * this.settings.SYMBOL_SCALE)) 219 | wrapper.appendChild(line) 220 | }, this) 221 | 222 | const circle = this.paper.circle(this.cx, this.cy, startRadius - this.rulerRadius) 223 | circle.setAttribute('stroke', this.settings.CIRCLE_COLOR) 224 | circle.setAttribute('stroke-width', (this.settings.CUSPS_STROKE * this.settings.SYMBOL_SCALE).toString()) 225 | wrapper.appendChild(circle) 226 | } 227 | 228 | /** 229 | * Draw aspects 230 | * @param{Array | null} customAspects - posible custom aspects to draw; 231 | */ 232 | aspects(customAspects: FormedAspect[]): Transit { 233 | const aspectsList = customAspects != null && Array.isArray(customAspects) 234 | ? customAspects 235 | : new AspectCalculator(this.toPoints, this.settings).transit(this.data.planets) 236 | 237 | const universe = this.universe 238 | const wrapper = getEmptyWrapper(universe, this.paper.root.id + '-' + this.settings.ID_ASPECTS, this.paper._paperElementId) 239 | 240 | for (let i = 0, ln = aspectsList.length; i < ln; i++) { 241 | const startPoint = getPointPosition(this.cx, this.cy, this.radius / this.settings.INDOOR_CIRCLE_RADIUS_RATIO, aspectsList[i].toPoint.position + this.shift, this.settings) 242 | const endPoint = getPointPosition(this.cx, this.cy, this.radius / this.settings.INDOOR_CIRCLE_RADIUS_RATIO, aspectsList[i].point.position + this.shift, this.settings) 243 | 244 | const line = this.paper.line(startPoint.x, startPoint.y, endPoint.x, endPoint.y) 245 | line.setAttribute('stroke', this.settings.STROKE_ONLY ? this.settings.LINE_COLOR : aspectsList[i].aspect.color) 246 | line.setAttribute('stroke-width', (this.settings.CUSPS_STROKE * this.settings.SYMBOL_SCALE).toString()) 247 | 248 | line.setAttribute('data-name', aspectsList[i].aspect.name) 249 | line.setAttribute('data-degree', aspectsList[i].aspect.degree.toString()) 250 | line.setAttribute('data-point', aspectsList[i].point.name) 251 | line.setAttribute('data-toPoint', aspectsList[i].toPoint.name) 252 | line.setAttribute('data-precision', aspectsList[i].precision.toString()) 253 | 254 | wrapper.appendChild(line) 255 | } 256 | 257 | // this 258 | return this.context 259 | } 260 | 261 | /** 262 | * Moves points to another position. 263 | * 264 | * @param {Object} data - planets target positions. 265 | * @param {Integer} duration - in seconds 266 | * @param {boolean} isReverse 267 | * @param {Function | undefined} callbck - the function executed at the end of animation 268 | */ 269 | animate(data: AstroData, duration: number, isReverse: boolean, callback: () => void): Transit { 270 | // Validate data 271 | const status = validate(data) 272 | if (status.hasError) { 273 | throw new Error(status.messages.join(' | ')) 274 | } 275 | 276 | // remove aspects 277 | getEmptyWrapper(this.universe, this.paper._paperElementId + '-' + this.settings.ID_ASPECTS, this.paper._paperElementId) 278 | 279 | const animator = new Animator(this.context, this.settings) 280 | animator.animate(data, duration, isReverse, function () { 281 | // animation is finished 282 | this.data = data 283 | this.drawPoints() 284 | this.drawCusps() 285 | this.aspects() 286 | 287 | if (typeof callback === 'function') { 288 | callback() 289 | } 290 | }.bind(this)) 291 | 292 | // this 293 | return this.context 294 | } 295 | } 296 | 297 | export default Transit 298 | -------------------------------------------------------------------------------- /project/src/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { LocatedPoint, Points } from './radix'; 2 | import default_settings, { Settings } from './settings'; 3 | import { getPointPosition, isCollision, comparePoints, placePointsInCollision, assemble } from './utils' 4 | 5 | describe('getPointPosition', () => { 6 | test.each([ 7 | [10, 10, 10, 0, {x:0, y:10}], 8 | [10, 10, 10, 90, {x:10, y:20}], 9 | [10, 10, 10, 180, {x:20, y:10}], 10 | [10, 10, 10, 270, {x:10, y:0}], 11 | [10, 10, 10, 360, {x:0, y:10}], 12 | [10, 10, 10, 450, {x:10, y:20}], 13 | ])('should return position for angle', (cx, cy, radius, angle, expected) => { 14 | const position = getPointPosition(cx, cy, radius, angle, default_settings) 15 | expect(position.x).toBeCloseTo(expected.x) 16 | expect(position.y).toBeCloseTo(expected.y) 17 | }) 18 | }); 19 | 20 | describe('isCollision', () => { 21 | test.each([ 22 | [{x:10, y:10, r:5}, {x:10, y:10, r:5}, true], 23 | [{x:10, y:10, r:5}, {x:21, y:10, r:5}, false], 24 | [{x:10, y:10, r:5}, {x:10, y:20, r:5}, true], 25 | [{x:10, y:10, r:5}, {x:10, y:21, r:5}, false], 26 | [{x:10, y:10, r:5}, {x:20, y:10, r:10}, true] 27 | ])('should return if circles are in collision', (circle1, circle2, expected) => { 28 | expect(isCollision(circle1, circle2)).toBe(expected) 29 | }) 30 | }) 31 | 32 | describe('comparePoints', () => { 33 | test.each([ 34 | [ 10, 20, -10], 35 | [ 20, 20, 0], 36 | [ 30, 20, 10], 37 | [ 0, 1, -1], 38 | [259.99, 0, 259.99], 39 | ])('should return if circles are in collision', (angle1, angle2, expected) => { 40 | expect(comparePoints({angle: angle1}, {angle: angle2})).toBe(expected) 41 | }) 42 | }) 43 | 44 | describe('placePointsInCollision', () => { 45 | test('should place correctly planets', () => { 46 | let p1 = {name:'Sun', pointer:1, angle:1, x:1, y:1, r:1}; 47 | let p2 = {name:'Mercury', pointer:359, angle:1, x:1, y:1, r:1}; 48 | 49 | placePointsInCollision(p1, p2); 50 | 51 | expect(p1).toStrictEqual({name:'Sun', pointer:1, angle:2, x:1, y:1, r:1}); 52 | expect(p2).toStrictEqual({name:'Mercury', pointer:359, angle:0, x:1, y:1, r:1}); 53 | }) 54 | 55 | test('should place correctly planets when passing in other order', () => { 56 | let p1 = {name:'Mercury', pointer:359, angle:1, x:1, y:1, r:1}; 57 | let p2 = {name:'Sun', pointer:1, angle:1, x:1, y:1, r:1}; 58 | 59 | placePointsInCollision(p1, p2); 60 | 61 | expect(p1).toStrictEqual({name:'Mercury', pointer:359, angle:0, x:1, y:1, r:1}); 62 | expect(p2).toStrictEqual({name:'Sun', pointer:1, angle:2, x:1, y:1, r:1}); 63 | }) 64 | 65 | test('should place correctly planets when in collision', () => { 66 | let p1 = {name:'Sun', pointer:10, angle:10, x:1, y:1, r:1}; 67 | let p2 = {name:'Mercury', pointer:20, angle:10, x:1, y:1, r:1}; 68 | 69 | placePointsInCollision(p1, p2); 70 | 71 | expect(p1).toStrictEqual({name:'Sun', pointer:10, angle:9, x:1, y:1, r:1}); 72 | expect(p2).toStrictEqual({name:'Mercury', pointer:20, angle:11, x:1, y:1, r:1}); 73 | }) 74 | }) 75 | 76 | describe('assemble', () => { 77 | test('assemble', () => { 78 | let locatedPoints: LocatedPoint[] = []; 79 | const universe = {cx:0, cy:0, r:100} 80 | const planetRadius = 10 81 | const settings: Settings = {...default_settings, COLLISION_RADIUS: planetRadius} 82 | 83 | let point: LocatedPoint = {name:"Point1", x:100, y:0, r:planetRadius, angle:180} 84 | locatedPoints = assemble(locatedPoints, point, universe, settings); 85 | 86 | point = {name:"Point2", x:100, y:0, r:planetRadius, angle:180} 87 | locatedPoints = assemble(locatedPoints, point, universe, settings); 88 | 89 | point = {name:"Point3", x:100, y:0, r:planetRadius, angle:180} 90 | locatedPoints = assemble(locatedPoints, point, universe, settings); 91 | 92 | point = {name:"Point4", x:100, y:0, r:planetRadius, angle:180} 93 | locatedPoints = assemble(locatedPoints, point, universe, settings); 94 | 95 | point = {name:"Point5", x:100, y:0, r:planetRadius, angle:180} 96 | locatedPoints = assemble(locatedPoints, point, universe, settings); 97 | 98 | point = {name:"Point6", x:100, y:0, r:planetRadius, angle:180} 99 | locatedPoints = assemble(locatedPoints, point, universe, settings); 100 | 101 | point = {name:"Point7", x:100, y:0, r:planetRadius, angle:180} 102 | locatedPoints = assemble(locatedPoints, point, universe, settings); 103 | 104 | point = {name:"Point8", x:100, y:0, r:planetRadius, angle:180} 105 | locatedPoints = assemble(locatedPoints, point, universe, settings); 106 | 107 | point = {name:"Point9", x:100, y:0, r:planetRadius, angle:180} 108 | locatedPoints = assemble(locatedPoints, point, universe, settings); 109 | 110 | point = {name:"Point10", x:100, y:0, r:planetRadius, angle:180} 111 | locatedPoints = assemble(locatedPoints, point, universe, settings); 112 | 113 | expect( locatedPoints ).toHaveLength( 10 ); 114 | }) 115 | 116 | test('assemble2', () => { 117 | const universe = {cx:0, cy:0, r:100}; 118 | const collisionRadius = 10; 119 | let angle, planetPosition, sun, mercury, venus, result, locatedPoints; 120 | 121 | angle = 0; 122 | planetPosition = getPointPosition(universe.cx, universe.cy, universe.r, angle, default_settings); 123 | sun = {name:"Sun", x:planetPosition.x, y:planetPosition.y, r:collisionRadius, angle:angle}; 124 | expect(assemble([], sun, universe, default_settings)).toStrictEqual([sun]); 125 | 126 | angle = 90; 127 | planetPosition = getPointPosition(universe.cx, universe.cy, universe.r, angle, default_settings); 128 | mercury = {name:"Mercury", x:planetPosition.x, y:planetPosition.y, r:collisionRadius, angle:angle}; 129 | 130 | locatedPoints = assemble([sun], mercury, universe, default_settings); 131 | expect(locatedPoints[0].angle).toBe(0); 132 | expect(locatedPoints[1].angle).toBe(90); 133 | 134 | angle = 2; 135 | planetPosition = getPointPosition(universe.cx, universe.cy, universe.r, angle, default_settings); 136 | mercury = {name:"Mercury", x:planetPosition.x, y:planetPosition.y, r:collisionRadius, angle:angle}; 137 | 138 | locatedPoints = assemble([sun], mercury, universe, default_settings); 139 | expect(locatedPoints[0].angle).toBe(7); 140 | expect(locatedPoints[1].angle).toBe(355); 141 | expect(locatedPoints).toHaveLength(2); 142 | 143 | angle = 1; 144 | planetPosition = getPointPosition(universe.cx, universe.cy, universe.r, angle, default_settings); 145 | venus = {name:"Venus", x:planetPosition.x, y:planetPosition.y, r:collisionRadius, angle:angle}; 146 | 147 | locatedPoints = assemble([sun, mercury], venus, universe, default_settings); 148 | 149 | expect( locatedPoints[0].name ).toBe("Venus"); 150 | expect( locatedPoints[0].angle ).toBe(1); 151 | 152 | expect( locatedPoints[1].name ).toBe("Mercury"); 153 | expect( locatedPoints[1].angle ).toBe(13); 154 | 155 | expect( locatedPoints[2].name ).toBe("Sun"); 156 | expect( locatedPoints[2].angle ).toBe(349); 157 | 158 | expect(locatedPoints).toHaveLength(3); 159 | }) 160 | 161 | test('assemble should not change planet position when planet are in collision', () => { 162 | const shift = 344.07396032340205; 163 | const universe = {cx:300, cy:300, r:185.125}; 164 | const collisionRadius = 10; 165 | let angle, planetPosition, sun, mercury, venus, result 166 | let locatedPoints: LocatedPoint[] = []; 167 | const planets: Points = {"Sun": [264.7071707108],"Mercury": [283.248841516],"Saturn": [283.8137792332],"Uranus": [274.8367919812],"Neptune": [281.4462451253]} 168 | 169 | for(var planet in planets){ 170 | planetPosition = getPointPosition(universe.cx, universe.cy, universe.r, planets[planet][0]+shift, default_settings); 171 | const planetObject = {name:planet, x:planetPosition.x, y:planetPosition.y, r:collisionRadius, angle:planets[planet][0]+shift,pointer:planets[planet][0]+shift}; 172 | 173 | locatedPoints = assemble(locatedPoints, planetObject, universe, default_settings); 174 | } 175 | 176 | var expectedPlanetOrder = ["Sun", "Uranus", "Neptune", "Mercury", "Saturn"]; 177 | 178 | expect(locatedPoints.map(lp => lp.name)).toStrictEqual(expectedPlanetOrder); 179 | }) 180 | }) -------------------------------------------------------------------------------- /project/src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { AstroData, LocatedPoint } from './radix' 2 | import type { Settings } from './settings' 3 | 4 | /** 5 | * Calculate position of the point on the circle. 6 | * 7 | * @param {int} cx - center x 8 | * @param {int} cy - center y 9 | * @param {int} radius 10 | * @param {double} angle - degree 11 | * 12 | * @return {{x: number, y: number}} Obj - {x:10, y:20} 13 | */ 14 | export const getPointPosition = (cx: number, cy: number, radius: number, angle: number, astrology: { SHIFT_IN_DEGREES: number }): { x: number; y: number } => { 15 | const angleInRadius = (astrology.SHIFT_IN_DEGREES - angle) * Math.PI / 180 16 | const xPos = cx + radius * Math.cos(angleInRadius) 17 | const yPos = cy + radius * Math.sin(angleInRadius) 18 | return { x: xPos, y: yPos } 19 | } 20 | 21 | export const degreeToRadians = (degrees: number): number => degrees * Math.PI / 180 22 | 23 | export const radiansToDegree = (radians: number): number => radians * 180 / Math.PI 24 | 25 | interface TextLocation { text: string; x: number; y: number } 26 | 27 | /** 28 | * Calculates positions of the point description 29 | * 30 | * @param {Object} point 31 | * @param {Array} texts 32 | * 33 | * @return {Array} [{text:"abc", x:123, y:456}, {text:"cvb", x:456, y:852}, ...] 34 | */ 35 | export const getDescriptionPosition = function (point: { x: number; y: number }, texts: string[], astrology: { COLLISION_RADIUS: number; SYMBOL_SCALE: number }): TextLocation[] { 36 | const RATION = 1.4 37 | const result: TextLocation[] = [] 38 | const posX = point.x + (astrology.COLLISION_RADIUS / RATION * astrology.SYMBOL_SCALE) 39 | const posY = point.y - (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE) 40 | 41 | texts.forEach((text, idx) => { 42 | result.push({ text, x: posX, y: posY + (astrology.COLLISION_RADIUS / RATION * astrology.SYMBOL_SCALE * idx) }) 43 | }, this) 44 | 45 | return result 46 | } 47 | 48 | /** 49 | * Checks a source data 50 | * @private 51 | * 52 | * @param {Object} data 53 | * @return {{hasError: boolean, messages: string[]}} status 54 | */ 55 | export const validate = (data: AstroData): { hasError: boolean; messages: string[] } => { 56 | const status = { hasError: false, messages: [] as string[] } 57 | 58 | if (data == null) { 59 | status.messages.push('Data is not set.') 60 | status.hasError = true 61 | return status 62 | } 63 | 64 | if (data.planets == null) { 65 | status.messages.push('There is not property \'planets\'.') 66 | status.hasError = true 67 | } 68 | 69 | for (const property in data.planets) { 70 | if (data.planets.hasOwnProperty(property)) { 71 | if (!Array.isArray(data.planets[property])) { 72 | status.messages.push('The planets property \'' + property + '\' has to be Array.') 73 | status.hasError = true 74 | } 75 | } 76 | } 77 | 78 | if (data.cusps != null && !Array.isArray(data.cusps)) { 79 | status.messages.push('Property \'cusps\' has to be Array.') 80 | status.hasError = true 81 | } 82 | 83 | if (data.cusps != null && data.cusps.length !== 12) { 84 | status.messages.push('Count of \'cusps\' values has to be 12.') 85 | status.hasError = true 86 | } 87 | 88 | return status 89 | } 90 | 91 | /** 92 | * Get empty DOMElement with ID 93 | * 94 | * @param{String} elementID 95 | * @param{DOMElement} parent 96 | * @return {DOMElement} 97 | */ 98 | export const getEmptyWrapper = (parent: Element, elementID: string, _paperElementId: string): Element => { 99 | const element = document.getElementById(elementID) 100 | if (element != null) { 101 | removeChilds(element) 102 | return element 103 | } 104 | const paper = document.getElementById(_paperElementId) 105 | if (paper == null) throw new Error('Paper element should exist') 106 | 107 | const wrapper = document.createElementNS(paper.namespaceURI, 'g') 108 | wrapper.setAttribute('id', elementID) 109 | parent.appendChild(wrapper) 110 | 111 | return wrapper 112 | } 113 | 114 | /** 115 | * Remove childs 116 | * 117 | * @param{DOMElement} parent 118 | */ 119 | export const removeChilds = (parent: HTMLElement): void => { 120 | if (parent == null) { 121 | return 122 | } 123 | 124 | let last 125 | while ((last = parent.lastChild) != null) { 126 | parent.removeChild(last) 127 | } 128 | } 129 | 130 | /** 131 | * Check circle collision between two objects 132 | * 133 | * @param {Object} circle1, {x:123, y:123, r:50} 134 | * @param {Object} circle2, {x:456, y:456, r:60} 135 | * @return {boolean} 136 | */ 137 | export const isCollision = (circle1: { x: number; y: number; r: number }, circle2: { x: number; y: number; r: number }): boolean => { 138 | // Calculate the vector between the circles’ center points 139 | const vx = circle1.x - circle2.x 140 | const vy = circle1.y - circle2.y 141 | 142 | const magnitude = Math.sqrt(vx * vx + vy * vy) 143 | 144 | // circle.radius has been set to astrology.COLLISION_RADIUS; 145 | const totalRadii = circle1.r + circle2.r 146 | 147 | return magnitude <= totalRadii 148 | } 149 | 150 | /** 151 | * Places a new point in the located list 152 | * 153 | * @param {Array} locatedPoints, [{name:"Mars", x:123, y:123, r:50, ephemeris:45.96}, {name:"Sun", x:1234, y:1234, r:50, ephemeris:100.96}] 154 | * @param {Object} point, {name:"Venus", x:78, y:56, r:50, angle:15.96} 155 | * @param {Object} universe - current universe 156 | * @return {Array} locatedPoints 157 | */ 158 | export const assemble = (locatedPoints: LocatedPoint[], point: LocatedPoint, universe: { cx: number; cy: number; r: number }, astrology: Settings): LocatedPoint[] => { 159 | // first item 160 | if (locatedPoints.length === 0) { 161 | locatedPoints.push(point) 162 | return locatedPoints 163 | } 164 | 165 | if ((2 * Math.PI * universe.r) - (2 * (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE) * (locatedPoints.length + 2)) <= 0) { 166 | if (astrology.DEBUG) console.log('Universe circumference: ' + (2 * Math.PI * universe.r) + ', Planets circumference: ' + (2 * (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE) * (locatedPoints.length + 2))) 167 | throw new Error('Unresolved planet collision. Try change SYMBOL_SCALE or paper size.') 168 | } 169 | 170 | let hasCollision = false 171 | let locatedButInCollisionPoint 172 | locatedPoints.sort(comparePoints) 173 | for (let i = 0, ln = locatedPoints.length; i < ln; i++) { 174 | if (isCollision(locatedPoints[i], point)) { 175 | hasCollision = true 176 | locatedButInCollisionPoint = locatedPoints[i] 177 | locatedButInCollisionPoint.index = i 178 | 179 | if (astrology.DEBUG) console.log('Resolve collision: ' + locatedButInCollisionPoint.name + ' X ' + point.name) 180 | 181 | break 182 | } 183 | } 184 | 185 | if (hasCollision && locatedButInCollisionPoint != null && locatedButInCollisionPoint.index != null) { 186 | placePointsInCollision(locatedButInCollisionPoint, point) 187 | 188 | let newPointPosition = getPointPosition(universe.cx, universe.cy, universe.r, locatedButInCollisionPoint.angle, astrology) 189 | locatedButInCollisionPoint.x = newPointPosition.x 190 | locatedButInCollisionPoint.y = newPointPosition.y 191 | 192 | newPointPosition = getPointPosition(universe.cx, universe.cy, universe.r, point.angle, astrology) 193 | point.x = newPointPosition.x 194 | point.y = newPointPosition.y 195 | 196 | // remove locatedButInCollisionPoint from locatedPoints 197 | locatedPoints.splice(locatedButInCollisionPoint.index, 1) 198 | 199 | // call recursive 200 | locatedPoints = assemble(locatedPoints, locatedButInCollisionPoint, universe, astrology) 201 | locatedPoints = assemble(locatedPoints, point, universe, astrology) 202 | } else { 203 | locatedPoints.push(point) 204 | } 205 | locatedPoints.sort(comparePoints) 206 | return locatedPoints 207 | } 208 | 209 | /** 210 | * Sets the positions of two points that are in collision. 211 | * 212 | * @param {Object} p1, {..., pointer:123, angle:456} 213 | * @param {Object} p2, {..., pointer:23, angle:56} 214 | */ 215 | export const placePointsInCollision = (p1: LocatedPoint, p2: LocatedPoint): void => { 216 | const step = 1 217 | let adjustedP1Pointer = p1.pointer === undefined ? p1.angle : p1.pointer 218 | let adjustedP2Pointer = p2.pointer === undefined ? p2.angle : p2.pointer 219 | 220 | // solving problems with zero crossing 221 | if (Math.abs(adjustedP1Pointer - adjustedP2Pointer) > 180) { 222 | adjustedP1Pointer = (adjustedP1Pointer + 180) % 360 223 | adjustedP2Pointer = (adjustedP2Pointer + 180) % 360 224 | } 225 | 226 | if (adjustedP1Pointer <= adjustedP2Pointer) { 227 | p1.angle = p1.angle - step 228 | p2.angle = p2.angle + step 229 | } else if (adjustedP1Pointer >= adjustedP2Pointer) { 230 | p1.angle = p1.angle + step 231 | p2.angle = p2.angle - step 232 | } 233 | 234 | p1.angle = (p1.angle + 360) % 360 235 | p2.angle = (p2.angle + 360) % 360 236 | } 237 | 238 | /** 239 | * Check collision between angle and object 240 | * 241 | * @param {double} angle 242 | * @param {Array} points, [{x:456, y:456, r:60, angle:123}, ...] 243 | * @return {boolean} 244 | */ 245 | export const isInCollision = (angle: number, points: string | any[], astrology: Settings): boolean => { 246 | const deg360 = radiansToDegree(2 * Math.PI) 247 | const collisionRadius = (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE) / 2 248 | 249 | let result = false 250 | for (let i = 0, ln = points.length; i < ln; i++) { 251 | if (Math.abs(points[i].angle - angle) <= collisionRadius || 252 | (deg360 - Math.abs(points[i].angle - angle)) <= collisionRadius) { 253 | result = true 254 | break 255 | } 256 | } 257 | 258 | return result 259 | } 260 | 261 | interface InitialEndPosition { 262 | startX: number 263 | startY: number 264 | endX: number 265 | endY: number 266 | } 267 | 268 | /** 269 | * Calculates positions of the dashed line passing through the obstacle. 270 | * * 271 | * @param {double} centerX 272 | * @param {double} centerY 273 | * @param {double} angle - line angle 274 | * @param {double} lineStartRadius 275 | * @param {double} lineEndRadius 276 | * @param {double} obstacleRadius 277 | * @param {Array} obstacles, [{x:456, y:456, r:60, angle:123}, ...] 278 | * 279 | * @return {Array} [{startX:1, startY:1, endX:4, endY:4}, {startX:6, startY:6, endX:8, endY:8}] 280 | */ 281 | export const getDashedLinesPositions = (centerX: number, centerY: number, angle: number, lineStartRadius: number, lineEndRadius: number, obstacleRadius: number, obstacles: LocatedPoint[], astrology: Settings): InitialEndPosition[] => { 282 | let startPos 283 | let endPos 284 | const result = [] 285 | 286 | if (isInCollision(angle, obstacles, astrology)) { 287 | startPos = getPointPosition(centerX, centerY, lineStartRadius, angle, astrology) 288 | endPos = getPointPosition(centerX, centerY, obstacleRadius - (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE), angle, astrology) 289 | result.push({ startX: startPos.x, startY: startPos.y, endX: endPos.x, endY: endPos.y }) 290 | 291 | // the second part of the line when is space 292 | if ((obstacleRadius + 2 * (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE)) < lineEndRadius) { 293 | startPos = getPointPosition(centerX, centerY, obstacleRadius + 2 * (astrology.COLLISION_RADIUS * astrology.SYMBOL_SCALE), angle, astrology) 294 | endPos = getPointPosition(centerX, centerY, lineEndRadius, angle, astrology) 295 | result.push({ startX: startPos.x, startY: startPos.y, endX: endPos.x, endY: endPos.y }) 296 | } 297 | } else { 298 | startPos = getPointPosition(centerX, centerY, lineStartRadius, angle, astrology) 299 | endPos = getPointPosition(centerX, centerY, lineEndRadius, angle, astrology) 300 | result.push({ startX: startPos.x, startY: startPos.y, endX: endPos.x, endY: endPos.y }) 301 | } 302 | 303 | return result 304 | } 305 | 306 | /** 307 | * Calculate ruler positions. 308 | * 309 | * @param {Double} centerX 310 | * @param {Double} centerY 311 | * @param {Double} startRadius 312 | * @param {Double} endRadius 313 | * @param {Boolean} startAngle 314 | * 315 | * @return {Array} [ {startX:1,startY:2, endX:3, endX:4 }, ...] 316 | */ 317 | export const getRulerPositions = (centerX: number, centerY: number, startRadius: number, endRadius: number, startAngle: number, astrology: { SHIFT_IN_DEGREES: number }): InitialEndPosition[] => { 318 | const result = [] 319 | 320 | const rayRadius = endRadius 321 | const halfRayRadius = (startRadius <= endRadius) ? rayRadius - (Math.abs(endRadius - startRadius) / 2) : rayRadius + (Math.abs(endRadius - startRadius) / 2) 322 | 323 | for (let i = 0, start = 0, step = 5; i < 72; i++) { 324 | const angle = start + startAngle 325 | const startPos = getPointPosition(centerX, centerY, startRadius, angle, astrology) 326 | const endPos = getPointPosition(centerX, centerY, (i % 2 === 0 ? rayRadius : halfRayRadius), angle, astrology) 327 | result.push({ startX: startPos.x, startY: startPos.y, endX: endPos.x, endY: endPos.y }) 328 | 329 | start += step 330 | } 331 | 332 | return result 333 | } 334 | 335 | /** 336 | * Compare two points 337 | * 338 | * @param {Object} pointA, {name:"Venus", x:78, y:56, r:50, angle:15.96} 339 | * @param {Object} pointB, {name:"Mercury", x:78, y:56, r:50, angle:20.26} 340 | * @return 341 | */ 342 | export const comparePoints = (pointA: { angle: number }, pointB: { angle: number }): number => { 343 | return pointA.angle - pointB.angle 344 | } 345 | -------------------------------------------------------------------------------- /project/src/zodiac.test.ts: -------------------------------------------------------------------------------- 1 | import default_settings from "./settings"; 2 | import Zodiac from "./zodiac"; 3 | 4 | describe('getSign', () => { 5 | const cusps = [296, 350, 30, 56, 75, 94, 116, 170, 210, 236, 255, 274]; 6 | var reporter = new Zodiac( cusps ); 7 | test.each([ 8 | [0, 1], 9 | [30, 2], 10 | [270, 10], 11 | [359, 12], 12 | [361, 1], 13 | ])('should return correct sign', (point, expected) => { 14 | expect( reporter.getSign( point ) ).toBe( expected ); 15 | }) 16 | }) 17 | 18 | test.each([ 19 | [-1, true], 20 | [1, false], 21 | ])('should return retrograde if speed is negative', (speed, expected) => { 22 | const cusps = [296, 350, 30, 56, 75, 94, 116, 170, 210, 236, 255, 274]; 23 | const zodiac = new Zodiac( cusps ); 24 | 25 | expect(zodiac.isRetrograde(speed)).toBe(expected); 26 | }) 27 | 28 | test.each([ 29 | [274, 12], 30 | [296, 12], 31 | [296.1, 1], 32 | [350, 2], 33 | [359, 2], 34 | [0, 2], 35 | [361, 2], 36 | [29.9, 2], 37 | [30, 3], 38 | ])('getHouseNumber [1] %i', (house, expected) => { 39 | const cusps = [296.1, 350, 30, 56, 75, 94, 116, 170, 210, 236, 255, 274]; 40 | const zodiac = new Zodiac( cusps ); 41 | 42 | expect(zodiac.getHouseNumber(house)).toBe(expected); 43 | }) 44 | 45 | test.each([ 46 | [248.58, 12], 47 | [265, 12], 48 | [265.7, 1], 49 | [307.65, 2], 50 | [353.4, 3], 51 | [0, 3], 52 | [361, 3], 53 | [26.87, 4], 54 | ])('getHouseNumber [2] %i', (house, expected) => { 55 | const cusps = [265.6850555442075,307.6441825689919,353.38796689506074,26.86890880306794,50.191811553503044,68.57049261566578,85.6850555442075,127.64418256899188,173.3879668950608,206.8689088030679,230.19181155350307,248.5704926156658]; 56 | const zodiac = new Zodiac( cusps ); 57 | 58 | expect(zodiac.getHouseNumber(house)).toBe(expected); 59 | }) 60 | 61 | test.each([ 62 | [266.1234, "266° 7' 24"], 63 | [0.1234, "0° 7' 24"], 64 | [360.1234, "360° 7' 24"], 65 | [266.3251184363515, "266° 19' 30"], 66 | ])('toDMS %i', (angle, expected) => { 67 | const cusps = [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330]; 68 | const zodiac = new Zodiac( cusps ); 69 | 70 | expect(zodiac.toDMS(angle)).toBe(expected); 71 | }) 72 | 73 | describe('dignities', () => { 74 | const cusps = [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330]; 75 | const reporter = new Zodiac( cusps ); 76 | test('Sun', () => { 77 | expect( reporter.getDignities( {name:"Sun", position:120} )).toStrictEqual([default_settings.DIGNITIES_RULERSHIP] ); 78 | expect( reporter.getDignities( {name:"Sun", position:300} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 79 | expect( reporter.getDignities( {name:"Sun", position:150} )).toStrictEqual([default_settings.DIGNITIES_FALL] ); 80 | expect( reporter.getDignities( {name:"Sun", position:18} )).toStrictEqual([default_settings.DIGNITIES_EXALTATION] ); 81 | expect( reporter.getDignities( {name:"Sun", position:0}, [{"name":"Sun", "position":0, "orbit":2}] )).toStrictEqual([default_settings.DIGNITIES_EXALTATION, default_settings.DIGNITIES_EXACT_EXALTATION] ); 82 | }) 83 | 84 | test('Moon', () => { 85 | expect( reporter.getDignities( {name:"Moon", position:90} )).toStrictEqual([default_settings.DIGNITIES_RULERSHIP] ); 86 | expect( reporter.getDignities( {name:"Moon", position:270} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 87 | expect( reporter.getDignities( {name:"Moon", position:210} )).toStrictEqual([default_settings.DIGNITIES_FALL] ); 88 | expect( reporter.getDignities( {name:"Moon", position:30} )).toStrictEqual([default_settings.DIGNITIES_EXALTATION] ); 89 | expect( reporter.getDignities( {name:"Moon", position:32}, [{"name":"Moon", "position":33, "orbit":2}] )).toStrictEqual([default_settings.DIGNITIES_EXALTATION, default_settings.DIGNITIES_EXACT_EXALTATION] ); 90 | }) 91 | 92 | test('Mercury', () => { 93 | expect( reporter.getDignities( {name:"Mercury", position:60} )).toStrictEqual([default_settings.DIGNITIES_RULERSHIP] ); 94 | expect( reporter.getDignities( {name:"Mercury", position:240} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 95 | expect( reporter.getDignities( {name:"Mercury", position:330} )).toStrictEqual([default_settings.DIGNITIES_FALL] ); 96 | expect( reporter.getDignities( {name:"Mercury", position:150} )).toStrictEqual([default_settings.DIGNITIES_EXALTATION] ); 97 | expect( reporter.getDignities( {name:"Mercury", position:156}, [{"name":"Mercury", "position":155, "orbit":2}] )).toStrictEqual([default_settings.DIGNITIES_EXALTATION, default_settings.DIGNITIES_EXACT_EXALTATION] ); 98 | }) 99 | 100 | test('Venus', () => { 101 | expect( reporter.getDignities( {name:"Venus", position:30} )).toStrictEqual([default_settings.DIGNITIES_RULERSHIP] ); 102 | expect( reporter.getDignities( {name:"Venus", position:180} )).toStrictEqual([default_settings.DIGNITIES_RULERSHIP] ); 103 | 104 | expect( reporter.getDignities( {name:"Venus", position:0} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 105 | expect( reporter.getDignities( {name:"Venus", position:210} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 106 | 107 | expect( reporter.getDignities( {name:"Venus", position:150} )).toStrictEqual([default_settings.DIGNITIES_FALL] ); 108 | 109 | expect( reporter.getDignities( {name:"Venus", position:330} )).toStrictEqual([default_settings.DIGNITIES_EXALTATION] ); 110 | expect( reporter.getDignities( {name:"Venus", position:357.987}, [{"name":"Venus", "position":357, "orbit":2}] )).toStrictEqual([default_settings.DIGNITIES_EXALTATION, default_settings.DIGNITIES_EXACT_EXALTATION] ); 111 | }) 112 | 113 | test('Mars', () => { 114 | 115 | expect( reporter.getDignities( {name:"Mars", position:0} )).toStrictEqual([default_settings.DIGNITIES_RULERSHIP] ); 116 | expect( reporter.getDignities( {name:"Mars", position:210} )).toStrictEqual([default_settings.DIGNITIES_RULERSHIP] ); 117 | 118 | expect( reporter.getDignities( {name:"Mars", position:30} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 119 | expect( reporter.getDignities( {name:"Mars", position:180} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 120 | 121 | expect( reporter.getDignities( {name:"Mars", position:90} )).toStrictEqual([default_settings.DIGNITIES_FALL] ); 122 | 123 | expect( reporter.getDignities( {name:"Mars", position:270} )).toStrictEqual([default_settings.DIGNITIES_EXALTATION] ); 124 | expect( reporter.getDignities( {name:"Mars", position:298}, [{"name":"Mars", "position":298, "orbit":2}] )).toStrictEqual([default_settings.DIGNITIES_EXALTATION, default_settings.DIGNITIES_EXACT_EXALTATION] ); 125 | }) 126 | 127 | test('Jupiter', () => { 128 | expect( reporter.getDignities( {name:"Jupiter", position:240} )).toStrictEqual([default_settings.DIGNITIES_RULERSHIP] ); 129 | expect( reporter.getDignities( {name:"Jupiter", position:330} )).toStrictEqual([default_settings.DIGNITIES_RULERSHIP] ); 130 | 131 | expect( reporter.getDignities( {name:"Jupiter", position:60} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 132 | expect( reporter.getDignities( {name:"Jupiter", position:150} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 133 | 134 | expect( reporter.getDignities( {name:"Jupiter", position:270} )).toStrictEqual([default_settings.DIGNITIES_FALL] ); 135 | 136 | expect( reporter.getDignities( {name:"Jupiter", position:90} )).toStrictEqual([default_settings.DIGNITIES_EXALTATION] ); 137 | expect( reporter.getDignities( {name:"Jupiter", position:105}, [{"name":"Jupiter", "position":105, "orbit":2}] )).toStrictEqual([default_settings.DIGNITIES_EXALTATION, default_settings.DIGNITIES_EXACT_EXALTATION] ); 138 | }) 139 | 140 | test('Saturn', () => { 141 | expect( reporter.getDignities( {name:"Saturn", position:300} )).toStrictEqual([default_settings.DIGNITIES_RULERSHIP] ); 142 | 143 | expect( reporter.getDignities( {name:"Saturn", position:90} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 144 | expect( reporter.getDignities( {name:"Saturn", position:120} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 145 | 146 | expect( reporter.getDignities( {name:"Saturn", position:0} )).toStrictEqual([default_settings.DIGNITIES_FALL] ); 147 | 148 | expect( reporter.getDignities( {name:"Saturn", position:180} )).toStrictEqual([default_settings.DIGNITIES_EXALTATION] ); 149 | expect( reporter.getDignities( {name:"Saturn", position:201}, [{"name":"Saturn", "position":201, "orbit":2}] )).toStrictEqual([default_settings.DIGNITIES_EXALTATION, default_settings.DIGNITIES_EXACT_EXALTATION] ); 150 | }) 151 | 152 | test('Uranus', () => { 153 | expect( reporter.getDignities( {name:"Uranus", position:300} )).toStrictEqual([default_settings.DIGNITIES_RULERSHIP] ); 154 | expect( reporter.getDignities( {name:"Uranus", position:120} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 155 | expect( reporter.getDignities( {name:"Uranus", position:30} )).toStrictEqual([default_settings.DIGNITIES_FALL] ); 156 | expect( reporter.getDignities( {name:"Uranus", position:210} )).toStrictEqual([default_settings.DIGNITIES_EXALTATION] ); 157 | expect( reporter.getDignities( {name:"Uranus", position:218}, [{"name":"Uranus", "position":218, "orbit":2}] )).toStrictEqual([default_settings.DIGNITIES_EXALTATION, default_settings.DIGNITIES_EXACT_EXALTATION] ); 158 | }) 159 | 160 | test('Neptune', () => { 161 | expect( reporter.getDignities( {name:"Neptune", position:330} )).toStrictEqual([default_settings.DIGNITIES_RULERSHIP] ); 162 | expect( reporter.getDignities( {name:"Neptune", position:150} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 163 | expect( reporter.getDignities( {name:"Neptune", position:60} )).toStrictEqual([default_settings.DIGNITIES_FALL] ); 164 | expect( reporter.getDignities( {name:"Neptune", position:300} )).toStrictEqual([default_settings.DIGNITIES_FALL] ); 165 | 166 | expect( reporter.getDignities( {name:"Neptune", position:120} )).toStrictEqual([default_settings.DIGNITIES_EXALTATION] ); 167 | expect( reporter.getDignities( {name:"Neptune", position:240} )).toStrictEqual([default_settings.DIGNITIES_EXALTATION] ); 168 | expect( reporter.getDignities( {name:"Neptune", position:241}, [{"name":"Neptune", "position":241, "orbit":2}] )).toStrictEqual([default_settings.DIGNITIES_EXALTATION, default_settings.DIGNITIES_EXACT_EXALTATION] ); 169 | }) 170 | 171 | test('Pluto', () => { 172 | expect( reporter.getDignities( {name:"Pluto", position:210} )).toStrictEqual([default_settings.DIGNITIES_RULERSHIP] ); 173 | expect( reporter.getDignities( {name:"Pluto", position:30} )).toStrictEqual([default_settings.DIGNITIES_DETRIMENT] ); 174 | expect( reporter.getDignities( {name:"Pluto", position:180} )).toStrictEqual([default_settings.DIGNITIES_FALL] ); 175 | expect( reporter.getDignities( {name:"Pluto", position:0} )).toStrictEqual([default_settings.DIGNITIES_EXALTATION] ); 176 | expect( reporter.getDignities( {name:"Pluto", position:18}, [{"name":"Pluto", "position":18, "orbit":2}] )).toStrictEqual([default_settings.DIGNITIES_EXALTATION, default_settings.DIGNITIES_EXACT_EXALTATION] ); 177 | }) 178 | }) 179 | -------------------------------------------------------------------------------- /project/src/zodiac.ts: -------------------------------------------------------------------------------- 1 | import default_settings from './settings' 2 | import type { Dignity, Settings } from './settings' 3 | import { radiansToDegree } from './utils' 4 | // Zodiac 5 | const SIGNS_ARIES = 1 6 | const SIGNS_TAURUS = 2 7 | const SIGNS_GEMINI = 3 8 | const SIGNS_CANCER = 4 9 | const SIGNS_LEO = 5 10 | const SIGNS_VIRGO = 6 11 | const SIGNS_LIBRA = 7 12 | const SIGNS_SCORPIO = 8 13 | const SIGNS_SAGITTARIUS = 9 14 | const SIGNS_CAPRICORN = 10 15 | const SIGNS_AQUARIUS = 11 16 | const SIGNS_PISCES = 12 17 | 18 | /** 19 | * Zodiac 20 | * 21 | * Gives the position of points in the zodiac. 22 | * Position of point in the zodiac. 23 | * Position of point in houses. 24 | * Dignities of planets. 25 | * 26 | * @class 27 | * @public 28 | * @constructor 29 | * @param {Array} cusps - cusprs in zodiac; [296, 350, 30, 56, 75, 94, 116, 170, 210, 236, 255, 274] 30 | * @param {Object | null } settings 31 | */ 32 | class Zodiac { 33 | cusps: number[] 34 | settings: Settings 35 | constructor (cusps: number[], settings?: Settings) { 36 | if (cusps === null) { 37 | throw new Error('Param \'cusps\' must not be empty.') 38 | } 39 | 40 | if (!(Array.isArray(cusps) && cusps.length === 12)) { 41 | throw new Error('Param \'cusps\' is not 12 length Array.') 42 | } 43 | 44 | this.cusps = cusps 45 | this.settings = settings ?? default_settings 46 | } 47 | 48 | /** 49 | * Get astrological sign 50 | * 1 - Arise, ... , 12 - Pisces 51 | * 52 | * @param {double} point - angle of point in circle 53 | * @return { \[1-9] | 1[0-2]\ } 54 | */ 55 | getSign (point: number): number { 56 | const angle = point % radiansToDegree(2 * Math.PI) 57 | return Math.floor((angle / 30) + 1) 58 | } 59 | 60 | /** 61 | * Is retrograde 62 | * 63 | * @param {double} speed 64 | * @return {boolean} 65 | */ 66 | isRetrograde (speed: number): boolean { 67 | return speed < 0 68 | } 69 | 70 | /** 71 | * Get house number 72 | * 1 - 12 73 | * 74 | * @param {double} point - angle of point in circle 75 | * @return { \[1-9] | 1[0-2]\ } 76 | */ 77 | getHouseNumber (point: number): number { 78 | const angle = point % radiansToDegree(2 * Math.PI) 79 | 80 | for (let i = 0, ln = this.cusps.length; i < ln; i++) { 81 | if (angle >= this.cusps[i] && angle < this.cusps[(i % (ln - 1)) + 1]) { 82 | return i + 1 83 | } 84 | } 85 | 86 | // cusp passes over zero 87 | for (let i = 0, ln = this.cusps.length; i < ln; i++) { 88 | if (this.cusps[i] > this.cusps[(i % (ln - 1)) + 1]) { 89 | return i + 1 90 | } 91 | } 92 | 93 | throw new Error('Oops, serious error in the method: \'astrology.Zodiac.getHouseNumber\'.') 94 | } 95 | 96 | /** 97 | * Calculate dignities of planet 98 | * 99 | * r - Rulership 100 | * d - Detriment 101 | * e - Exaltation 102 | * E - Exalatation - Exact exaltation 103 | * f - Fall 104 | * 105 | * @param {Object} planet, { name:"Sun", position:60.2 } 106 | * @param {Array | null } exactExaltation - list of named angles, [{ name:"Sun", position:278, orbit:2 }, { name:"Moon", position:3, , orbit:2 }] 107 | * @return {Array} 108 | */ 109 | getDignities (planet: { name: string; position: number }, exactExaltation?: Dignity[]): string[] { 110 | if (!(planet && planet.name && planet.position != null)) { 111 | return [] 112 | } 113 | 114 | const result = [] 115 | const sign = this.getSign(planet.position) 116 | 117 | const position = planet.position % radiansToDegree(2 * Math.PI) 118 | 119 | switch (planet.name) { 120 | case this.settings.SYMBOL_SUN: 121 | 122 | if (sign === SIGNS_LEO) { 123 | result.push(this.settings.DIGNITIES_RULERSHIP) 124 | } else if (sign === SIGNS_AQUARIUS) { 125 | result.push(this.settings.DIGNITIES_DETRIMENT) 126 | } 127 | 128 | if (sign === SIGNS_ARIES) { 129 | result.push(this.settings.DIGNITIES_EXALTATION) 130 | } else if (sign === SIGNS_VIRGO) { 131 | result.push(this.settings.DIGNITIES_FALL) 132 | } 133 | 134 | break 135 | 136 | case this.settings.SYMBOL_MOON: 137 | 138 | if (sign === SIGNS_CANCER) { 139 | result.push(this.settings.DIGNITIES_RULERSHIP) 140 | } else if (sign === SIGNS_CAPRICORN) { 141 | result.push(this.settings.DIGNITIES_DETRIMENT) 142 | } 143 | 144 | if (sign === SIGNS_TAURUS) { 145 | result.push(this.settings.DIGNITIES_EXALTATION) 146 | } else if (sign === SIGNS_SCORPIO) { 147 | result.push(this.settings.DIGNITIES_FALL) 148 | } 149 | 150 | break 151 | 152 | case this.settings.SYMBOL_MERCURY: 153 | 154 | if (sign === SIGNS_GEMINI) { 155 | result.push(this.settings.DIGNITIES_RULERSHIP) 156 | } else if (sign === SIGNS_SAGITTARIUS) { 157 | result.push(this.settings.DIGNITIES_DETRIMENT) 158 | } 159 | 160 | if (sign === SIGNS_VIRGO) { 161 | result.push(this.settings.DIGNITIES_EXALTATION) 162 | } else if (sign === SIGNS_PISCES) { 163 | result.push(this.settings.DIGNITIES_FALL) 164 | } 165 | 166 | break 167 | 168 | case this.settings.SYMBOL_VENUS: 169 | 170 | if (sign === SIGNS_TAURUS || sign === SIGNS_LIBRA) { 171 | result.push(this.settings.DIGNITIES_RULERSHIP) 172 | } else if (sign === SIGNS_ARIES || sign === SIGNS_SCORPIO) { 173 | result.push(this.settings.DIGNITIES_DETRIMENT) 174 | } 175 | 176 | if (sign === SIGNS_PISCES) { 177 | result.push(this.settings.DIGNITIES_EXALTATION) 178 | } else if (sign === SIGNS_VIRGO) { 179 | result.push(this.settings.DIGNITIES_FALL) 180 | } 181 | 182 | break 183 | 184 | case this.settings.SYMBOL_MARS: 185 | 186 | if (sign === SIGNS_ARIES || sign === SIGNS_SCORPIO) { 187 | result.push(this.settings.DIGNITIES_RULERSHIP) 188 | } else if (sign === SIGNS_TAURUS || sign === SIGNS_LIBRA) { 189 | result.push(this.settings.DIGNITIES_DETRIMENT) 190 | } 191 | 192 | if (sign === SIGNS_CAPRICORN) { 193 | result.push(this.settings.DIGNITIES_EXALTATION) 194 | } else if (sign === SIGNS_CANCER) { 195 | result.push(this.settings.DIGNITIES_FALL) 196 | } 197 | 198 | break 199 | 200 | case this.settings.SYMBOL_JUPITER: 201 | 202 | if (sign === SIGNS_SAGITTARIUS || sign === SIGNS_PISCES) { 203 | result.push(this.settings.DIGNITIES_RULERSHIP) 204 | } else if (sign === SIGNS_GEMINI || sign === SIGNS_VIRGO) { 205 | result.push(this.settings.DIGNITIES_DETRIMENT) 206 | } 207 | 208 | if (sign === SIGNS_CANCER) { 209 | result.push(this.settings.DIGNITIES_EXALTATION) 210 | } else if (sign === SIGNS_CAPRICORN) { 211 | result.push(this.settings.DIGNITIES_FALL) 212 | } 213 | 214 | break 215 | 216 | case this.settings.SYMBOL_SATURN: 217 | 218 | if (sign === SIGNS_CAPRICORN || sign === SIGNS_AQUARIUS) { 219 | result.push(this.settings.DIGNITIES_RULERSHIP) 220 | } else if (sign === SIGNS_CANCER || sign === SIGNS_LEO) { 221 | result.push(this.settings.DIGNITIES_DETRIMENT) 222 | } 223 | 224 | if (sign === SIGNS_LIBRA) { 225 | result.push(this.settings.DIGNITIES_EXALTATION) 226 | } else if (sign === SIGNS_ARIES) { 227 | result.push(this.settings.DIGNITIES_FALL) 228 | } 229 | 230 | break 231 | 232 | case this.settings.SYMBOL_URANUS: 233 | 234 | if (sign === SIGNS_AQUARIUS) { 235 | result.push(this.settings.DIGNITIES_RULERSHIP) 236 | } else if (sign === SIGNS_LEO) { 237 | result.push(this.settings.DIGNITIES_DETRIMENT) 238 | } 239 | 240 | if (sign === SIGNS_SCORPIO) { 241 | result.push(this.settings.DIGNITIES_EXALTATION) 242 | } else if (sign === SIGNS_TAURUS) { 243 | result.push(this.settings.DIGNITIES_FALL) 244 | } 245 | 246 | break 247 | 248 | case this.settings.SYMBOL_NEPTUNE: 249 | 250 | if (sign === SIGNS_PISCES) { 251 | result.push(this.settings.DIGNITIES_RULERSHIP) 252 | } else if (sign === SIGNS_VIRGO) { 253 | result.push(this.settings.DIGNITIES_DETRIMENT) 254 | } 255 | 256 | if (sign === SIGNS_LEO || sign === SIGNS_SAGITTARIUS) { 257 | result.push(this.settings.DIGNITIES_EXALTATION) 258 | } else if (sign === SIGNS_AQUARIUS || sign === SIGNS_GEMINI) { 259 | result.push(this.settings.DIGNITIES_FALL) 260 | } 261 | 262 | break 263 | 264 | case this.settings.SYMBOL_PLUTO: 265 | 266 | if (sign === SIGNS_SCORPIO) { 267 | result.push(this.settings.DIGNITIES_RULERSHIP) 268 | } else if (sign === SIGNS_TAURUS) { 269 | result.push(this.settings.DIGNITIES_DETRIMENT) 270 | } 271 | 272 | if (sign === SIGNS_ARIES) { 273 | result.push(this.settings.DIGNITIES_EXALTATION) 274 | } else if (sign === SIGNS_LIBRA) { 275 | result.push(this.settings.DIGNITIES_FALL) 276 | } 277 | break 278 | default: 279 | break 280 | } 281 | 282 | if (exactExaltation != null && Array.isArray(exactExaltation)) { 283 | for (let i = 0, ln = exactExaltation.length; i < ln; i++) { 284 | if (planet.name === exactExaltation[i].name) { 285 | if (this.hasConjunction(planet.position, exactExaltation[i].position, exactExaltation[i].orbit)) { 286 | result.push(this.settings.DIGNITIES_EXACT_EXALTATION) 287 | } 288 | } 289 | } 290 | } 291 | 292 | return result 293 | } 294 | 295 | /* 296 | * To hours:minutes:seconds 297 | * @param {Double} d 298 | * @return {String} 299 | */ 300 | toDMS (d: number): string { 301 | d += 0.5 / 3600.0 / 10000.0 // round to 1/1000 of a second 302 | const deg = parseInt(d.toString(), 10) 303 | d = (d - deg) * 60 304 | const min = parseInt(d.toString(), 10) 305 | const sec = parseInt(((d - min) * 60).toString(), 10) 306 | return deg + '° ' + min + '\' ' + sec 307 | } 308 | 309 | /* 310 | * Has conjunction with point 311 | * 312 | * @private 313 | * 314 | * @param {Double} planetPosition 315 | * @param {Double} poitPosition 316 | * @param {Integer} orbit 317 | * @return {boolean} 318 | */ 319 | hasConjunction (planetPosition: number, pointPosition: number, orbit: number): boolean { 320 | let result = false 321 | 322 | const minOrbit = (pointPosition - orbit / 2) < 0 323 | ? radiansToDegree(2 * Math.PI) - (pointPosition - orbit / 2) 324 | : pointPosition - orbit / 2 325 | 326 | const maxOrbit = (pointPosition + orbit / 2) >= radiansToDegree(2 * Math.PI) 327 | ? (pointPosition + orbit / 2) - radiansToDegree(2 * Math.PI) 328 | : (pointPosition + orbit / 2) 329 | 330 | if (minOrbit > maxOrbit) { // crossing over zero 331 | if (minOrbit >= planetPosition && planetPosition <= minOrbit) { 332 | result = true 333 | } 334 | } else { 335 | if (minOrbit <= planetPosition && planetPosition <= maxOrbit) { 336 | result = true 337 | } 338 | } 339 | 340 | return result 341 | } 342 | } 343 | 344 | export default Zodiac 345 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es5", 7 | "jsx": "react", 8 | "allowJs": true, 9 | "moduleResolution": "node", 10 | "declaration": true, 11 | "strictNullChecks": true 12 | }, 13 | "exclude": ["project/src/**/*.test.ts", "dist"] 14 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: './project/src/index.ts', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.tsx?$/, 9 | use: 'ts-loader', 10 | exclude: [/node_modules/, /.test.ts/], 11 | }, 12 | ], 13 | }, 14 | resolve: { 15 | extensions: ['.tsx', '.ts', '.js'], 16 | }, 17 | mode: 'production', 18 | output: { 19 | clean: true, 20 | path: path.resolve(__dirname, 'dist'), 21 | filename: 'astrochart.js', 22 | library: { 23 | name: 'astrochart', 24 | type: 'umd' 25 | } 26 | }, 27 | } --------------------------------------------------------------------------------