def get_vsx_obs(star_id, max_num_obs=None, jd_start=None, jd_end=None, num_days=500): """ Downloads observations from AAVSO's webobs for ONE star (not fov), returns MiniDataFrame. If star not in AAVSO's webobs site, return a dataframe with no rows. Columns: target_name, date_string, filter, observer, jd, mag, error. :param star_id: the STAR id (not the fov's name). :param max_num_obs: maximum number of observations to get [int]. -- NOT YET IMPLEMENTED. :param jd_start: optional Julian date. :param jd_end: optional JD. :return: MiniDataFrame containing data for 1 star, 1 row per observation downloaded, (or None if there was some problem). """ # Simpler single multiple-character delimiter adopted Nov 7 2018 per G. Silvis recommendation. parm_ident = '&ident=' + util.make_safe_star_id(star_id) if jd_end is None: jd_end = util.jd_now() parm_tojd = '&tojd=' + '{:20.5f}'.format(jd_end).strip() if jd_start is None: jd_start = jd_end - num_days parm_fromjd = '&fromjd=' + '{:20.5f}'.format(jd_start).strip() parm_delimiter = '&delimiter=' + VSX_DELIMITER url = VSX_OBSERVATIONS_HEADER + parm_ident + parm_tojd + parm_fromjd + parm_delimiter minidataframe = util.MiniDataFrame.from_url(url, delimiter=VSX_DELIMITER) if 'uncert' in minidataframe.column_names(): uncert_value_strings = [ u if u != '' else '0' for u in minidataframe.column('uncert') ] minidataframe.set_column('uncert', uncert_value_strings) if minidataframe_has_data(minidataframe): if minidataframe_data_appear_valid(minidataframe): for column_name in ['JD', 'mag', 'uncert']: minidataframe.to_float(column_name) if minidataframe is None: print('NO DATA: url=\"' + url + '\"' + ' delim=' + VSX_DELIMITER) return minidataframe
def redraw_plot(canvas, mdf, star_id, bands_to_plot, show_errorbars=True, show_grid=True, show_lessthans=False, observer_selected='', highlight_observer=False, plot_observer_only=False, plot_in_jd=True, jd_start=None, jd_end=None, num_days=None): """ Reformat data for matplotlib, then clear and redraw plot area only, and trigger replacement of the old plot by the new plot within the containing tkinter Frame. Do not touch other areas of main page, and do not change any external data. :param plot_in_jd: :param canvas: canvas containing LCG plot [matplotlib FigureCanvasTkAgg object]. :param mdf: data to plot [MiniDataFrame object]. :param star_id: star ID to plot [string]. :param bands_to_plot: AAVSO codes of bands to draw, e.g., ['V', 'Vis.'] [list of strings]. :param show_errorbars: True to plot errorbars with datapoints, else False [boolean]. :param show_grid: True to plot grid behind plot, else False [boolean]. :param show_lessthans: True to plot "less-than" datapoints as normal ones, else omit [boolean]. :param observer_selected: observer code whose observations to highlight ('' means none) [string]. :param highlight_observer: True iff observations from obscode_to_highlight to be highlighted [boolean]. :param plot_observer_only: True iff only observations from obscode_to_highlight to be plotted [boolean]. :param plot_in_jd: True to plot x-axis in Julian Data, False for US-format calendar dates [boolean]. ==== User, via the GUI, needs to supply 2 of the following 3 values to define x-range of plot: :param jd_start: JD to be at plot's left edge [float]. :param jd_end: JD to be at plot's right edge, often the current JD [float]. :param num_days: number of days to plot [int or float] :return [None] """ if mdf.dict is None: message_popup('No observations found for ' + star_id + ' in this date range.') return False if mdf.len() <= 0: message_popup('No observations found for ' + star_id + ' in this date range.') return False # Clean up uncertainty data: uncert = [0.0 if isnan(u) else u for u in mdf.column('uncert')] # set any missing values to zero. uncert = [max(0.0, u) for u in uncert] # set any negatives to zero. mdf.set_column('uncert', uncert) # Remove less-than observations if flag dictates: if not show_lessthans: is_not_lessthan = [f == '0' for f in mdf.column('fainterThan')] mdf = mdf.row_subset(is_not_lessthan) # Construct plot elements: ax = canvas.figure.axes[0] ax.clear() ax.set_title(star_id.upper(), color=PLOT_TITLE_COLOR, fontname='Consolas', fontsize=16, weight='bold') if plot_in_jd: ax.set_xlabel('JD') else: ax.set_xlabel('Date (UTC)') ax.set_ylabel('Magnitude') if show_grid: ax.grid(True, color=GRID_COLOR, zorder=-1000) # zorder->behind everything else. if show_errorbars: is_to_be_drawn = [b in bands_to_plot for b in mdf.column('band')] mdf_to_be_drawn = mdf.row_subset(is_to_be_drawn) if plot_observer_only: is_observer = [ o.upper() == observer_selected.upper() for o in mdf_to_be_drawn.column('by') ] mdf_to_be_drawn = mdf_to_be_drawn.row_subset(is_observer) ax.errorbar( x=mdf_to_be_drawn.column('JD'), y=mdf_to_be_drawn.column('mag'), xerr=0.0, yerr=mdf_to_be_drawn.column('uncert'), fmt='none', ecolor='gray', capsize=2, alpha=1, zorder=+900) # zorder->behind datapoint markers, above grid. legend_handles, legend_labels = [], [] # defaults if no points to plot x_to_highlight, y_to_highlight = [], [] for band in bands_to_plot: band_color = BAND_DEFAULT_COLORS.get(band, BAND_DEFAULT_COLOR_DEFAULT) band_marker = BAND_MARKERS.get(band, BAND_MARKERS_DEFAULT) is_band = [b == band for b in mdf.column('band')] mdf_band = mdf.row_subset(is_band) if plot_observer_only: is_observer = [ o.upper() == observer_selected.upper() for o in mdf_band.column('by') ] mdf_band = mdf_band.row_subset(is_observer) if mdf_band is not None: if mdf_band.len() >= 1: if plot_in_jd: x_plot = mdf_band.column( 'JD') # use Julian Dates just as they are. else: x_plot = [ util.datetime_utc_from_jd(jd) for jd in mdf_band.column('JD') ] # to datetime. y_plot = mdf_band.column('mag') ax.scatter(x=x_plot, y=y_plot, color=band_color, marker=band_marker, s=25, alpha=0.9, zorder=+1000) # zorder->on top. legend_labels.append(band) # Before we leave this band, store x and y for any points to be highlighted for observer: if highlight_observer: if observer_selected is not None: if observer_selected.strip() != '': is_obscode = [ u.upper() == observer_selected.upper() for u in mdf_band.column('by') ] if sum(is_obscode) >= 1: x_highlight_this_band = [ xx for (xx, keep) in zip(x_plot, is_obscode) if keep ] y_highlight_this_band = [ yy for (yy, keep) in zip(y_plot, is_obscode) if keep ] x_to_highlight.extend(x_highlight_this_band) y_to_highlight.extend(y_highlight_this_band) # Plot legend here, before more scatter plots can mess it up: ax.legend(labels=legend_labels, scatterpoints=1, bbox_to_anchor=(0, 1.02, 1, .102), loc=3, ncol=2, borderaxespad=0) # Highlight observer's points, if requested: if len(x_to_highlight) >= 1: ax.scatter(x=x_to_highlight, y=y_to_highlight, color=HIGHLIGHT_COLOR, marker='o', s=200, alpha=0.75, zorder=+800) # under point marker and errorbar. # Compute x-axis limits: if jd_end is None: x_high = util.jd_now() else: x_high = jd_end if jd_start is None: x_low = x_high - num_days else: x_low = jd_start if not plot_in_jd: x_high = util.datetime_utc_from_jd(x_high) x_low = util.datetime_utc_from_jd(x_low) x_range = abs(x_high - x_low) # print(x_low, x_high, x_range) ax.set_xlim(x_low - 0.00 * x_range, x_high + 0.00 * x_range) # Format x-axis labels: if plot_in_jd: set_jd_formatter(ax) else: # Improve default formatter's poor day spacing: x_locator = mpldates.AutoDateLocator() # TODO: remove too-close ticks here (low-priority). ax.xaxis.set_major_locator(x_locator) x_formatter = mpldates.AutoDateFormatter(x_locator) # Quick improvements to x-axis tick labels in calendar mode: x_formatter.scaled[1 / 24.0] = '%m-%d %H:%M' x_formatter.scaled[1 / (24.0 * 60.0)] = '%m-%d %H:%M' ax.xaxis.set_major_formatter(x_formatter) # Rotate all tick labels for readability (date-based labels can be long): for label in ax.get_xticklabels(): # list of matplotlib 'Text' objects label.set_ha("right") label.set_rotation(30) # Arrange the y-axis limits (not trivial as we follow convention of brighter (lesser-value) magnitudes # to be plotted toward top of plot. # We don't use ax.invert_yaxis() as it has side-effect of repeatedly inverting y on successive calls. y_low, y_high = ax.set_ylim() ax.set_ylim(max(y_low, y_high), min(y_low, y_high)) # Used for zoom/pan...declare and register callbacks: def on_xlims_change(axes): if plot_in_jd: set_jd_formatter(ax) # print("on_xlims_change(): ", ax.get_xlim()) def on_ylims_change(axes): # print("on_ylims_change(): ", ax.get_ylim()) pass ax.callbacks.connect('xlim_changed', on_xlims_change) ax.callbacks.connect('ylim_changed', on_ylims_change) # Must be last statement of this function: canvas.draw() pass
def redraw_plot(canvas, mdf, star_id, bands_to_plot, show_errorbars=True, show_grid=True, show_lessthans=False, jd_start=None, jd_end=None, num_days=None): """ Reformat data for matplotlib, then clear and redraw plot area only, and trigger replacement of the old plot by the new plot within the containing tkinter Frame. Do not touch other areas of main page, and do not change any external data. :param canvas: canvas containing LCG plot [matplotlib FigureCanvasTkAgg object]. :param mdf: data to plot [MiniDataFrame object]. :param star_id: star ID to plot [string]. :param bands_to_plot: AAVSO codes of bands to draw, e.g., ['V', 'Vis.'] [list of strings]. :param show_errorbars: True to plot errorbars with datapoints, else False [boolean]. :param show_grid: True to plot grid behind plot, else False [boolean]. :param show_lessthans: True to plot "less-than" datapoints as normal ones, else omit [boolean]. ==== User, via the GUI, needs to supply 2 of the following 3 values to define x-range of plot: :param jd_start: JD to be at plot's left edge [float]. :param jd_end: JD to be at plot's right edge, often the current JD [float]. :param num_days: number of days to plot [int or float] :return [None] """ if mdf.dict is None: message_popup('No observations found for ' + star_id + ' in this date range.') return False if mdf.len() <= 0: message_popup('No observations found for ' + star_id + ' in this date range.') return False # Clean up uncertainty data: uncert = [0.0 if isnan(u) else u for u in mdf.column('uncert')] # set any missing values to zero. uncert = [max(0.0, u) for u in uncert] # set any negatives to zero. mdf.set_column('uncert', uncert) # Remove less-than observations if flag dictates: if not show_lessthans: is_not_lessthan = [f == '0' for f in mdf.column('fainterThan')] mdf = mdf.row_subset(is_not_lessthan) # Construct plot elements: ax = canvas.figure.axes[0] ax.clear() ax.set_title(star_id.upper(), color=PLOT_TITLE_COLOR, fontname='Consolas', fontsize=16, weight='bold') ax.set_xlabel('JD') ax.set_ylabel('Magnitude') if show_grid: ax.grid(True, color=GRID_COLOR, zorder=-1000) # zorder->behind everything else. if show_errorbars: is_to_be_drawn = [b in bands_to_plot for b in mdf.column('band')] mdf_to_be_drawn = mdf.row_subset(is_to_be_drawn) ax.errorbar( x=mdf_to_be_drawn.column('JD'), y=mdf_to_be_drawn.column('mag'), xerr=0.0, yerr=mdf_to_be_drawn.column('uncert'), fmt='none', ecolor='gray', capsize=2, alpha=1, zorder=+900) # zorder->behind datapoint markers, above grid. legend_labels = [] for band in bands_to_plot: band_color = BAND_DEFAULT_COLORS.get(band, BAND_DEFAULT_COLOR_DEFAULT) band_marker = BAND_MARKERS.get(band, BAND_MARKERS_DEFAULT) is_band = [b == band for b in mdf.column('band')] mdf_band = mdf.row_subset(is_band) if sum(is_band) >= 1: ax.scatter(x=mdf_band.column('JD'), y=mdf_band.column('mag'), color=band_color, marker=band_marker, s=25, alpha=0.9, zorder=+1000) # zorder->on top of everything. legend_labels.append(band) if jd_end is None: x_high = util.jd_now() else: x_high = jd_end if jd_start is None: x_low = x_high - num_days else: x_low = jd_start x_range = abs(x_high - x_low) ax.set_xlim(x_low - 0.00 * x_range, x_high + 0.00 * x_range) ax.get_xaxis().get_major_formatter().set_useOffset( False) # possibly improve this later. # To follow convention of brighter (=lesser value) manitudes to be plotted toward plot top. # The next lines are a kludge, due to ax.invert_yaxis() repeatedly inverting on successive calls. y_low, y_high = ax.set_ylim() ax.set_ylim(max(y_low, y_high), min(y_low, y_high)) ax.legend(labels=legend_labels, scatterpoints=1, bbox_to_anchor=(0, 1.02, 1, .102), loc=3, ncol=2, borderaxespad=0) canvas.draw()
def _use_now(self): """ Set GUI's End JD entry box to current JD. """ self.jdend.set('{:20.6f}'.format(jd_now()).strip())
def build_control_frame(self): """ Build the entire tall frame along right side of program's main window. :return: [None] """ # Control Frame: self.control_frame.grid_rowconfigure(1, weight=1) self.control_frame.grid_columnconfigure(0, weight=1) # Subframe 'logo_frame': logo_frame = ttk.Frame(self.control_frame) logo_frame.grid(row=0, column=0, padx=20, pady=8) label_logo = tk.Label(logo_frame, text='\n' + PYLCG_LOGO, font=PYLCG_LOGO_FONT, fg='gray') label_logo.grid(sticky='sew') label_logo = tk.Label(logo_frame, text=PYLCG_SUB_LOGO, font=PYLCG_SUB_LOGO_FONT, fg='black') label_logo.grid(sticky='sew') # Subframe 'control_subframe1': control_subframe1 = ttk.Frame(self.control_frame, padding=5) control_subframe1.grid(row=1, column=0, sticky=tk.N) control_subframe1.grid_columnconfigure(0, weight=1) control_subframe1.grid_columnconfigure(1, weight=1) # ----- Star labelframe: star_labelframe = tk.LabelFrame(control_subframe1, text=' Star ', padx=10, pady=8) star_labelframe.grid(pady=15, sticky='ew') self.star_entered = tk.StringVar() self.star_entered.set('') # self.this_star.trace("w", lambda name, index, mode: print(self.star_entered.get())) self.star_entry = ttk.Entry(star_labelframe, textvariable=self.star_entered) self.star_entry.grid(row=0, columnspan=2, sticky='ew') self.button_plot_this_star = ttk.Button(star_labelframe, text='Plot this star', command=lambda: self._plot_star(self.star_entered.get(), True)) self.button_plot_this_star.grid(row=1, column=0, columnspan=2, sticky='ew') self.star_entry.bind('<Return>', lambda d: self.button_plot_this_star.invoke()) self.prev_star = tk.StringVar() # reserve name for label, later. self.button_prev = ttk.Button(star_labelframe, text='Prev', command=self._prev_star) self.button_prev.grid(row=2, column=0, sticky='e') self.next_star = tk.StringVar() # reserve name for label, later. self.button_next = ttk.Button(star_labelframe, text='Next', command=self._next_star) self.button_next.grid(row=2, column=1, sticky='w') self.button_prev.config(state='disabled') # For now self.button_next.config(state='disabled') # For now # ----- Time span labelframe: timespan_labelframe = tk.LabelFrame(control_subframe1, text=' Time span ', padx=10, pady=8) timespan_labelframe.grid(pady=15, sticky='ew') timespan_labelframe.grid_columnconfigure(1, weight=1) self.days_to_plot = tk.StringVar() self.days_to_plot.set(self.preferences.get('Data preferences', 'Days')) self.days_entry = ttk.Entry(timespan_labelframe, width=6, justify=tk.RIGHT, textvariable=self.days_to_plot) self.days_entry.grid(row=0, column=1, sticky='e') self.days_entry.bind('<Return>', lambda d: self.button_plot_this_star.invoke()) days_label = tk.Label(timespan_labelframe, text=' days') days_label.grid(row=0, column=2) jdstart_label = ttk.Label(timespan_labelframe, text='JD Start: ') jdstart_label.grid(row=1, column=0) self.jdstart = tk.StringVar() self.jdstart.set('') self.jdstart_entry = ttk.Entry(timespan_labelframe, width=12, justify=tk.RIGHT, textvariable=self.jdstart) self.jdstart_entry.grid(row=1, column=1, columnspan=2, sticky='ew') jdend_label = ttk.Label(timespan_labelframe, text='JD End: ') jdend_label.grid(row=2, column=0) self.jdend = tk.StringVar() self.jdend.set('{:20.6f}'.format(jd_now()).strip()) self.jdend_entry = ttk.Entry(timespan_labelframe, width=12, justify=tk.RIGHT, textvariable=self.jdend) self.jdend_entry.grid(row=2, column=1, columnspan=2, sticky='ew') use_now_button = ttk.Button(timespan_labelframe, text='JD End = Now', command=self._use_now) use_now_button.grid(row=3, column=1, columnspan=2, sticky='ew') # ----- Bands labelframe: self.bands_labelframe = tk.LabelFrame(control_subframe1, text=' Bands ', padx=15, pady=10) self.bands_labelframe.grid(pady=10, sticky='ew') self.bands_labelframe.grid_columnconfigure(0, weight=1) self.bands_labelframe.grid_columnconfigure(1, weight=1) self.band_flags = OrderedDict() self.band_flags['U'] = tk.BooleanVar() self.band_flags['V'] = tk.BooleanVar() self.band_flags['B'] = tk.BooleanVar() self.band_flags['R'] = tk.BooleanVar() self.band_flags['I'] = tk.BooleanVar() self.band_flags['Vis.'] = tk.BooleanVar() self.band_flags['TG'] = tk.BooleanVar() self.band_flags['others'] = tk.BooleanVar() self.band_flags['ALL'] = tk.BooleanVar() # Make dict of key=band, value=True iff band has its own checkbutton. band_individually_selectable = [(band, (band in self.band_flags.keys())) for band in ALL_BANDS] self.band_individually_selectable = OrderedDict((key, value) for (key, value) in band_individually_selectable) self.bands_to_plot = self.preferences.get('Data preferences', 'bands') self._set_band_flags_from_preferences() self.band_u_checkbutton = ttk.Checkbutton(self.bands_labelframe, text='U ', variable=self.band_flags['U'], command=self._update_bands_to_plot_then_plot) self.band_b_checkbutton = ttk.Checkbutton(self.bands_labelframe, text='B', variable=self.band_flags['B'], command=self._update_bands_to_plot_then_plot) self.band_v_checkbutton = ttk.Checkbutton(self.bands_labelframe, text='V', variable=self.band_flags['V'], command=self._update_bands_to_plot_then_plot) self.band_r_checkbutton = ttk.Checkbutton(self.bands_labelframe, text='R', variable=self.band_flags['R'], command=self._update_bands_to_plot_then_plot) self.band_i_checkbutton = ttk.Checkbutton(self.bands_labelframe, text='I', variable=self.band_flags['I'], command=self._update_bands_to_plot_then_plot) self.band_vis_checkbutton = ttk.Checkbutton(self.bands_labelframe, text='Vis.', variable=self.band_flags['Vis.'], command=self._update_bands_to_plot_then_plot) self.band_tg_checkbutton = ttk.Checkbutton(self.bands_labelframe, text='TG', variable=self.band_flags['TG'], command=self._update_bands_to_plot_then_plot) self.band_others_checkbutton = ttk.Checkbutton(self.bands_labelframe, text='others', variable=self.band_flags['others'], command=self._update_bands_to_plot_then_plot) self.band_all_checkbutton = ttk.Checkbutton(self.bands_labelframe, text='ALL', variable=self.band_flags['ALL'], command=self._update_bands_to_plot_then_plot) self.band_u_checkbutton.grid(row=0, column=0, sticky='w') self.band_b_checkbutton.grid(row=1, column=0, sticky='w') self.band_v_checkbutton.grid(row=2, column=0, sticky='w') self.band_r_checkbutton.grid(row=3, column=0, sticky='w') self.band_i_checkbutton.grid(row=4, column=0, sticky='w') self.band_vis_checkbutton.grid(row=0, column=1, sticky='w') self.band_tg_checkbutton.grid(row=1, column=1, sticky='w') self.band_others_checkbutton.grid(row=2, column=1, sticky='w') self.band_all_checkbutton.grid(row=4, column=1, sticky='w') # ----- Checkbutton frame: checkbutton_frame = tk.Frame(control_subframe1) checkbutton_frame.grid(sticky='ew') self.grid_flag = tk.BooleanVar() self.errorbar_flag = tk.BooleanVar() self.lessthan_flag = tk.BooleanVar() self.grid_flag.set(True) self.errorbar_flag.set(True) self.lessthan_flag.set(False) self.grid_flag.trace("w", lambda name, index, mode: self._plot_star(self.star_entered.get(), True)) self.errorbar_flag.trace("w", lambda name, index, mode: self._plot_star(self.star_entered.get(), True)) self.lessthan_flag.trace("w", lambda name, index, mode: self._plot_star(self.star_entered.get(), True)) grid_checkbutton = ttk.Checkbutton(checkbutton_frame, text='grid', variable=self.grid_flag) errorbars_checkbutton = ttk.Checkbutton(checkbutton_frame, text='error bars', variable=self.errorbar_flag) lessthan_checkbutton = ttk.Checkbutton(checkbutton_frame, text='less-than obs', variable=self.lessthan_flag) grid_checkbutton.grid(row=0, column=0, sticky='w') errorbars_checkbutton.grid(row=1, column=0, sticky='w') lessthan_checkbutton.grid(row=0, column=1, sticky='w') self.mdf_obs_data = MiniDataFrame() # declare here, as will be depended upon later. # ----- Button frame: button_frame = tk.Frame(control_subframe1, pady=12) button_frame.grid(sticky='ew') button_frame.grid_columnconfigure(0, weight=1) button_frame.grid_columnconfigure(1, weight=1) button_preferences = ttk.Button(button_frame, text='Preferences...', command=self._preferences_window) button_listobservers = ttk.Button(button_frame, text='List Observers', command=self._listobservers_window) button_vsx = ttk.Button(button_frame, text='VSX', command=lambda: web.webbrowse_vsx(self.star_entered.get())) button_webobs = ttk.Button(button_frame, text='Observations', command=lambda: web.webbrowse_webobs(self.star_entered.get())) button_clearcache = ttk.Button(button_frame, text='Clear cache', command=web.get_vsx_obs.cache_clear) button_preferences.grid(row=0, column=0, sticky='ew') button_listobservers.grid(row=1, column=0, sticky='ew') button_vsx.grid(row=0, column=1, sticky='ew') button_webobs.grid(row=1, column=1, sticky='ew') button_clearcache.grid(row=2, column=1, sticky='ew') button_preferences.state(['disabled']) button_listobservers.state(['disabled']) button_vsx.state(['!disabled']) # enabled button_webobs.state(['!disabled']) # enabled button_clearcache.state(['!disabled']) # enabled # Subframe quit_frame: quit_frame = tk.Frame(self.control_frame, height=60) quit_frame.grid_columnconfigure(0, weight=1) quit_frame.grid(row=2, column=0, padx=8, pady=10, sticky='ew') self.quit_button = ttk.Button(quit_frame, text='QUIT pylcg', width=16, cursor='pirate', command=self._quit_window) self.quit_button.grid(row=0, column=0, sticky='ew')