Kathuko › SkySutra › Part 5

Part 5

Tonight's sky

SkySutra's home page: the Moon, a Tonight table, the panchang and a round sky chart with planets
Tonight The Moon, tonight's times, the panchang, your own sky chart plugin and the planets, all worked out in PL/SQL when the page opens.

Parts 1 to 4 built the sky as data. Part 5 puts it on screen: an app that opens dark, three small components you write once and reuse everywhere, a card for today's Moon and panchang, a chart of the sky above your home, and the Tonight page that joins them. You finish with a home page that answers the first question anyone asks of a sky app: what can I see right now?

How Part 5 works

Lesson You build New in APEX
5.1 Dark first Night, Night Vision and Day theme styles; a switch that remembers each person's choice Theme styles with your own CSS; apex_theme.set_session_style
5.2 Template Components Sky Pill, Moon Badge, Planet Card Template component plugins; one component inside another
5.3 Today in the sky The Moon, Tonight and Panchang cards SS_TODAY_V; Value Attribute Pairs
5.4 The sky chart A region plugin that draws the sky as SVG Region plugins; a render procedure in a package
5.5 The Tonight page Planet cards that know whether each planet is up, in daylight, in the Sun's glare, or rising later SS_TONIGHT_PLANETS_V

Before you start

You need Part 4 done, and SS_RISE_SET filled for your home place for today (Lesson 3.5: ss_almanac.refresh(<your place id>)). Download:

File What it is
skysutra-night.css, skysutra-night-vision.css, skysutra-day.css The three styles' colors
skysutra.css Maps the colors onto Universal Theme, plus the components' styles
skysutra-chart.js Draws the sky chart
skysutra_auth.sql Lesson 5.1 adds ss_auth.apply_theme and set_theme
skysutra_tonight.sql Lessons 5.3 and 5.5: two views
skysutra_chart.sql Lesson 5.4: SS_CHART

Lesson 5.1 — Dark first

Outcome: an app that opens dark, can turn red for the eyepiece, and remembers which one each person likes.

The question. Part 0 picked Vita - Dark. Why isn't that enough?

The decision. People use SkySutra outside at night. A white page ruins half an hour of dark adaptation; even a dark blue one dims it. Astronomers use red light, because the eye's rods barely see red. So SkySutra has three looks:

Style When Background Text
Night (the default) Indoors in the evening, planning Midnight #0B1026 Starlight #F5F3EA
Night Vision Outside, at the eyepiece Near-black #0A0000 Red #E8473A; pictures turned red too
Day Daylight, printing Paper #F7F5EF Midnight #141B3C

Each style is Vita (dark or light) plus two of our files: one that sets SkySutra's colors as CSS variables (--ss-bg, --ss-text, --ss-astro …), and skysutra.css, shared by all three, which hands those colors to Universal Theme's own variables (--ut-body-background-color: var(--ss-bg) and so on). Every region, report, button and menu then takes the brand with no CSS on any page. Two colors carry meaning everywhere: blue (--ss-astro) for measured astronomy, saffron (--ss-lore) for tradition.

Build: the three styles

  1. Shared Components › Static Application Files › Create File, and upload the four CSS files into a css directory. APEX makes a .min.css copy of each when you save.

  2. Shared Components › Themes › Universal Theme - 42 › Theme Styles › Create:

    Name Base Style File URLs
    Night Vita - Dark #THEME_FILES#css/Vita-Dark#MIN#.css?v=#APEX_VERSION#
    #APP_FILES#css/skysutra-night#MIN#.css
    #APP_FILES#css/skysutra#MIN#.css
    Night Vision Vita - Dark the same, with skysutra-night-vision
    Day Vita #THEME_FILES#css/Vita#MIN#.css?v=#APEX_VERSION#, then skysutra-day, then skysutra

    The order matters: Vita first, our colors second, the mapping last.

  3. On the theme, set Current Theme Style to Night. Sign out: the sign-in page is now midnight blue with a saffron focus ring.

Build: remembering the choice

SS_USERS.THEME (Part 1) holds NIGHT, NIGHT_VISION or DAY. Run the new skysutra_auth.sql. It adds:

procedure apply_theme;               -- read ss_users.theme, set this session's style
procedure set_theme (p_theme varchar2);  -- save a new choice, then apply it

apply_theme calls apex_theme.set_session_style(p_theme_number => 42, p_name => 'Night Vision'), and both post-authentication procedures now call it, so the style follows you to every device you sign in on.

set_theme looks the person's id up before its update:

procedure set_theme (p_theme varchar2) is
    l_user_id ss_users.id%type := current_user_id;
begin
    update ss_users set theme = p_theme where id = l_user_id;
    apply_theme;
end set_theme;

The first version wrote where id = current_user_id straight in the update, and the first click on Day failed with ORA-04091: table WKSP_BHAARATH.SS_USERS is mutating, trigger/function may not see it. current_user_id is a function that reads SS_USERS; called from the WHERE clause of a statement that is changing SS_USERS, Oracle refuses, because the function could see the table half-updated. A function that reads a table can sit in a query on that table, but not in an insert, update or delete on it: call it first and keep the answer in a variable.

The switch:

  1. Shared Components › Application Processes › Create: Switch theme style, Before Header, code ss_auth.set_theme(substr(:REQUEST, 7));, condition Request is contained in Value THEME_NIGHT,THEME_NIGHT_VISION,THEME_DAY.
  2. Shared Components › Navigation Bar List, three entries before the &APP_USER. menu: Night (fa-moon-o), Night Vision (fa-eye) and Day (fa-sun-o), each with the target URL f?p=&APP_ID.:&APP_PAGE_ID.:&SESSION.:THEME_NIGHT (and THEME_NIGHT_VISION, THEME_DAY). Set each one's List Item CSS Classes (attribute 2) to icon-only, and give it a title and aria-label in Link Attributes (title="Night theme" aria-label="Night theme"). skysutra.css hides the text of icon-only entries visually but keeps it for screen readers, so the top bar shows three small icons, one click each, and hovering shows the name.

Clicking Night Vision reloads the same page with the request THEME_NIGHT_VISION; the process strips THEME_ and saves NIGHT_VISION.

Check yourselfWhy save the choice in SS_USERS and not in a browser cookie?

So it follows the person to their phone at the telescope, not just the laptop where they chose it.

Lesson 5.2 — Template Components

Outcome: three small pieces of HTML, each written once and reused on every page that needs it.

The question. The planets report from Part 3 is a table. A table of five rows and six columns is hard to read on a phone at night. How do we show each planet as a card, the same way on every page?

The decision. A template component: a plugin whose body is an HTML template with placeholders. APEX turns each placeholder into an attribute you map to a column. It can render one row (Partial) or many (Report), and one template component can use another.

Component Shows Used by
Sky Pill Whether something is up: Up now, Up, in daylight, In the Sun's glare, Rises 20:37, Below the horizon Planet Card; later the ISS and stars
Moon Badge A disc lit to the right share, the phase name and percent Today in the sky; the calendar in Part 7
Planet Card Name, Vedic name, a Sky Pill, magnitude and where to look The Tonight page; planet pages in Part 6

Build: Sky Pill

Shared Components › Plug-ins › Create › From Scratch: Name Sky Pill, Internal Name SKYSUTRA.SKY_PILL, Type Template Component, Available as Single (Partial). Partial template:

{case STATE/}
  {when UP/}<span class="ss-pill ss-pill--up">Up now</span>
  {when DAY/}<span class="ss-pill ss-pill--day">Up, in daylight</span>
  {when GLARE/}<span class="ss-pill ss-pill--glare">In the Sun's glare</span>
  {when LATER/}<span class="ss-pill ss-pill--later">Rises #TIME#</span>
  {otherwise/}<span class="ss-pill ss-pill--down">Below the horizon</span>
{endcase/}

Create Plug-in. Under Custom Attributes APEX has already made STATE and TIME from the placeholders (Synchronize from Templates does it again after an edit). The look lives in skysutra.css: the up pill is filled blue, glare is outlined saffron, the rest are outlines.

Build: Moon Badge

Internal name SKYSUTRA.MOON_BADGE, Partial:

<span class="ss-moon-badge" title="#PHASE#, #LIT_PCT#% lit">
  <span class="ss-moon-disc{if ?WANING/} ss-moon-disc--waning{endif/}" style="--lit:#LIT_PCT#%"></span>
  <span class="ss-moon-label">#PHASE# &middot; #LIT_PCT#%</span>
</span>

The disc is one CSS gradient: lit from the right up to --lit, and mirrored when waning. {if ?WANING/} tests whether WANING has a value, so the column must be null when the Moon is waxing; 'N' would count as true.

Build: Planet Card

Internal name SKYSUTRA.PLANET_CARD, available as Partial and Report. Partial template:

<div class="ss-planet ss-astro">
  <div class="ss-planet-head">
    <span class="ss-planet-name">#NAME#</span>{if ?VEDIC_NAME/} <span class="ss-vedic-name">(#VEDIC_NAME#)</span>{endif/}
    {with/}
      STATE:=#STATE#
      TIME:=#RISES#
    {apply SKYSUTRA.SKY_PILL/}
  </div>
  <div class="ss-planet-facts">magnitude #MAGNITUDE#{if ?DIRECTION/} &middot; #ALTITUDE#&deg; up in the #DIRECTION#{endif/}</div>
</div>

{with/} … {apply SKYSUTRA.SKY_PILL/} renders the Sky Pill inside the card, passing it this card's values. Report body <div class="ss-planets">#APEX$ROWS#</div>; report row:

<div class="ss-planet-row" data-id="#APEX$ROW_IDENTIFICATION#">#APEX$PARTIAL#</div>

APEX refuses a report row without #APEX$ROW_IDENTIFICATION#. The Vedic name is in saffron (ss-vedic-name) and the card has a blue edge (ss-astro): one card, both traditions, never mixed up.

Check yourselfWhere do you change the wording "Up now" so it changes on every page?

In the Sky Pill's template, once.

Lesson 5.3 — Today in the sky

Outcome: three cards at the top of the home page: the Moon, tonight's times, and the panchang.

The question. The values come from four packages and two tables (SS_SUN_MOON, SS_VEDIC, SS_RISE_SET, SS_MOON_PHASES). Should each region call them itself?

The decision. One view, SS_TODAY_V, with one row for the signed-in person's home place: the Moon's phase and lit share, WANING ('Y' or null), tithi, nakshatra, vara, sunrise and sunset, when full darkness starts and ends, moonrise and moonset, and the next principal phase. Each region is then a one-line query, and the view can be checked in SQL Commands, where ss_auth.current_user_id is you.

Run skysutra_tonight.sql, then:

select * from ss_today_v;

On our run, at Wesley Chapel on Saturday, October 24, 2026, at 10:30 local time: Waxing gibbous, 97% lit; Shukla Chaturdashi; Uttara Bhadrapada; Shanivar; sunset 18:51; dark from 20:09 until 06:17; next phase Full Moon on Monday the 26th. Astropy agrees: 96.7% lit, and the Moon's sidereal longitude puts it in the 26th nakshatra.

One detail in the view: to join today's Sun row and today's Moon row from SS_RISE_SET, the first try was left join ss_rise_set sun on … and sun.body_id = (select id from ss_bodies where code = 'SUN'), and Oracle said ORA-01799: a column may not be outer-joined to a subquery. The view joins SS_BODIES into a small rs subquery first, then outer-joins that by code.

Build: the three cards

On page 1:

  1. The Moon: region type Moon Badge, source select place_id, lit_pct, phase, waning from ss_today_v. Map Lit Pct, Phase and Waning to their columns. Column span 4.

  2. Tonight: a Classic Report, Start New Row off, column span 4, CSS class ss-astro, template Value Attribute Pairs - Row:

    select l.label, l.value
      from ss_today_v t,
           lateral (select 1 seq, 'Sunset' label, t.sunset value from dual union all
                    select 2, 'Dark from',  t.dark_from  from dual union all
                    select 3, 'Dark until', t.dark_until from dual union all
                    select 4, 'Moonrise',   t.moonrise   from dual union all
                    select 5, 'Moonset',    t.moonset    from dual union all
                    select 6, 'Next',       t.next_phase from dual) l
     order by l.seq
    

    The report template shows the first column as a label and the second as its value. Set Pagination › Type to No Pagination (Show All Rows): six rows don't need next and previous links. Turn off Sortable on both columns, or APEX objects that the query already has an ORDER BY.

  3. Panchang: the same, with CSS class ss-lore, for Vara, Tithi and Nakshatra.

Blue edge for what a telescope can check, saffron for the calendar of tradition.

Check yourselfWhy is WANING 'Y' or null and not 'Y' or 'N'?

Template directives test whether a value is present; 'N' would be present, so every Moon would draw as waning.

Lesson 5.4 — The sky chart

Outcome: a region plugin that draws the sky above your home, right now, as an SVG that stays sharp however far you zoom.

The question. No built-in region draws a star chart. Should the page call a JavaScript library and fetch the stars over REST?

The decision. A small region plugin of our own. The database already knows where every star and planet is (Parts 2 and 3), so the plugin's PL/SQL works out the positions and writes them into the page; 80 lines of JavaScript draw them. No library, no extra round trip, nothing to keep up to date.

Job Where
Which stars, lines and bodies are above the horizon, and where ss_chart.sky_json, PL/SQL
Putting an empty <svg> and that JSON in the page ss_chart.render_region, the plugin's render procedure
Drawing skysutra.skyChart, in skysutra-chart.js

The data

Run skysutra_chart.sql. ss_chart.sky_json(place, moment, faintest magnitude) returns:

{ "at": "2026-10-24T21:00", "place": "Wesley Chapel",
  "stars":  [[50.2, 297.8, 0.03, "Vega"], [14.7, 175.5, 1.74, null], ...],
  "lines":  [[alt1, az1, alt2, az2], ...],
  "bodies": [{"code":"MOON","name":"Moon","alt":41,"az":102.3}, ...] }

Each star is precessed and turned to altitude and azimuth once (Part 3's ss_coord.precess and to_horizon), and kept in a PL/SQL table by star id so the constellation lines can look their ends up without computing them twice. Stars on a figure are included even when they're fainter than the limit, so no figure is missing a corner. Only stars brighter than 1.6 carry a name.

For Wesley Chapel at 9 p.m. on October 24: 407 stars, 368 line segments, and the Moon, Saturn, Uranus and Neptune above the horizon, in 0.9 seconds. Astropy gives the same places to a tenth of a degree: Vega 50.2° up at azimuth 297.8°, the Moon at 41.0° and 102.3°, Saturn at 40.2° and 114.7° (we say 114.8°).

apex_json.write has a trap for arrays. The first try wrote each star's name with apex_json.write(name, true), meaning "write it even when null", and the JSON came out as "Vega":true: with two arguments APEX chose the overload that writes a named boolean. The fix is a one-argument call with a variable: apex_json.write(l_name), which writes null for a null name.

The plugin

  1. Upload skysutra-chart.js into a js directory of Static Application Files.
  2. Plug-ins › Create › From Scratch: Name Sky Chart, Internal Name SKYSUTRA.SKY_CHART, Type Region, API Interface Procedure, Render Procedure/Function Name ss_chart.render_region. Create Plug-in.
  3. File URLs to Load › JavaScript #APP_FILES#js/skysutra-chart#MIN#.js. Apply Changes.
  4. Custom Attributes › Add Attribute: Static ID mag_limit, Label Faintest star (magnitude), Type Number, Required on, Default 4.5.

The render procedure lives in the package, not in the plugin's PL/SQL Code box. Compiled code gets checked when you compile it, can be tested from SQL Commands, and shows up in Git diffs; a plugin only needs its name.

procedure render_region (p_plugin in            apex_plugin.t_plugin,
                         p_region in            apex_plugin.t_region,
                         p_param  in            apex_plugin.t_region_render_param,
                         p_result in out nocopy apex_plugin.t_region_render_result) is
    l_id    varchar2(255) := apex_escape.html_attribute(p_region.static_id);
    l_limit number := nvl(p_region.attributes.get_number('mag_limit'), 4.5);
    ...
begin
    select home_place_id into l_place_id from ss_users where id = ss_auth.current_user_id;
    l_json := sky_json(l_place_id, systimestamp, l_limit);
    sys.htp.p('<div class="ss-chart" id="' || l_id || '_chart"><svg ...></svg>'
           || '<div class="ss-chart-caption"></div><script type="application/json">');
    -- the JSON, 8,000 characters at a time
    sys.htp.p('</script></div>');
    apex_javascript.add_onload_code('skysutra.skyChart("' || l_id || '");');
end;

The JSON goes in a <script type="application/json"> block: the browser doesn't run it, it has no size limit (inline JavaScript strings do), and the drawing code reads it with JSON.parse.

The drawing

The chart is the sky as you see it lying on your back: the zenith in the middle, the horizon as the circle, north at the top and east on the left. A map of the sky overhead is a mirror image of a map of the ground below, which is why every planisphere has east and west "the wrong way round". An object at altitude alt and azimuth az goes at

r = (90 − alt) / 90 × R      x = centre − r·sin(az)      y = centre − r·cos(az)

SVG, not a picture. The first version drew on a <canvas>, which is a bitmap: zoom in on a phone and the stars go soft. SVG is drawing instructions, so the browser redraws it sharp at any zoom, and every star is an element you could later click. The script builds the SVG in a fixed 1000 × 1000 coordinate system (viewBox="0 0 1000 1000") and CSS makes it as wide as the region, up to 720 px. About 400 stars, one path for all the constellation lines, and a dozen labels: small enough for any phone.

Brighter stars get bigger dots; planets are blue, the Sun saffron, the Moon starlight. The colors are CSS classes in skysutra.css (.sc-stars { fill: var(--ss-star); } and so on), not values in the script, so the chart turns red in Night Vision with no extra code. Hover over a planet for its altitude.

On page 1, add a region of type Sky Chart, Sky chart, Static ID sky.

Check yourselfWhy does the chart put east on the left?

It's a view looking up; seen from below, east and west swap places compared with a map looking down.

Lesson 5.5 — The Tonight page

Outcome: planet cards that say, for each planet, whether you can see it now, and if not, why not or when.

The question. A planet above the horizon isn't always visible. What states does a planet card need?

The decision. Five, worked out in a view, SS_TONIGHT_PLANETS_V, in this order:

State Rule Pill
GLARE Above the horizon, within 15° of the Sun In the Sun's glare
DAY Above the horizon while the Sun is up Up, in daylight
UP Above the horizon, Sun down Up now
LATER Below the horizon, rising within 12 hours Rises 20:37
DOWN Anything else Below the horizon

"Rises" is the next rise_at after now in SS_RISE_SET; "Sun is up" means now falls between a Sun row's rise and set.

DAY wasn't in the first version. Checking the page at 10:30 in the morning, it said Jupiter, Mars and Mercury were "Up now": true, and useless. The fix was one more line in the view's case and one more {when DAY/} in the Sky Pill. Test a sky app at noon as well as at midnight.

Saturday morning, October 24, 2026, at Wesley Chapel:

Planet State Why
Venus GLARE 6.5° from the Sun: inferior conjunction is this week
Jupiter, Mars, Mercury DAY Up, but so is the Sun
Saturn, Uranus, Neptune LATER Rising at 17:50, 20:37 and 17:24

Build: planet cards

Change page 1's Planets region (from Part 3) to type Planet Card, Display Report, and the source to:

select id, name, vedic_name, magnitude, altitude, direction, state, rises,
       case code when 'MERCURY' then 1 when 'VENUS' then 2 when 'MARS' then 3 when 'JUPITER' then 4
                 when 'SATURN' then 5 when 'URANUS' then 6 else 7 end as from_the_sun
  from ss_tonight_planets_v

A template component region won't take an ORDER BY in its query ("ORDER BY clause is not supported, use Region Order By instead"). Set Order By › Type to Static Value and the clause to FROM_THE_SUN.

Map each attribute to its column, and set the ID column's Primary Key on, for #APEX$ROW_IDENTIFICATION#. When No Data Found: "Set a home place to see tonight's planets."

The planets keep their order from the Sun, and skysutra.css lays the cards out four to a row (two on a phone): Mercury, Venus, Mars and Jupiter, then Saturn, Uranus and Neptune. Each card's pill says what you can see, so the order doesn't have to; and a planet is always in the same place, which matters more to someone glancing at the page every night.

The home page now reads, top to bottom: the Moon, Tonight, Panchang; the sky chart; the planets; bright stars up now (Part 2); and Live (Part 4).

Two finishing touches for the reports from earlier parts. Bright stars up now and Live each have a row to themselves, so in Attributes › Appearance › Template Options turn on Stretch Report (t-Report--stretch) to fill the width, and set both to No Pagination (Show All Rows): a handful of rows has no second page.

Check yourselfVenus is 32° up at noon. Why does its card say "In the Sun's glare" and not "Up, in daylight"?

GLARE is checked first: 6.5° from the Sun, it is lost in the Sun's light whatever the time.

Checkpoint

The home page is now the Tonight page: dark by default, red at the eyepiece, light by day, and honest about what can actually be seen.

Test it

  1. Sign in: the page is midnight blue. The top bar has three icons, Night, Night Vision and Day; Night Vision turns everything red, including the sky chart and the logo. Sign out and in again: your choice stays.
  2. select * from ss_today_v returns one row with every column filled.
  3. select name, state, rises from ss_tonight_planets_v returns seven rows.
  4. The sky chart shows the horizon circle, N at the top and E on the left, constellation figures, and named bright stars.
  5. Every planet card has a pill; the cards run in order from the Sun, four to a row.

Files

File Used in
skysutra-night.css, skysutra-night-vision.css, skysutra-day.css, skysutra.css 5.1, 5.2
skysutra_auth.sql 5.1 (apply_theme, set_theme)
skysutra_tonight.sql 5.3, 5.5 (SS_TODAY_V, SS_TONIGHT_PLANETS_V)
skysutra_chart.sql, skysutra-chart.js 5.4 (SS_CHART, the drawing)

Exercises

  • Give the Sky Chart plugin a second attribute, Show constellation figures (Yes/No), and skip the lines when it's off.
  • Add the ISS's next pass to the Tonight card from SS_ISS_PASSES, with a Sky Pill showing LATER and its start time.
  • Draw the Moon on the chart as a disc lit to the right share, reusing the Moon Badge's idea in SVG.
  • Stretch: let the chart take a time from a page item (P1_AT) and add a slider for the next six hours.

Review questions

Try each one before you open it. The graded quiz is at the end of the page.

1Why three theme styles and not one dark one?

Night for planning, Night Vision keeps dark adaptation at the eyepiece, Day for daylight and printing

2In what order do a style's CSS files load, and why?

Vita, then our colors, then the mapping, so each can use what came before

3Where is a person's style kept, and what applies it at sign-in?

SS_USERS.THEME; ss_auth.apply_theme, called from post-authentication

4Why does set_theme put current_user_id in a variable before its update?

The function reads SS_USERS; called inside an update of SS_USERS it raises ORA-04091, mutating table

5What's the difference between a template component's Partial and Report?

One row, or many rows each rendered with the partial

6How does Planet Card show a Sky Pill?

{with/} sets STATE and TIME, {apply SKYSUTRA.SKY_PILL/} renders it

7Why is WANING null rather than 'N' when the Moon is waxing?

{if ?WANING/} is true for any value

8Why does SS_TODAY_V exist instead of three queries?

One place for the logic, testable in SQL Commands, one-line regions

9Why does the Sky Chart write its data in a script type="application/json" block?

Not executed, no size limit, read with JSON.parse

10Why keep the render procedure in a package?

Compiled and checked, testable, visible in Git; the plugin only names it

11What made the DAY state necessary?

At noon, planets above the horizon were shown as "Up now" though nobody can see them

Downloads for Part 5

Everything this part uses. The SQL files are plain text: open one in any editor before you run it.

Part 5 quiz

5 questions. Get 4 right to pass. You can retake it as often as you like.