Skip to content

plot

Functionality for visualizing analemma projections on sundial and related phenomena

annotate_analemma_with_hour(ax, hour_offset, planet, dial)

For the given hour, annotate with the time

Note that any non-integer part of the hour offset will be truncated

Source code in src/analemma/plot.py
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
def annotate_analemma_with_hour(
    ax: Axes,
    hour_offset: int,
    planet: orbit.PlanetParameters,
    dial: geom.DialParameters,
):
    """
    For the given hour, annotate with the time

    Note that any non-integer part of the hour offset will be truncated
    """
    if hour_offset % 3 == 0:
        points = _analemma_label_coordinates(hour_offset, planet, dial)
        if points:
            p, ptext = points
            return ax.annotate(
                hour_offset_to_oclock(hour_offset),
                xy=p,
                xytext=ptext,
                arrowprops={"arrowstyle": "-"},
                horizontalalignment="center",
                fontsize=_font_size(ax),
            )
    return None

hour_offset_to_oclock(hour_offset)

Render an integer hour offset (eg +2) as the corresponding time (eg '2pm')

Note that any non-integer part of the hour offset will be truncated

Source code in src/analemma/plot.py
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
def hour_offset_to_oclock(hour_offset: int):
    """
    Render an integer hour offset (eg +2) as the corresponding time (eg '2pm')

    Note that any non-integer part of the hour offset will be truncated
    """
    if hour_offset == 0:
        return "12pm"
    elif hour_offset == -12:
        return "12am"
    elif hour_offset > 0:
        return f"{hour_offset}pm"
    elif hour_offset < 0:
        return f"{12+hour_offset}am"
    else:
        raise Exception(f"hour_offset {hour_offset} doesn't seem to be a number")

plot_analemma(ax, hour_offset, planet, dial, format_string='', **kwargs)

Plot the analemma

Parameters:

Name Type Description Default
ax Axes

matplotlib axes

required
hour_offset float

Number of hours relative to noon, eg -2.25 corresponds to 9:45am

required
planet PlanetParameters

The planet on which the dial is located

required
dial DialParameters

The orientation and location of the sundial

required
Source code in src/analemma/plot.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
def plot_analemma(
    ax: Axes,
    hour_offset: float,
    planet: orbit.PlanetParameters,
    dial: geom.DialParameters,
    format_string: str = "",
    **kwargs,
):
    """
    Plot the analemma

    Parameters:
        ax: matplotlib axes
        hour_offset: Number of hours relative to noon, eg -2.25 corresponds to 9:45am
        planet: The planet on which the dial is located
        dial: The orientation and location of the sundial
    """

    times = planet.T_d * np.arange(0, 365 + 1, dtype=float)
    times += hour_offset * 3600
    ssda = geom.sin_sunray_dialface_angle(times, planet, dial)

    return _plot_analemma_segment(
        ax,
        times[ssda > 0],
        planet,
        dial,
        format_string,
        **kwargs,
    )

plot_analemma_season_segment(ax, season, hour_offset, planet, dial, **kwargs)

Plot the analemma segment for the given season

Parameters:

Name Type Description Default
ax Axes

matplotlib axes

required
season Season

The given season

required
hour_offset float

Number of hours relative to noon, eg -2.25 corresponds to 9:45am

required
planet PlanetParameters

The planet on which the dial is located

required
dial DialParameters

The orientation and location of the sundial

required
Source code in src/analemma/plot.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def plot_analemma_season_segment(
    ax: Axes,
    season: geom.Season,
    hour_offset: float,
    planet: orbit.PlanetParameters,
    dial: geom.DialParameters,
    **kwargs,
):
    """
    Plot the analemma segment for the given season

    Parameters:
        ax: matplotlib axes
        season: The given season
        hour_offset: Number of hours relative to noon, eg -2.25 corresponds to 9:45am
        planet: The planet on which the dial is located
        dial: The orientation and location of the sundial
    """

    times = _analemma_plot_sampling_times(season, hour_offset, planet, dial)
    if times.size == 0:
        return []
    return _plot_analemma_segment(
        ax,
        times,
        planet,
        dial,
        _season_format_strings[season.value],
        **kwargs,
    )

plot_annual_sunray_dialface_angle(ax1, ax2, planet, dial)

Plot the sine of the sunray-dialface angle for each hour in the day over one year

If the sine of the angle between the sun ray and the dial face is greater than zero, the gnomon's shadow may fall on the dial (depending on its size) and therefore part of the analemma may be visible. This defines daytime relative to the dial.

Parameters:

Name Type Description Default
ax1 Axes

A matplotlib axes object to hold plots for the morning hours

required
ax2 Axes

A matplotlib axes object to hold plots for the afternon and evening hours

required
planet PlanetParameters

The planet on which the dial is located

required
dial DialParameters

The orientation and location of the sundial

required
Source code in src/analemma/plot.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
def plot_annual_sunray_dialface_angle(
    ax1: Axes,
    ax2: Axes,
    planet: orbit.PlanetParameters,
    dial: geom.DialParameters,
):
    """
    Plot the sine of the sunray-dialface angle for each hour in the day over one year

    If the sine of the angle between the sun ray and the dial face is greater than zero, the gnomon's
    shadow may fall on the dial (depending on its size) and therefore part of the analemma may be
    visible. This defines daytime relative to the dial.

    Parameters:
        ax1: A matplotlib axes object to hold plots for the morning hours
        ax2: A matplotlib axes object to hold plots for the afternon and evening hours
        planet: The planet on which the dial is located
        dial: The orientation and location of the sundial
    """

    def _accentuate_x_axis(ax):
        ax.plot([0, 365], [0, 0], "k")

    def _plot_sunray_dialface_angle(
        ax,
        begin_hour,
        end_hour,
        planet: orbit.PlanetParameters,
        dial: geom.DialParameters,
    ):
        for hour_offset in np.arange(begin_hour, end_hour):
            times, sines = geom.sunray_dialface_angle_over_one_year(
                planet, dial, hour_offset
            )
            ax.plot(times / 3600 / 24, sines, label=hour_offset_to_oclock(hour_offset))
        _accentuate_x_axis(ax)
        ax.grid()
        ax.legend()
        ax.set_xlabel("Days since perihelion")

    _plot_sunray_dialface_angle(ax1, -12, 0, planet, dial)
    ax1.set_ylabel("Sine of sunray-dialface angle")
    _plot_sunray_dialface_angle(ax2, 0, 12, planet, dial)

plot_hourly_analemmas(ax, planet, dial, title=None, year=None, **kwargs)

Plot one analemma for each hour as seen on the face of a sundial

This function plots several analemmas, one per hour of daytime. The line style shows the season. One line showing the path of the shadow tip during the day for each solstice is also shown (with line style appropriate to the season) and on a horizontal dial forms an envelope marking the longest shadows in Winter and the shortest shadows in Summer. Similarly, the path of the shadow tip on each equinox is shown and appears as a straight line. Moreover, the two straight lines fall on top of each other.

In the legend, the seasons are labelled according to the hemisphere in which the sundial is located, so that for sundials in the southern hemisphere, summer occurs in December.

Parameters:

Name Type Description Default
ax Axes

matplotlib axes planet: The planet on which the dial is located dial: The orientation and location of the

required
sundial title

Title to add to the axes year: Year for which the plot hourly analemmas (defaults to current

required
Source code in src/analemma/plot.py
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
def plot_hourly_analemmas(
    ax: Axes,
    planet: orbit.PlanetParameters,
    dial: geom.DialParameters,
    title: str = None,
    year: int = None,
    **kwargs,
):
    """
    Plot one analemma for each hour as seen on the face of a sundial

    This function plots several analemmas, one per hour of daytime. The line style shows the season. One line showing
    the path of the shadow tip during the day for each solstice is also shown (with line style appropriate to the
    season) and on a horizontal dial forms an envelope marking the longest shadows in Winter and the shortest shadows in
    Summer. Similarly, the path of the shadow tip on each equinox is shown and appears as a straight line. Moreover, the
    two straight lines fall on top of each other.

    In the legend, the seasons are labelled according to the hemisphere in which the sundial is located, so that for
    sundials in the southern hemisphere, summer occurs in December.

    Parameters:
        ax: matplotlib axes planet: The planet on which the dial is located dial: The orientation and location of the
        sundial title: Title to add to the axes year: Year for which the plot hourly analemmas (defaults to current
        year)
    """
    hour_offsets = geom.find_daytime_offsets(planet, dial)

    legend_info = []
    for season in geom.Season:
        segment_lines = []
        for hour in hour_offsets:
            segment_lines += plot_analemma_season_segment(
                ax, season, hour, planet, dial, linewidth=0.75, **kwargs
            )
            annotate_analemma_with_hour(ax, hour, planet, dial)

        if len(segment_lines) > 0:
            legend_info.append((segment_lines[0], season.name))

        plot_season_event_sun_path(
            ax, season, planet, dial, linewidth=0.75, year=year, **kwargs
        )

    # put a circle at the base of the gnomon
    ax.plot(0, 0, "ok")

    handles, labels = zip(*_reorder_legend_info(legend_info))
    labels = _adjust_for_hemisphere(dial, labels)
    ax.legend(handles, labels, fontsize=_font_size(ax))

    if title:
        ax.set_title(title)

plot_season_event_sun_path(ax, season, planet, dial, year=None, **kwargs)

Plot the path of the sun across the dial on the equinox or solstice in the given season

Parameters:

Name Type Description Default
ax Axes

matplotlib axes

required
season Season

The given season

required
planet PlanetParameters

The planet on which the dial is located

required
dial DialParameters

The orientation and location of the sundial

required
year int

The year in which the seasons events fall (defaults to current year)

None
Source code in src/analemma/plot.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def plot_season_event_sun_path(
    ax: Axes,
    season: geom.Season,
    planet: orbit.PlanetParameters,
    dial: geom.DialParameters,
    year: int = None,
    **kwargs,
):
    """
    Plot the path of the sun across the dial on the equinox or solstice in the given season

    Parameters:
        ax: matplotlib axes
        season: The given season
        planet: The planet on which the dial is located
        dial: The orientation and location of the sundial
        year: The year in which the seasons events fall (defaults to current year)
    """

    num_times = 1000

    if not year:
        year = datetime.date.today().year

    season_event = orbit.season_event_info(season.value, year)

    # for an equatorial dial on the equinoxes, the sun ray is parallel to the dial face
    if (
        season.name in ("Spring", "Autumn")
        and abs(dial.d) < 1.0e-5
        and abs(dial.theta - dial.i) < 0.25 / 180 * pi
    ):
        return []

    orbit_day = orbit.orbit_date_to_day(season_event.date)
    day_type = _determine_day_type(planet, dial, orbit_day)
    if day_type == DayType.SunNeverRises:
        return []
    elif day_type == DayType.SunNeverSets:
        start_seconds = planet.T_d * orbit_day
        finish_seconds = start_seconds + planet.T_d
        times = np.linspace(start_seconds, finish_seconds, num_times)
    elif day_type == DayType.SunRisesAndSets:
        sun_times = geom.find_sun_rise_noon_set_relative_to_dial_face(
            orbit_day, planet, dial
        )
        buffer_seconds = 0.1 * 3600
        start_seconds = sun_times.sunrise.absolute_seconds + buffer_seconds
        finish_seconds = sun_times.sunset.absolute_seconds - buffer_seconds
        times = np.linspace(start_seconds, finish_seconds, num_times)
    else:
        raise Exception(
            f"Edge case encountered while plotting solstice or equinox for season {season}"
        )

    psis = planet.rotation_angle(times)

    sigma = season_event.sigma
    x_raw, y_raw = geom.shadow_coords_xy(
        planet.alpha, sigma, psis, dial.iota, dial.theta, dial.i, dial.d
    )

    x, y = dial.trim_coords(x_raw, y_raw)

    # see comment in _calc_analemma_points
    xx = y
    yy = -x

    return ax.plot(
        xx, yy, _season_format_strings[season.value], label=season.name, **kwargs
    )

plot_sunrise_sunset(ax, date, planet, dial)

Visualize sunrise and sunset relative to a sundial

This function adds a line and three points to the given axes. The line is the sine of the angle between sun rays the face of the sundial, over the course of a day. The three points mark sunrise, noon, and sunset, when that angle is zero, maximal, and \(\pi\) respectively.

Parameters:

Name Type Description Default
ax Axes

matplotlib axes

required
date date

The date for which to visual sunrise and sunset

required
planet PlanetParameters

The planet on which the dial is located

required
dial DialParameters

The orientation and location of the sundial

required
Source code in src/analemma/plot.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
def plot_sunrise_sunset(
    ax: Axes,
    date: datetime.date,
    planet: orbit.PlanetParameters,
    dial: geom.DialParameters,
):
    r"""
    Visualize sunrise and sunset relative to a sundial

    This function adds a line and three points to the given axes. The line is the sine of the angle between
    sun rays the face of the sundial, over the course of a day. The three points mark sunrise, noon, and sunset,
    when that angle is zero, maximal, and $\pi$ respectively.

    Parameters:
        ax: matplotlib axes
        date: The date for which to visual sunrise and sunset
        planet: The planet on which the dial is located
        dial: The orientation and location of the sundial
    """
    orbit_day = orbit.orbit_date_to_day(date)
    day_type = _determine_day_type(planet, dial, orbit_day)
    if not day_type == DayType.SunRisesAndSets:
        raise Exception(
            f"Sunrise and sunset events not detected at latitude {pi - dial.theta} on date {date}"
        )

    st = geom.find_sun_rise_noon_set_relative_to_dial_face(orbit_day, planet, dial)

    times = st.sample_times_for_one_day()
    abs_seconds = np.array([st.absolute_seconds for st in times])
    sines = geom.sin_sunray_dialface_angle(abs_seconds, planet, dial)

    ax.plot([st.hours_from_midnight for st in times], sines)
    ax.plot(
        st.sunrise.hours_from_midnight,
        geom.sin_sunray_dialface_angle(st.sunrise.absolute_seconds, planet, dial),
        "sr",
        label="Sunrise",
    )
    ax.plot(
        st.noon.hours_from_midnight,
        geom.sin_sunray_dialface_angle(st.noon.absolute_seconds, planet, dial),
        "og",
        label="Noon",
    )
    ax.plot(
        st.sunset.hours_from_midnight,
        geom.sin_sunray_dialface_angle(st.sunset.absolute_seconds, planet, dial),
        "Db",
        label="Sunset",
    )

    ax.grid()
    ax.set_xlabel("Time in hours since midnight")
    ax.set_ylabel("Sine of sunray-dialface angle")
    ax.set_title(f"Key sundial events on {date}")
    ax.legend()